From cefa810ad485603676bf457efb8b03e02f964319 Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 20:44:59 +0100 Subject: [PATCH 01/33] Look up member profile ENS records Adds the GetMemberProfileTextRecordsUseCase along with the supporting infrastructure: MemberProfile domain objects (subdomain + text record value objects), an EnvioMemberProfileStore that resolves the live text records from the indexer, and the AragonController endpoint that fronts the use case. --- .github/workflows/library-snapshot.yml | 18 +- .gitignore | 4 +- AGENTS.md | 107 +- README.md | 4 +- biome.json | 203 +++ jest.config.js | 18 + package.json | 40 +- pnpm-lock.yaml | 1137 +++++++++++++++-- .../MemberProfileAragonName.test.ts | 74 ++ .../member-profile/MemberProfileAragonName.ts | 53 + .../member-profile/MemberProfileStore.ts | 17 + .../MemberProfileTextRecord.test.ts | 35 + .../member-profile/MemberProfileTextRecord.ts | 32 + .../primitives/blockchain/Address.test.ts | 61 + src/domain/primitives/blockchain/Address.ts | 69 + .../primitives/events/SomethingWentWrong.ts | 13 + src/domain/primitives/index.ts | 26 + src/domain/primitives/math/HexNumber.test.ts | 118 ++ src/domain/primitives/math/HexNumber.ts | 72 ++ src/domain/primitives/pagination/Page.ts | 22 + .../primitives/pagination/PageRequest.test.ts | 22 + .../primitives/pagination/PageRequest.ts | 29 + src/domain/primitives/units/EVMUnit.ts | 9 + src/domain/primitives/units/Ether.test.ts | 31 + src/domain/primitives/units/Ether.ts | 37 + src/domain/primitives/units/Gwei.test.ts | 46 + src/domain/primitives/units/Gwei.ts | 63 + src/domain/primitives/units/Wei.test.ts | 29 + src/domain/primitives/units/Wei.ts | 40 + src/domain/primitives/uuid/UUID.test.ts | 37 + src/domain/primitives/uuid/UUID.ts | 28 + .../validation/ZodHexString.test.ts | 59 + .../primitives/validation/ZodHexString.ts | 25 + .../primitives/validation/index.test.ts | 7 + src/domain/primitives/validation/index.ts | 5 + src/index.ts | 7 +- .../AragonController/AragonController.ts | 51 + .../maps/domain/MemberProfileTextRecordMap.ts | 15 + .../GetMemberProfileTextRecordsMap.ts | 18 + src/infrastructure/stores/EnvioClient.ts | 30 + .../EnvioMemberProfileStore.test.ts | 205 +++ .../EnvioMemberProfileStore.ts | 43 + .../maps/MemberProfileTextRecordMap.ts | 82 ++ ...GetMemberProfileTextRecordsUseCase.test.ts | 62 + .../GetMemberProfileTextRecordsUseCase.ts | 37 + test/AragonSubdomain.test.ts | 120 ++ tsconfig.build.json | 13 + tsconfig.json | 16 +- tsup.config.ts | 11 + 49 files changed, 3108 insertions(+), 192 deletions(-) create mode 100644 biome.json create mode 100644 jest.config.js create mode 100644 src/domain/member-profile/MemberProfileAragonName.test.ts create mode 100644 src/domain/member-profile/MemberProfileAragonName.ts create mode 100644 src/domain/member-profile/MemberProfileStore.ts create mode 100644 src/domain/member-profile/MemberProfileTextRecord.test.ts create mode 100644 src/domain/member-profile/MemberProfileTextRecord.ts create mode 100644 src/domain/primitives/blockchain/Address.test.ts create mode 100644 src/domain/primitives/blockchain/Address.ts create mode 100644 src/domain/primitives/events/SomethingWentWrong.ts create mode 100644 src/domain/primitives/index.ts create mode 100644 src/domain/primitives/math/HexNumber.test.ts create mode 100644 src/domain/primitives/math/HexNumber.ts create mode 100644 src/domain/primitives/pagination/Page.ts create mode 100644 src/domain/primitives/pagination/PageRequest.test.ts create mode 100644 src/domain/primitives/pagination/PageRequest.ts create mode 100644 src/domain/primitives/units/EVMUnit.ts create mode 100644 src/domain/primitives/units/Ether.test.ts create mode 100644 src/domain/primitives/units/Ether.ts create mode 100644 src/domain/primitives/units/Gwei.test.ts create mode 100644 src/domain/primitives/units/Gwei.ts create mode 100644 src/domain/primitives/units/Wei.test.ts create mode 100644 src/domain/primitives/units/Wei.ts create mode 100644 src/domain/primitives/uuid/UUID.test.ts create mode 100644 src/domain/primitives/uuid/UUID.ts create mode 100644 src/domain/primitives/validation/ZodHexString.test.ts create mode 100644 src/domain/primitives/validation/ZodHexString.ts create mode 100644 src/domain/primitives/validation/index.test.ts create mode 100644 src/domain/primitives/validation/index.ts create mode 100644 src/infrastructure/controllers/AragonController/AragonController.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/domain/MemberProfileTextRecordMap.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetMemberProfileTextRecordsMap.ts create mode 100644 src/infrastructure/stores/EnvioClient.ts create mode 100644 src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts create mode 100644 src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts create mode 100644 src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts create mode 100644 src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts create mode 100644 src/use-cases/GetMemberProfileTextRecordsUseCase.ts create mode 100644 test/AragonSubdomain.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsup.config.ts diff --git a/.github/workflows/library-snapshot.yml b/.github/workflows/library-snapshot.yml index df93773..05db00f 100644 --- a/.github/workflows/library-snapshot.yml +++ b/.github/workflows/library-snapshot.yml @@ -31,8 +31,6 @@ jobs: # Checkout full history to correclty check changeset status fetch-depth: 0 token: ${{ steps.load-secrets.outputs.ARABOT_PAT_TRIGGER_WORKFLOW }} - - name: Check changeset - run: pnpm changeset status --since origin/main - name: Build library run: pnpm build - name: Update version @@ -48,5 +46,17 @@ jobs: pnpm publish --access public --tag snapshot-${{ github.run_id }} --no-git-checks env: NODE_AUTH_TOKEN: ${{ steps.load-secrets.outputs.NPM_TOKEN }} - - name: Print snapshot version - run: echo "Snapshot release - ${{ steps.package-version.outputs.current-version }} published with tag 'snapshot-${{ github.run_id }}'" + - name: Write snapshot version to summary + run: | + { + echo "## Snapshot Published" + echo "" + echo "**Version:** \`${{ steps.package-version.outputs.current-version }}\`" + echo "**npm tag:** \`snapshot-${{ github.run_id }}\`" + echo "" + echo "Install with:" + echo "" + echo '```bash' + echo "pnpm add @aragon/aragon-subdomain@snapshot-${{ github.run_id }}" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index deed335..a55fb2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -node_modules/ dist/ +node_modules/ +.DS_Store .env +*.tsbuildinfo diff --git a/AGENTS.md b/AGENTS.md index 124fe16..2a21c34 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ This package encapsulates all governance business logic for the Aragon platform. It replaces business logic currently fragmented across `app-backend` (Node.js) and `app` (Next.js frontend), consolidating it into a single importable TypeScript package. -The package sits between the Envio on-chain indexer (`envio-testing/`) and the Next.js BFF layer. It queries indexed data from Envio, applies domain logic (membership aggregation, voting power calculation, permission checks), enriches with external data (ENS, token prices), and exposes use cases via a controller. +The package sits between the Envio on-chain indexer (`aragon-indexer/`) and the Next.js BFF layer. It queries indexed data from Envio, applies domain logic (membership aggregation, voting power calculation, permission checks), enriches with external data (ENS, token prices), and exposes use cases via a controller. ## How to Approach This Repo @@ -48,44 +48,39 @@ Dependencies flow inward: Infrastructure → Use Cases → Domain. The domain la ### Project structure within each layer -Follow the `ddd-core-ts` project structure conventions: +Follow the `ddd-core-ts` project structure conventions. -``` -src/ -├── domain/ -│ ├── member/ -│ │ ├── Member.ts # Value object -│ │ ├── MemberMetrics.ts # Value object -│ │ └── MemberFilter.ts # Value object -│ ├── membership/ -│ │ ├── Membership.ts # Aggregate -│ │ ├── GovernanceType.ts # as const enum -│ │ └── GovernanceTypeResolver.ts # Pure function -│ ├── voting-power/ -│ │ ├── VotingPower.ts -│ │ ├── ERC20VotingPower.ts -│ │ └── VEVotingPower.ts -│ └── ... -│ -├── use-cases/ -│ ├── GetMembership.ts -│ ├── GetERC20Membership.ts -│ ├── GetVEMembership.ts -│ └── ... -│ -└── infrastructure/ - ├── controllers/ - │ └── MemberController/ - │ ├── MemberController.ts - │ └── maps/ - ├── stores/ - │ ├── EnvioTokenVotingDelegationStore/ - │ ├── EnvioVELockStore/ - │ └── ... - └── services/ - ├── EnsResolver.ts - └── TokenPriceProvider.ts -``` +## Formatting Rules + +1. **Indent with 2 spaces.** Configured via biome (`indentWidth: 2`). Run `pnpm run lint` to auto-format. +2. **JSDoc comments use multi-line format:** + ```typescript + /** + * My comment + */ + ``` + Never use single-line JSDoc (`/** My comment */`). +3. **Test files are colocated** with the source files they test: `Member.ts` → `Member.test.ts` in the same directory. Not in a separate `test/` folder. + +## Primitives + +The `src/domain/primitives/` directory contains reusable domain primitives. Always use these instead of raw types: + +| Primitive | Use instead of | Import from | +|-----------|---------------|-------------| +| `Address` | `string` for Ethereum addresses | `@/domain/primitives` | +| `HexString` | `string` for 0x-prefixed hex values | `@/domain/primitives` | +| `HexNumber` | `string` or `bigint` for hex-encoded numbers | `@/domain/primitives` | +| `Wei` / `Gwei` / `Ether` | `BigNumber` for EVM unit values | `@/domain/primitives` | +| `UUID` | `string` for UUIDs | `@/domain/primitives` | +| `Page` / `PageRequest` | ad-hoc pagination shapes | `@/domain/primitives` | + +**Rules:** +- **Use `Address` for all Ethereum addresses** in domain objects and use cases. Never store addresses as raw strings in domain types. +- **Use `HexString` as the parameter type** for functions that accept 0x-prefixed hex strings (e.g., `Address.fromHexString(hex: HexString)`). This catches invalid inputs at compile time. +- **Cast to `HexString` at infrastructure boundaries** where external data enters as `string` (e.g., DTOs from Envio). Use `as HexString` in mappers — the runtime validation happens inside the primitive's factory method. +- **Serialize via `.toHexString()`** when converting domain objects to DTOs for external consumers. +- Import primitives from the barrel `@/domain/primitives`, not from individual files. ## Key Rules @@ -102,19 +97,45 @@ src/ ## Data Sources -The primary data source is the Envio indexer (`envio-testing/`), which provides: +The primary data source is the Envio indexer (`aragon-indexer/`), which is partitioned internally into a generic / Aragon split (see `aragon-indexer/CLAUDE.md` → "Split path"): -- `TokenVotingDelegation` — ERC20Votes delegation state (delegator, delegate, voting power) -- `VELock` — Voting escrow lock positions (amount, status, delegation) +Generic (chain-wide ERC20Votes): +- `ERC20VotesDelegate` — per-(token, delegate) state: voting power, delegationCount, first/lastVotingPowerChangeTimestamp +- `Delegation` — per-(token, delegator) → currentDelegate + +Aragon-specific: - `Plugin` — Installed governance plugins (type, associated contracts) -- `VESettings` — Escrow configuration (curve parameters) -- `MemberMetrics` — Vote/proposal activity counts +- `VELock` — Voting escrow lock positions (amount, status, delegation) +- `MemberMetrics` — In-plugin engagement (VoteCast / ProposalCreated activity only) + +`EnvioTokenVotingMemberStore.findMembersByPluginAndToken` is the join point between the two halves: it pulls `ERC20VotesDelegate` rows and `MemberMetrics` rows in one query and merges activity timestamps client-side via `min`/`max`. If the indexer is ever split into two separate Envio projects, this is the file that gets a second `EnvioClient` argument and runs two queries instead of one — no other code change required. Infrastructure adapters query this data via Envio's GraphQL/SQL API. Additional data comes from: - ENS resolution (name, avatar) - On-chain RPC reads (real-time token balance, current voting power) - Token price feeds (USD conversion) +## Mapper function naming + +Every mapper module — controller-side and store-side — exposes its mapping conversion under one of two canonical names: + +- `mapDTOToDomain(...)` for the DTO → domain direction. +- `mapDomainToDTO(...)` for the domain → DTO direction. + +These names match what `ddd-core-ts`'s `handleRequest` looks up on controller-side mapper modules, so using them everywhere also keeps the controller and store layers consistent. Auxiliary helpers within a mapper (private parsers, ID-extraction utilities, etc.) keep descriptive names — the rule applies to the conversion function the rest of the codebase calls. + +## GraphQL response handling + +Anything returned by `EnvioClient.query` is `unknown`. The store's job is orchestration — form variables, call the client, hand the raw response off, combine results across queries, wrap errors. Shape assertions and DTO → domain conversion happen in mappers, which are the single trust boundary for indexer data. + +Each query has a corresponding mapper module under the store's `maps/` directory. The mapper owns: + +1. Zod schemas describing the expected response shape, with TypeScript types derived from those schemas via `z.infer<...>` so the schema is the single source of truth. +2. A `mapDTOToDomain(raw: unknown)` function that calls `.parse()` on the raw input and returns the domain objects. The Zod parse failure becomes a descriptive runtime error if the indexer's response shape ever drifts. +3. DTO → domain mapping. When the mapper passes raw inputs into a domain factory (`Address.fromHexString`, `TokenVotingMember.create`, etc.), the factory's own validation is the final shape check on the values inside the response. + +Stores never declare DTO interfaces inline and never pass a type argument to `query`. Calling `await this.envio.query(QUERY, vars)` produces `unknown`; that goes straight into the mapper's `mapDTOToDomain` function. + ## Testing - Unit tests for domain logic (value objects, aggregates, pure functions) diff --git a/README.md b/README.md index 8774842..e298062 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Based on the [Aragon Governance Membership Domain Model](https://www.figma.com/b ## Data Flow ``` -On-chain events → Envio (envio-testing/) → aragon-subdomain → Next.js BFF → Frontend +On-chain events → Envio (aragon-indexer/) → aragon-subdomain → Next.js BFF → Frontend ``` - **Envio** indexes raw on-chain state (delegations, locks, plugin installations) @@ -82,7 +82,7 @@ pnpm run test | Package | Path | Role | |---------|------|------| -| `envio-testing` | `/envio-testing` | Envio indexer for on-chain event data | +| `aragon-indexer` | `/aragon-indexer` | Envio indexer for on-chain event data | | `app-backend` | `/app-backend` | Legacy backend (being replaced) | | `app` | `/app` | Next.js frontend | | `ve-governance-indexer` | `/ve-governance-indexer` | Production VE governance indexer (reference) | diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..836c691 --- /dev/null +++ b/biome.json @@ -0,0 +1,203 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "includes": [ + "**", + "!*.json", + "!*.jsonc" + ] + }, + "json": { + "formatter": { + "indentStyle": "space", + "indentWidth": 2, + "expand": "always" + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "trailingCommas": "all" + }, + "globals": [ + "jest", + "describe", + "it", + "test", + "expect", + "beforeEach", + "afterEach", + "beforeAll", + "afterAll" + ] + }, + "linter": { + "rules": { + "recommended": true, + "complexity": { + "noUselessStringConcat": "warn", + "useLiteralKeys": "off" + }, + "correctness": { + "noUndeclaredVariables": "error" + }, + "style": { + "noCommonJs": "error", + "noInferrableTypes": "error", + "noNamespace": "error", + "noNonNullAssertion": "off", + "useAsConstAssertion": "error", + "useBlockStatements": "warn", + "useConsistentArrayType": "error", + "useConsistentTypeDefinitions": { + "level": "warn", + "options": { + "style": "interface" + } + }, + "useForOf": "error", + "useTemplate": "warn", + "useUnifiedTypeSignatures": "error" + }, + "suspicious": { + "noAssignInExpressions": "off", + "noConsole": "warn", + "noConstantBinaryExpressions": "error", + "noEmptyBlockStatements": "error", + "noEmptyInterface": "off", + "noImplicitAnyLet": "off", + "useAwait": "error", + "useIterableCallbackReturn": "off" + }, + "performance": { + "noAccumulatingSpread": "off" + } + }, + "includes": [ + "**", + "!node_modules", + "!dist/**/*", + "!coverage" + ] + }, + "overrides": [ + { + "includes": [ + "**/*.ts", + "**/*.mts", + "**/*.cts" + ], + "linter": { + "rules": { + "complexity": { + "noArguments": "error" + }, + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidBuiltinInstantiation": "off", + "noInvalidConstructorSuper": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { + "useConst": "error" + }, + "suspicious": { + "noClassAssign": "off", + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "noVar": "error", + "noWith": "off", + "useGetterReturn": "off" + } + } + } + }, + { + "includes": [ + "**/*.test.ts" + ], + "linter": { + "rules": {} + } + }, + { + "includes": [ + "src/domain/**" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "@/use-cases/**" + ], + "message": "Domain layer must not import from use-cases. Dependencies flow inward: infrastructure → use-cases → domain." + }, + { + "group": [ + "@/infrastructure/**" + ], + "message": "Domain layer must not import from infrastructure. Dependencies flow inward: infrastructure → use-cases → domain." + } + ] + } + } + } + } + } + }, + { + "includes": [ + "src/use-cases/**" + ], + "linter": { + "rules": { + "style": { + "noRestrictedImports": { + "level": "error", + "options": { + "patterns": [ + { + "group": [ + "@/infrastructure/**" + ], + "message": "Use-cases layer must not import from infrastructure. Dependencies flow inward: infrastructure → use-cases → domain." + } + ] + } + } + } + } + } + } + ], + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on", + "useSortedAttributes": "on" + } + } + } +} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..b3d0cba --- /dev/null +++ b/jest.config.js @@ -0,0 +1,18 @@ +/** @type {import('jest').Config} */ +// biome-ignore lint/style/noCommonJs: Jest requires CJS config +module.exports = { + testEnvironment: 'node', + roots: ['/src', '/test'], + forceExit: true, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + tsconfig: 'tsconfig.json', + }, + ], + }, + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; diff --git a/package.json b/package.json index 5210ee7..6eb84ad 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,27 @@ "version": "0.1.0", "description": "Aragon governance business logic package — domain model, use cases, and infrastructure adapters", "main": "dist/index.js", + "module": "dist/index.mjs", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, "license": "GPL-3.0", "author": "Aragon Association", "files": [ "dist" ], "scripts": { - "build": "tsc --build", - "clean": "tsc --build --clean", - "watch": "tsc --watch", - "test": "jest --passWithNoTests", - "test:watch": "jest --watch --passWithNoTests", + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rm -rf dist", + "lint": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", + "test": "jest --watchAll=false", + "test:watch": "jest --watch", "type-check": "tsc --noEmit", "changeset": "changeset" }, @@ -27,18 +36,23 @@ }, "homepage": "https://github.com/aragon/aragon-subdomain#readme", "dependencies": { - "ddd-core-ts": "^0.1.1", - "zod": "^3.23.0" + "bignumber.js": "11.0.0", + "ddd-core-ts": "0.1.1", + "graphql": "16.13.2", + "graphql-request": "7.4.0", + "js-sha3": "0.9.3", + "zod": "3.23.0" }, "devDependencies": { + "@biomejs/biome": "2.3.11", "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.30.0", - "@types/jest": "^29.5.0", - "@types/node": "^20.8.8", - "jest": "^29.7.0", - "prettier": "^3.3.3", - "ts-jest": "^29.2.0", - "typescript": "^5.4.0" + "@types/jest": "29.5.0", + "@types/node": "20.8.8", + "jest": "29.7.0", + "ts-jest": "29.2.0", + "tsup": "8.5.1", + "typescript": "5.4.5" }, "engines": { "node": ">=22.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab99215..335f8c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,37 +8,52 @@ importers: .: dependencies: + bignumber.js: + specifier: 11.0.0 + version: 11.0.0 ddd-core-ts: - specifier: ^0.1.1 + specifier: 0.1.1 version: 0.1.1 + graphql: + specifier: 16.13.2 + version: 16.13.2 + graphql-request: + specifier: 7.4.0 + version: 7.4.0(graphql@16.13.2) + js-sha3: + specifier: 0.9.3 + version: 0.9.3 zod: - specifier: ^3.23.0 - version: 3.25.76 + specifier: 3.23.0 + version: 3.23.0 devDependencies: + '@biomejs/biome': + specifier: 2.3.11 + version: 2.3.11 '@changesets/changelog-github': specifier: ^0.6.0 version: 0.6.0 '@changesets/cli': specifier: ^2.30.0 - version: 2.31.0(@types/node@20.19.41) + version: 2.31.0(@types/node@20.8.8) '@types/jest': - specifier: ^29.5.0 - version: 29.5.14 + specifier: 29.5.0 + version: 29.5.0 '@types/node': - specifier: ^20.8.8 - version: 20.19.41 + specifier: 20.8.8 + version: 20.8.8 jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.41) - prettier: - specifier: ^3.3.3 - version: 3.8.3 + specifier: 29.7.0 + version: 29.7.0(@types/node@20.8.8) ts-jest: - specifier: ^29.2.0 - version: 29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.41))(typescript@5.9.3) + specifier: 29.2.0 + version: 29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5) + tsup: + specifier: 8.5.1 + version: 8.5.1(typescript@5.4.5) typescript: - specifier: ^5.4.0 - version: 5.9.3 + specifier: 5.4.5 + version: 5.4.5 packages: @@ -211,6 +226,63 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@biomejs/biome@2.3.11': + resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.11': + resolution: {integrity: sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.11': + resolution: {integrity: sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.11': + resolution: {integrity: sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.3.11': + resolution: {integrity: sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.3.11': + resolution: {integrity: sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.3.11': + resolution: {integrity: sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.3.11': + resolution: {integrity: sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.11': + resolution: {integrity: sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@changesets/apply-release-plan@7.1.1': resolution: {integrity: sha512-9qPCm/rLx/xoOFXIHGB229+4GOL76S4MC+7tyOuTsR6+1jYlfFDQORdvwR5hDA6y4FL2BPt3qpbcQIS+dW85LA==} @@ -272,6 +344,167 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -389,6 +622,144 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -410,6 +781,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -422,14 +796,14 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/jest@29.5.0': + resolution: {integrity: sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==} '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@20.19.41': - resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + '@types/node@20.8.8': + resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -440,6 +814,11 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -460,6 +839,9 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -511,6 +893,9 @@ packages: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bignumber.js@11.0.0: + resolution: {integrity: sha512-pkOSwKRi2HLwEbmTNf7C0M9lw3Z5dXQ1irbanJvySh/nJQBcFsMxF+g5o2JEn5BPFmfAqYalrhOwoFdLJXohEg==} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -533,6 +918,16 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -559,6 +954,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -584,9 +983,20 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -667,6 +1077,11 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -708,6 +1123,15 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -716,6 +1140,9 @@ packages: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -766,10 +1193,14 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - handlebars@4.7.9: - resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} - engines: {node: '>=0.4.7'} - hasBin: true + graphql-request@7.4.0: + resolution: {integrity: sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==} + peerDependencies: + graphql: 14 - 16 + + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -1009,6 +1440,13 @@ packages: node-notifier: optional: true + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-sha3@0.9.3: + resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1044,9 +1482,17 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -1060,6 +1506,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} @@ -1088,8 +1537,8 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} @@ -1098,12 +1547,12 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -1127,6 +1576,10 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1187,6 +1640,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1194,6 +1650,10 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} @@ -1206,16 +1666,32 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - prettier@3.8.3: - resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} - engines: {node: '>=14'} - hasBin: true - pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -1240,6 +1716,10 @@ packages: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1265,6 +1745,11 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -1312,6 +1797,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -1350,6 +1839,11 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1370,6 +1864,20 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1380,19 +1888,25 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - ts-jest@29.4.9: - resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-jest@29.2.0: + resolution: {integrity: sha512-eFmkE9MG0+oT6nqSOcUwL+2UUmK2IvhhUV8hFDsCHnc++v2WCCbQQZh5vvjsa8sgOY/g9T0325hmkEmi6rninA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 + '@jest/transform': ^29.0.0 + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <7' + jest: ^29.0.0 + typescript: '>=4.3 <6' peerDependenciesMeta: '@babel/core': optional: true @@ -1404,7 +1918,24 @@ packages: optional: true esbuild: optional: true - jest-util: + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: optional: true type-detect@4.0.8: @@ -1415,22 +1946,16 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@5.4.5: + resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} hasBin: true - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@5.25.3: + resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -1460,9 +1985,6 @@ packages: engines: {node: '>= 8'} hasBin: true - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -1493,8 +2015,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@3.23.0: + resolution: {integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==} snapshots: @@ -1689,6 +2211,41 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@biomejs/biome@2.3.11': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.11 + '@biomejs/cli-darwin-x64': 2.3.11 + '@biomejs/cli-linux-arm64': 2.3.11 + '@biomejs/cli-linux-arm64-musl': 2.3.11 + '@biomejs/cli-linux-x64': 2.3.11 + '@biomejs/cli-linux-x64-musl': 2.3.11 + '@biomejs/cli-win32-arm64': 2.3.11 + '@biomejs/cli-win32-x64': 2.3.11 + + '@biomejs/cli-darwin-arm64@2.3.11': + optional: true + + '@biomejs/cli-darwin-x64@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-arm64@2.3.11': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.11': + optional: true + + '@biomejs/cli-linux-x64@2.3.11': + optional: true + + '@biomejs/cli-win32-arm64@2.3.11': + optional: true + + '@biomejs/cli-win32-x64@2.3.11': + optional: true + '@changesets/apply-release-plan@7.1.1': dependencies: '@changesets/config': 3.1.4 @@ -1726,7 +2283,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.31.0(@types/node@20.19.41)': + '@changesets/cli@2.31.0(@types/node@20.8.8)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -1742,7 +2299,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@20.19.41) + '@inquirer/external-editor': 1.0.3(@types/node@20.8.8) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -1847,12 +2404,94 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@inquirer/external-editor@1.0.3(@types/node@20.19.41)': + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@graphql-typed-document-node/core@3.2.0(graphql@16.13.2)': + dependencies: + graphql: 16.13.2 + + '@inquirer/external-editor@1.0.3(@types/node@20.8.8)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 20.19.41 + '@types/node': 20.8.8 '@istanbuljs/load-nyc-config@1.1.0': dependencies: @@ -1867,7 +2506,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1880,14 +2519,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 3.9.0 exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.41) + jest-config: 29.7.0(@types/node@20.8.8) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1912,7 +2551,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1930,7 +2569,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.19.41 + '@types/node': 20.8.8 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1952,7 +2591,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit: 0.1.2 @@ -2022,7 +2661,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 20.19.41 + '@types/node': 20.8.8 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -2073,6 +2712,81 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@rollup/rollup-android-arm-eabi@4.60.2': + optional: true + + '@rollup/rollup-android-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-x64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true + '@sinclair/typebox@0.27.10': {} '@sinonjs/commons@3.0.1': @@ -2104,9 +2818,11 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/estree@1.0.8': {} + '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 20.19.41 + '@types/node': 20.8.8 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2118,16 +2834,16 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 - '@types/jest@29.5.14': + '@types/jest@29.5.0': dependencies: expect: 29.7.0 pretty-format: 29.7.0 '@types/node@12.20.55': {} - '@types/node@20.19.41': + '@types/node@20.8.8': dependencies: - undici-types: 6.21.0 + undici-types: 5.25.3 '@types/stack-utils@2.0.3': {} @@ -2137,6 +2853,8 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + acorn@8.16.0: {} + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -2151,6 +2869,8 @@ snapshots: ansi-styles@5.2.0: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -2227,6 +2947,8 @@ snapshots: dependencies: is-windows: 1.0.2 + bignumber.js@11.0.0: {} + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -2254,6 +2976,13 @@ snapshots: buffer-from@1.1.2: {} + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + callsites@3.1.0: {} camelcase@5.3.1: {} @@ -2271,6 +3000,10 @@ snapshots: chardet@2.1.1: {} + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + ci-info@3.9.0: {} cjs-module-lexer@1.4.3: {} @@ -2291,17 +3024,23 @@ snapshots: color-name@1.1.4: {} + commander@4.1.1: {} + concat-map@0.0.1: {} + confbox@0.1.8: {} + + consola@3.4.2: {} + convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@20.19.41): + create-jest@29.7.0(@types/node@20.8.8): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.41) + jest-config: 29.7.0(@types/node@20.8.8) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2359,6 +3098,35 @@ snapshots: es-errors@1.3.0: {} + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-string-regexp@2.0.0: {} @@ -2407,6 +3175,10 @@ snapshots: dependencies: bser: 2.1.1 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -2416,6 +3188,12 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.60.2 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -2467,14 +3245,12 @@ snapshots: graceful-fs@4.2.11: {} - handlebars@4.7.9: + graphql-request@7.4.0(graphql@16.13.2): dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + graphql: 16.13.2 + + graphql@16.13.2: {} has-flag@4.0.0: {} @@ -2589,7 +3365,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.2 @@ -2609,16 +3385,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.19.41): + jest-cli@29.7.0(@types/node@20.8.8): dependencies: '@jest/core': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.41) + create-jest: 29.7.0(@types/node@20.8.8) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.41) + jest-config: 29.7.0(@types/node@20.8.8) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2628,7 +3404,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.19.41): + jest-config@29.7.0(@types/node@20.8.8): dependencies: '@babel/core': 7.29.0 '@jest/test-sequencer': 29.7.0 @@ -2653,7 +3429,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 20.19.41 + '@types/node': 20.8.8 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2682,7 +3458,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2692,7 +3468,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 20.19.41 + '@types/node': 20.8.8 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2731,7 +3507,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2766,7 +3542,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2794,7 +3570,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 cjs-module-lexer: 1.4.3 collect-v8-coverage: 1.0.3 @@ -2840,7 +3616,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -2859,7 +3635,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.19.41 + '@types/node': 20.8.8 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -2868,23 +3644,27 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 20.19.41 + '@types/node': 20.8.8 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.19.41): + jest@29.7.0(@types/node@20.8.8): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.41) + jest-cli: 29.7.0(@types/node@20.8.8) transitivePeerDependencies: - '@types/node' - babel-plugin-macros - supports-color - ts-node + joycon@3.1.1: {} + + js-sha3@0.9.3: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -2910,8 +3690,12 @@ snapshots: leven@3.1.0: {} + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} + load-tsconfig@0.2.5: {} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 @@ -2924,6 +3708,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@4.0.0: dependencies: semver: 7.8.0 @@ -2949,15 +3737,24 @@ snapshots: dependencies: brace-expansion: 1.1.14 - minimist@1.2.8: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 mri@1.2.0: {} ms@2.1.3: {} - natural-compare@1.4.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 - neo-async@2.6.2: {} + natural-compare@1.4.0: {} node-fetch@2.7.0: dependencies: @@ -2973,6 +3770,8 @@ snapshots: dependencies: path-key: 3.1.1 + object-assign@4.1.1: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3024,10 +3823,14 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} + picomatch@4.0.4: {} + pify@4.0.1: {} pirates@4.0.7: {} @@ -3036,9 +3839,17 @@ snapshots: dependencies: find-up: 4.1.0 - prettier@2.8.8: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 - prettier@3.8.3: {} + postcss-load-config@6.0.1: + dependencies: + lilconfig: 3.1.3 + + prettier@2.8.8: {} pretty-format@29.7.0: dependencies: @@ -3066,6 +3877,8 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + readdirp@4.1.2: {} + require-directory@2.1.1: {} resolve-cwd@3.0.0: @@ -3085,6 +3898,37 @@ snapshots: reusify@1.1.0: {} + rollup@4.60.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -3118,6 +3962,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + spawndamnit@3.0.1: dependencies: cross-spawn: 7.0.6 @@ -3152,6 +3998,16 @@ snapshots: strip-json-comments@3.1.1: {} + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.16 + ts-interface-checker: 0.1.13 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3170,6 +4026,21 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -3178,38 +4049,64 @@ snapshots: tr46@0.0.3: {} - ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.41))(typescript@5.9.3): + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + ts-jest@29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.9 - jest: 29.7.0(@types/node@20.19.41) + jest: 29.7.0(@types/node@20.8.8) + jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.8.0 - type-fest: 4.41.0 - typescript: 5.9.3 + typescript: 5.4.5 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.29.0) - jest-util: 29.7.0 + + tsup@8.5.1(typescript@5.4.5): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.60.2 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tree-kill: 1.2.2 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml type-detect@4.0.8: {} type-fest@0.21.3: {} - type-fest@4.41.0: {} + typescript@5.4.5: {} - typescript@5.9.3: {} + ufo@1.6.3: {} - uglify-js@3.19.3: - optional: true - - undici-types@6.21.0: {} + undici-types@5.25.3: {} universalify@0.1.2: {} @@ -3240,8 +4137,6 @@ snapshots: dependencies: isexe: 2.0.0 - wordwrap@1.0.0: {} - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -3273,4 +4168,4 @@ snapshots: yocto-queue@0.1.0: {} - zod@3.25.76: {} + zod@3.23.0: {} diff --git a/src/domain/member-profile/MemberProfileAragonName.test.ts b/src/domain/member-profile/MemberProfileAragonName.test.ts new file mode 100644 index 0000000..9b489e4 --- /dev/null +++ b/src/domain/member-profile/MemberProfileAragonName.test.ts @@ -0,0 +1,74 @@ +import { MemberProfileAragonName } from './MemberProfileAragonName'; + +describe('MemberProfileAragonName', () => { + it('accepts a single-label subdomain of aragon.eth', () => { + const name = MemberProfileAragonName.fromString('ea1.aragon.eth'); + expect(name.toString()).toBe('ea1.aragon.eth'); + }); + + it('accepts a multi-segment label', () => { + const name = MemberProfileAragonName.fromString('foo.bar.aragon.eth'); + expect(name.toString()).toBe('foo.bar.aragon.eth'); + }); + + it('lowercases the input', () => { + const name = MemberProfileAragonName.fromString('EA1.Aragon.ETH'); + expect(name.toString()).toBe('ea1.aragon.eth'); + }); + + it('trims surrounding whitespace', () => { + const name = MemberProfileAragonName.fromString(' ea1.aragon.eth '); + expect(name.toString()).toBe('ea1.aragon.eth'); + }); + + it('NFC-normalizes the input', () => { + // Both render as "café" but differ in length and bytes. + const decomposed = 'caf\u0065\u0301.aragon.eth'; + const composed = 'caf\u00e9.aragon.eth'; + + expect(decomposed).not.toBe(composed); + expect(decomposed.length).toBe(composed.length + 1); + + const name = MemberProfileAragonName.fromString(decomposed); + expect(name.toString()).toBe(composed); + }); + + it('rejects names that do not end in .aragon.eth', () => { + expect(() => MemberProfileAragonName.fromString('vitalik.eth')).toThrow( + /\.aragon\.eth/, + ); + }); + + it('rejects the bare parent domain', () => { + expect(() => MemberProfileAragonName.fromString('aragon.eth')).toThrow( + /label/, + ); + }); + + it('rejects an empty label before the parent', () => { + expect(() => MemberProfileAragonName.fromString('.aragon.eth')).toThrow( + /label/, + ); + }); + + it('rejects empty input', () => { + expect(() => MemberProfileAragonName.fromString('')).toThrow(); + }); + + it('two equal names are structurally equal', () => { + const a = MemberProfileAragonName.fromString('ea1.aragon.eth'); + const b = MemberProfileAragonName.fromString('EA1.aragon.eth'); + expect(a.equals(b)).toBe(true); + }); + + it('two different names are not structurally equal', () => { + const a = MemberProfileAragonName.fromString('ea1.aragon.eth'); + const b = MemberProfileAragonName.fromString('ea2.aragon.eth'); + expect(a.equals(b)).toBe(false); + }); + + it('toString returns the canonical value', () => { + const name = MemberProfileAragonName.fromString('EA1.Aragon.ETH'); + expect(name.toString()).toBe('ea1.aragon.eth'); + }); +}); diff --git a/src/domain/member-profile/MemberProfileAragonName.ts b/src/domain/member-profile/MemberProfileAragonName.ts new file mode 100644 index 0000000..5214d16 --- /dev/null +++ b/src/domain/member-profile/MemberProfileAragonName.ts @@ -0,0 +1,53 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; + +const PARENT_DOMAIN = 'aragon.eth'; +const PARENT_SUFFIX = `.${PARENT_DOMAIN}` as const; + +/** + * Normalizes a raw name input into its canonical form. + */ +function canonicalize(input: string): string { + return input.normalize('NFC').trim().toLowerCase(); +} + +const MemberProfileAragonNamePropsSchema = z + .object({ + value: z.string().min(1, 'name must not be empty'), + }) + .transform(({ value }) => ({ value: canonicalize(value) })) + .refine( + (props) => props.value.endsWith(PARENT_SUFFIX), + `name must end with ${PARENT_SUFFIX}`, + ) + .refine((props) => { + const label = props.value.slice(0, -PARENT_SUFFIX.length); + return label.length > 0; + }, 'name must include at least one label before .aragon.eth'); + +type MemberProfileAragonNameProps = z.output< + typeof MemberProfileAragonNamePropsSchema +>; +type MemberProfileAragonNameInput = z.input< + typeof MemberProfileAragonNamePropsSchema +>; + +/** + * Subdomain of `aragon.eth` issued by an ENS `MemberRegistry`. + */ +export class MemberProfileAragonName extends ValueObject { + toString(): string { + return this.props.value; + } + + static create( + props: MemberProfileAragonNameInput, + ): MemberProfileAragonName { + const validated = MemberProfileAragonNamePropsSchema.parse(props); + return new MemberProfileAragonName(validated); + } + + static fromString(input: string): MemberProfileAragonName { + return MemberProfileAragonName.create({ value: input }); + } +} diff --git a/src/domain/member-profile/MemberProfileStore.ts b/src/domain/member-profile/MemberProfileStore.ts new file mode 100644 index 0000000..de48a86 --- /dev/null +++ b/src/domain/member-profile/MemberProfileStore.ts @@ -0,0 +1,17 @@ +import type { MemberProfileAragonName } from './MemberProfileAragonName'; +import type { MemberProfileTextRecord } from './MemberProfileTextRecord'; + +export interface MemberProfileStore { + /** + * Returns the current forward text records for the given subdomain. + * + * Returns an empty array when: + * - the subdomain is unknown to the indexer + * - the subdomain exists but has no resolver attached yet + * - the resolver has no current text records (all cleared or + * superseded by a `VersionChanged` bump) + */ + findTextRecordsBySubdomain( + subdomain: MemberProfileAragonName, + ): Promise; +} diff --git a/src/domain/member-profile/MemberProfileTextRecord.test.ts b/src/domain/member-profile/MemberProfileTextRecord.test.ts new file mode 100644 index 0000000..6342360 --- /dev/null +++ b/src/domain/member-profile/MemberProfileTextRecord.test.ts @@ -0,0 +1,35 @@ +import { MemberProfileTextRecord } from './MemberProfileTextRecord'; + +describe('MemberProfileTextRecord', () => { + it('creates a valid text record', () => { + const record = MemberProfileTextRecord.create({ + key: 'avatar', + value: 'ipfs://bafy…', + }); + expect(record.key).toBe('avatar'); + expect(record.value).toBe('ipfs://bafy…'); + }); + + it('allows an empty value string', () => { + const record = MemberProfileTextRecord.create({ key: 'bio', value: '' }); + expect(record.value).toBe(''); + }); + + it('rejects an empty key', () => { + expect(() => + MemberProfileTextRecord.create({ key: '', value: 'x' }), + ).toThrow(); + }); + + it('structural equality', () => { + const a = MemberProfileTextRecord.create({ key: 'avatar', value: 'x' }); + const b = MemberProfileTextRecord.create({ key: 'avatar', value: 'x' }); + expect(a.equals(b)).toBe(true); + }); + + it('structural inequality when value differs', () => { + const a = MemberProfileTextRecord.create({ key: 'avatar', value: 'x' }); + const b = MemberProfileTextRecord.create({ key: 'avatar', value: 'y' }); + expect(a.equals(b)).toBe(false); + }); +}); diff --git a/src/domain/member-profile/MemberProfileTextRecord.ts b/src/domain/member-profile/MemberProfileTextRecord.ts new file mode 100644 index 0000000..0972320 --- /dev/null +++ b/src/domain/member-profile/MemberProfileTextRecord.ts @@ -0,0 +1,32 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; + +const MemberProfileTextRecordPropsSchema = z.object({ + key: z.string().min(1, 'key must not be empty'), + value: z.string(), +}); + +type MemberProfileTextRecordProps = z.output< + typeof MemberProfileTextRecordPropsSchema +>; + +/** + * A single forward text record on a member's subdomain — for example + * `{ key: 'avatar', value: 'ipfs://…' }`. + */ +export class MemberProfileTextRecord extends ValueObject { + get key(): string { + return this.props.key; + } + + get value(): string { + return this.props.value; + } + + static create( + props: MemberProfileTextRecordProps, + ): MemberProfileTextRecord { + const validated = MemberProfileTextRecordPropsSchema.parse(props); + return new MemberProfileTextRecord(validated); + } +} diff --git a/src/domain/primitives/blockchain/Address.test.ts b/src/domain/primitives/blockchain/Address.test.ts new file mode 100644 index 0000000..1509ec1 --- /dev/null +++ b/src/domain/primitives/blockchain/Address.test.ts @@ -0,0 +1,61 @@ +import { HexNumber } from '../math/HexNumber'; +import { Address } from './Address'; + +describe('Address', () => { + let addressValue: HexNumber; + let address: Address; + + beforeEach(() => { + addressValue = HexNumber.create({ + hexNumberValue: '0x0123456789abcdef0123456789abcdef01234567', + }); + address = Address.create({ + addressValue, + }); + }); + + it('creates an Address instance with valid props', () => { + const address1 = Address.create({ addressValue }); + const address2 = Address.create({ addressValue }); + + expect(address1.equals(address2)).toBeTruthy(); + }); + + it('throws an error when creating a Address with an invalid prop', () => { + const invalidAddressValue = HexNumber.create({ + hexNumberValue: '0x0123456789abcdef', // Invalid length + }); + const createAddressWithInvalidProp = () => + Address.create({ addressValue: invalidAddressValue }); + + expect(createAddressWithInvalidProp).toThrow( + /addressValue must be 20 bytes/, + ); + }); + + it('returns the correct checksum-based hex string', () => { + const checksumValue = address.toHexString(); + + const expectedChecksumValue = '0x0123456789abcDEF0123456789abCDef01234567'; + + expect(checksumValue).toBe(expectedChecksumValue); + }); + + it('checks equality between two Address instances', () => { + const addressValue1 = HexNumber.create({ + hexNumberValue: '0x0123456789abcdef0123456789abcdef01234567', + }); + const addressValue2 = HexNumber.create({ + hexNumberValue: '0xabcdef0123456789abcdef0123456789abcdef01', + }); + const addressValue3 = HexNumber.create({ + hexNumberValue: '0xabcdef0123456789abcdef0123456789abcdef01', + }); + const address1 = Address.create({ addressValue: addressValue1 }); + const address2 = Address.create({ addressValue: addressValue2 }); + const address3 = Address.create({ addressValue: addressValue3 }); + + expect(address1.equals(address2)).toBe(false); + expect(address2.equals(address3)).toBe(true); + }); +}); diff --git a/src/domain/primitives/blockchain/Address.ts b/src/domain/primitives/blockchain/Address.ts new file mode 100644 index 0000000..e2997c6 --- /dev/null +++ b/src/domain/primitives/blockchain/Address.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert'; +import { ValueObject } from 'ddd-core-ts'; +import { keccak256 } from 'js-sha3'; +import { z } from 'zod'; +import { HexNumber } from '../math/HexNumber'; +import type { HexString } from '../validation/ZodHexString'; + +const AddressPropsSchema = z.object({ + addressValue: z + .instanceof(HexNumber) + .refine( + (value) => value.getByteLength() === 20, + 'addressValue must be 20 bytes', + ), +}); + +type AddressProps = z.infer; + +/** + * EIP-55 checksum: keccak256 the lowercase hex address (without 0x), + * then uppercase each hex character where the corresponding hash nibble >= 8. + */ +function toChecksumAddress(hex: string): HexString { + const addr = hex.replace(/^0x/, '').toLowerCase(); + const hash = keccak256(addr); + let result = '0x'; + for (let i = 0; i < addr.length; i++) { + const hashNibble = Number.parseInt(hash[i], 16); + result += hashNibble >= 8 ? addr[i].toUpperCase() : addr[i]; + } + return result as HexString; +} + +export class Address extends ValueObject { + public toHexNumber(): HexNumber { + return this.props.addressValue; + } + + /** + * @returns A 0x-prefixed checksum-based hex string of the address (EIP-55). + */ + public toHexString(): HexString { + return toChecksumAddress(this.props.addressValue.toHexString()); + } + + public equals(other: Address): boolean { + return this.toHexString() === other.toHexString(); + } + + public static create(props: AddressProps): Address { + const validatedProps = AddressPropsSchema.parse(props); + const { addressValue } = validatedProps; + const byteLength = addressValue.getByteLength(); + assert( + byteLength === 20, + `addressValue must be 20 bytes. Got ${byteLength}`, + ); + return new Address(props); + } + + /** + * Creates an Address from a 0x-prefixed hex string. + */ + public static fromHexString(hex: HexString): Address { + return Address.create({ + addressValue: HexNumber.create({ hexNumberValue: hex }), + }); + } +} diff --git a/src/domain/primitives/events/SomethingWentWrong.ts b/src/domain/primitives/events/SomethingWentWrong.ts new file mode 100644 index 0000000..3b7e799 --- /dev/null +++ b/src/domain/primitives/events/SomethingWentWrong.ts @@ -0,0 +1,13 @@ +import { DomainEventBase, defineEvent } from 'ddd-core-ts'; + +export const SomethingWentWrong = defineEvent( + class SomethingWentWrong extends DomainEventBase { + static readonly code = 'SomethingWentWrong'; + + constructor(public readonly error: Error) { + super(); + } + }, +); + +export type SomethingWentWrong = InstanceType; diff --git a/src/domain/primitives/index.ts b/src/domain/primitives/index.ts new file mode 100644 index 0000000..ca07a09 --- /dev/null +++ b/src/domain/primitives/index.ts @@ -0,0 +1,26 @@ +// blockchain +export { Address } from './blockchain/Address'; + +// events +export { SomethingWentWrong } from './events/SomethingWentWrong'; + +// math +export { HexNumber } from './math/HexNumber'; + +// pagination +export type { Page } from './pagination/Page'; +export { createPage } from './pagination/Page'; +export { PageRequest } from './pagination/PageRequest'; + +// units +export { Ether } from './units/Ether'; +export { Gwei } from './units/Gwei'; +export { Wei } from './units/Wei'; + +// uuid +export { UUID } from './uuid/UUID'; + +// validation +export { zExtended } from './validation'; +export type { HexString } from './validation/ZodHexString'; +export { assertHexString } from './validation/ZodHexString'; diff --git a/src/domain/primitives/math/HexNumber.test.ts b/src/domain/primitives/math/HexNumber.test.ts new file mode 100644 index 0000000..0dcbfdd --- /dev/null +++ b/src/domain/primitives/math/HexNumber.test.ts @@ -0,0 +1,118 @@ +import { HexNumber } from './HexNumber'; + +describe('HexNumber', () => { + let hexNumber: HexNumber; + + beforeEach(() => { + const hexNumberValue = '0x123456'; + hexNumber = HexNumber.create({ + hexNumberValue, + }); + }); + + it('returns the correct hex number value', () => { + const value = hexNumber.toHexString(); + + expect(value).toBe('0x123456'); + }); + + it('returns the correct big int number', () => { + const value = HexNumber.create({ hexNumberValue: '0x' }); + + expect(value.toBigint()).toBe(0n); + }); + + it('returns the correct BigInt value', () => { + const decimalValue = hexNumber.toBigint(); + + expect(decimalValue).toEqual(BigInt(1_193_046)); + }); + + it('returns the correct BigInt value larger than Number.MAX_SAFE_INTEGER', () => { + const decimalValue = BigInt(Number.MAX_SAFE_INTEGER) * BigInt(3); + const largerHexNumber = HexNumber.createFromDecimal(decimalValue); + + expect(largerHexNumber.toBigint()).toEqual(decimalValue); + }); + + it('creates a HexNumber instance from a decimal number', () => { + const decimalNumber = 1_234_567_890; + hexNumber = HexNumber.createFromDecimal(decimalNumber); + + expect(hexNumber.toHexString()).toBe('0x499602d2'); + }); + + it('throws an error when an invalid hex number is created', () => { + const invalidProps = { + hexNumberValue: 'invalid_hex_string', + }; + + expect(() => HexNumber.create(invalidProps)).toThrow(); + }); + + it('returns the correct byte length', () => { + const byteLength = hexNumber.getByteLength(); + + expect(byteLength).toBe(3); + }); + + it('checks equality between two HexNumber instances and ignores the alphabetic case', () => { + const hexNumber1 = HexNumber.create({ + hexNumberValue: '0x123456abcd', + }); + const hexNumber2 = HexNumber.create({ + hexNumberValue: '0x123456ABCD', + }); + const hexNumber3 = HexNumber.create({ + hexNumberValue: '0xabcdef', + }); + + expect(hexNumber1.equals(hexNumber2)).toBe(true); + expect(hexNumber2.equals(hexNumber3)).toBe(false); + }); + + it('handles HexNumber initialized with "0x"', () => { + const hexNumberZero = HexNumber.create({ hexNumberValue: '0x' }); + + expect(hexNumberZero.toHexString()).toBe('0x'); + expect(hexNumberZero.toBigint()).toBe(0n); + + expect(hexNumberZero.getByteLength()).toBe(0); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x' })), + ).toBe(true); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x0' })), + ).toBe(false); + }); + + it('handles HexNumber initialized with "0x0"', () => { + const hexNumberZero = HexNumber.create({ hexNumberValue: '0x0' }); + + expect(hexNumberZero.toHexString()).toBe('0x0'); + expect(hexNumberZero.toBigint()).toBe(0n); + + expect(hexNumberZero.getByteLength()).toBe(1); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x0' })), + ).toBe(true); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x' })), + ).toBe(false); + }); + + it('handles HexNumber initialized with "0x000"', () => { + const hexNumberZero = HexNumber.create({ hexNumberValue: '0x000' }); + + expect(hexNumberZero.toHexString()).toBe('0x000'); + expect(hexNumberZero.toBigint()).toBe(0n); + + expect(hexNumberZero.getByteLength()).toBe(2); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x000' })), + ).toBe(true); + expect( + hexNumberZero.equals(HexNumber.create({ hexNumberValue: '0x' })), + ).toBe(false); + }); +}); diff --git a/src/domain/primitives/math/HexNumber.ts b/src/domain/primitives/math/HexNumber.ts new file mode 100644 index 0000000..5b2226f --- /dev/null +++ b/src/domain/primitives/math/HexNumber.ts @@ -0,0 +1,72 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { assertHexString, type HexString } from '../validation/ZodHexString'; + +const HexNumberPropsSchema = z.object({ + hexNumberValue: z + .string() + .refine( + (value) => typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value), + 'hexNumberValue must be a 0x-prefixed hex string and not undefined', + ), +}); + +type HexNumberProps = z.infer; + +export class HexNumber extends ValueObject { + /** + * @returns The number as a lowercase base 16 string value, prefixed with 0x. + */ + public toHexString(): HexString { + return this.props.hexNumberValue.toLowerCase() as HexString; + } + + /** + * @returns The decimal value of the hex number. + */ + public toBigint(): bigint { + if (this.props.hexNumberValue === '0x') { + return 0n; + } + return BigInt(this.props.hexNumberValue); + } + + /** + * @returns The number of bytes represented by the hex string. + */ + public getByteLength(): number { + const hex = this.props.hexNumberValue.replace(/^0x/, ''); + if (hex.length === 0) { + return 0; + } + // Pad to even length, then divide by 2 (2 hex chars = 1 byte) + const padded = hex.length % 2 === 0 ? hex : `0${hex}`; + return padded.length / 2; + } + + public equals(other: HexNumber): boolean { + return ( + this.props.hexNumberValue.toLowerCase() === + other.props.hexNumberValue.toLowerCase() + ); + } + + public static create(props: HexNumberProps): HexNumber { + const validatedProps = HexNumberPropsSchema.parse(props); + assertHexString( + validatedProps.hexNumberValue, + 'hexNumberValue must be a 0x-prefixed hex string', + ); + return new HexNumber(validatedProps); + } + + /** + * Creates a HexNumber instance from a decimal number. + * @param decimalNumber A decimal bigint or number. + */ + public static createFromDecimal(decimalNumber: bigint | number): HexNumber { + return HexNumber.create({ + hexNumberValue: `0x${decimalNumber.toString(16)}`, + }); + } +} diff --git a/src/domain/primitives/pagination/Page.ts b/src/domain/primitives/pagination/Page.ts new file mode 100644 index 0000000..6a26afb --- /dev/null +++ b/src/domain/primitives/pagination/Page.ts @@ -0,0 +1,22 @@ +export interface Page { + items: T[]; + page: number; + pageSize: number; + totalPages: number; + totalRecords: number; +} + +export function createPage( + items: T[], + page: number, + pageSize: number, + totalRecords: number, +): Page { + return { + items, + page, + pageSize, + totalPages: Math.ceil(totalRecords / pageSize), + totalRecords, + }; +} diff --git a/src/domain/primitives/pagination/PageRequest.test.ts b/src/domain/primitives/pagination/PageRequest.test.ts new file mode 100644 index 0000000..222cd9b --- /dev/null +++ b/src/domain/primitives/pagination/PageRequest.test.ts @@ -0,0 +1,22 @@ +import { PageRequest } from './PageRequest'; + +describe('PageRequest', () => { + it('creates with valid props', () => { + const page = PageRequest.create({ page: 1, pageSize: 20 }); + expect(page.page).toBe(1); + expect(page.pageSize).toBe(20); + }); + + it('calculates offset', () => { + const page = PageRequest.create({ page: 3, pageSize: 10 }); + expect(page.offset).toBe(20); + }); + + it('rejects invalid page', () => { + expect(() => PageRequest.create({ page: 0, pageSize: 20 })).toThrow(); + }); + + it('rejects pageSize over 250', () => { + expect(() => PageRequest.create({ page: 1, pageSize: 300 })).toThrow(); + }); +}); diff --git a/src/domain/primitives/pagination/PageRequest.ts b/src/domain/primitives/pagination/PageRequest.ts new file mode 100644 index 0000000..8fb3782 --- /dev/null +++ b/src/domain/primitives/pagination/PageRequest.ts @@ -0,0 +1,29 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; + +const PageRequestPropsSchema = z.object({ + page: z.number().int().positive(), + pageSize: z.number().int().positive().max(250), +}); + +type PageRequestProps = z.output; +type PageRequestInput = z.input; + +export class PageRequest extends ValueObject { + get page(): number { + return this.props.page; + } + + get pageSize(): number { + return this.props.pageSize; + } + + get offset(): number { + return (this.props.page - 1) * this.props.pageSize; + } + + static create(props: PageRequestInput): PageRequest { + const validated = PageRequestPropsSchema.parse(props); + return new PageRequest(validated); + } +} diff --git a/src/domain/primitives/units/EVMUnit.ts b/src/domain/primitives/units/EVMUnit.ts new file mode 100644 index 0000000..d8e8112 --- /dev/null +++ b/src/domain/primitives/units/EVMUnit.ts @@ -0,0 +1,9 @@ +import type { Ether } from './Ether'; +import type { Gwei } from './Gwei'; +import type { Wei } from './Wei'; + +export interface EVMUnit { + toWei: () => Wei; + toGwei: () => Gwei; + toEther: () => Ether; +} diff --git a/src/domain/primitives/units/Ether.test.ts b/src/domain/primitives/units/Ether.test.ts new file mode 100644 index 0000000..7a932ba --- /dev/null +++ b/src/domain/primitives/units/Ether.test.ts @@ -0,0 +1,31 @@ +import BigNumber from 'bignumber.js'; + +import { Ether } from './Ether'; + +describe('Ether', () => { + it('returns the base value', () => { + const value = new BigNumber(1.23); + const token = Ether.create(value); + expect(token.toBigNumber()).toEqual(value); + }); + + it('converts to Wei', () => { + const value = new BigNumber(1.23); + const token = Ether.create(value); + expect(token.toWei().toBigNumber().toString()).toEqual( + '1230000000000000000', + ); + }); + + it('converts to Gwei', () => { + const value = new BigNumber(1.23); + const token = Ether.create(value); + expect(token.toGwei().toBigNumber().toNumber()).toEqual(1_230_000_000); + }); + + it('allows zero', () => { + const value = new BigNumber(0); + const token = Ether.create(value); + expect(token.toBigNumber()).toEqual(value); + }); +}); diff --git a/src/domain/primitives/units/Ether.ts b/src/domain/primitives/units/Ether.ts new file mode 100644 index 0000000..e6c48bd --- /dev/null +++ b/src/domain/primitives/units/Ether.ts @@ -0,0 +1,37 @@ +import BigNumber from 'bignumber.js'; +import { ValueObject } from 'ddd-core-ts'; +import type { EVMUnit } from './EVMUnit'; +import { Gwei } from './Gwei'; +import { Wei } from './Wei'; + +interface EtherProps { + etherValue: BigNumber; +} + +export class Ether extends ValueObject implements EVMUnit { + public toBigNumber(): BigNumber { + return this.props.etherValue; + } + + public toWei(): Wei { + const { etherValue } = this.props; + const conversionRate = new BigNumber(10).pow(18); + const weiValue = etherValue.times(conversionRate); + return Wei.create(weiValue); + } + + public toGwei(): Gwei { + const { etherValue } = this.props; + const conversionRate = new BigNumber(10).pow(9); + const gweiValue = etherValue.times(conversionRate); + return Gwei.create(gweiValue); + } + + public toEther(): Ether { + return this; + } + + public static create(etherValue: BigNumber): Ether { + return new Ether({ etherValue }); + } +} diff --git a/src/domain/primitives/units/Gwei.test.ts b/src/domain/primitives/units/Gwei.test.ts new file mode 100644 index 0000000..331d316 --- /dev/null +++ b/src/domain/primitives/units/Gwei.test.ts @@ -0,0 +1,46 @@ +import BigNumber from 'bignumber.js'; + +import { Gwei } from './Gwei'; + +describe('Gwei', () => { + it('returns the base value', () => { + const value = new BigNumber(1_230_000_000); + const token = Gwei.create(value); + expect(token.toBigNumber()).toEqual(value); + }); + + it('converts to Wei', () => { + const value = new BigNumber(1_230_000_000); + const token = Gwei.create(value); + expect(token.toWei().toBigNumber().toString()).toEqual( + '1230000000000000000', + ); + }); + + it('converts to Ether', () => { + const value = new BigNumber(1_230_000_000); + const token = Gwei.create(value); + expect(token.toEther().toBigNumber().toNumber()).toEqual(1.23); + }); + + it('allows zero', () => { + const value = new BigNumber(0); + const token = Gwei.create(value); + expect(token.toBigNumber()).toEqual(value); + }); + + it('adds another gwei', () => { + const value1 = new BigNumber(1_230_000_000); + const token1 = Gwei.create(value1); + const value2 = new BigNumber(321); + const token2 = Gwei.create(value2); + expect(token1.plus(token2).toBigNumber().toNumber()).toEqual(1_230_000_321); + }); + + it('multiplies a bignumber', () => { + const value1 = new BigNumber(321); + const token1 = Gwei.create(value1); + const value2 = new BigNumber(123); + expect(token1.times(value2).toBigNumber().toNumber()).toEqual(39_483); + }); +}); diff --git a/src/domain/primitives/units/Gwei.ts b/src/domain/primitives/units/Gwei.ts new file mode 100644 index 0000000..0ef33f4 --- /dev/null +++ b/src/domain/primitives/units/Gwei.ts @@ -0,0 +1,63 @@ +import BigNumber from 'bignumber.js'; +import { ValueObject } from 'ddd-core-ts'; +import { Ether } from './Ether'; +import type { EVMUnit } from './EVMUnit'; +import { Wei } from './Wei'; + +interface GweiProps { + gweiValue: BigNumber; +} + +export class Gwei extends ValueObject implements EVMUnit { + public toBigNumber(): BigNumber { + return this.props.gweiValue; + } + + public toWei(): Wei { + const { gweiValue } = this.props; + const conversionRate = new BigNumber(10).pow(9); + const weiValue = gweiValue.times(conversionRate); + return Wei.create(weiValue); + } + + public toGwei(): Gwei { + return this; + } + + public toEther(): Ether { + const { gweiValue } = this.props; + const conversionRate = new BigNumber(10).pow(-9); + const etherValue = gweiValue.times(conversionRate); + return Ether.create(etherValue); + } + + /** + * Adds this value to another value. + * @param other The other value to add to. + */ + public plus(other: Gwei): Gwei { + const thisValue = this.props.gweiValue; + const otherValue = other.toBigNumber(); + const totalValue = thisValue.plus(otherValue); + return Gwei.create(totalValue); + } + + /** + * Multiplies our value in gwei with a big number. Returns the value in Gwei. + * @param bigNumber The big number to multiply our value with. + */ + public times(bigNumber: BigNumber): Gwei { + const thisValue = this.props.gweiValue; + const totalValue = thisValue.times(bigNumber); + return Gwei.create(totalValue); + } + + public static create(gweiValue: BigNumber): Gwei { + return new Gwei({ gweiValue }); + } + + public static deserialize(gweiValue: string): Gwei { + const gweiValueBN = new BigNumber(gweiValue); + return Gwei.create(gweiValueBN); + } +} diff --git a/src/domain/primitives/units/Wei.test.ts b/src/domain/primitives/units/Wei.test.ts new file mode 100644 index 0000000..1df52b8 --- /dev/null +++ b/src/domain/primitives/units/Wei.test.ts @@ -0,0 +1,29 @@ +import BigNumber from 'bignumber.js'; + +import { Wei } from './Wei'; + +describe('Wei', () => { + it('returns the base value', () => { + const value = new BigNumber('1230000000000000000'); + const token = Wei.create(value); + expect(token.toBigNumber()).toEqual(value); + }); + + it('converts to Gwei', () => { + const value = new BigNumber('1230000000000000000'); + const token = Wei.create(value); + expect(token.toGwei().toBigNumber().toNumber()).toEqual(1_230_000_000); + }); + + it('converts to Ether', () => { + const value = new BigNumber('1230000000000000000'); + const token = Wei.create(value); + expect(token.toEther().toBigNumber().toNumber()).toEqual(1.23); + }); + + it('allows zero', () => { + const value = new BigNumber(0); + const token = Wei.create(value); + expect(token.toBigNumber()).toEqual(value); + }); +}); diff --git a/src/domain/primitives/units/Wei.ts b/src/domain/primitives/units/Wei.ts new file mode 100644 index 0000000..f83eae6 --- /dev/null +++ b/src/domain/primitives/units/Wei.ts @@ -0,0 +1,40 @@ +import BigNumber from 'bignumber.js'; +import { ValueObject } from 'ddd-core-ts'; + +import { Ether } from './Ether'; +import type { EVMUnit } from './EVMUnit'; +import { Gwei } from './Gwei'; + +interface WeiProps { + weiValue: BigNumber; +} + +export class Wei extends ValueObject implements EVMUnit { + public toBigNumber(): BigNumber { + return this.props.weiValue; + } + + public toWei(): Wei { + return this; + } + + public toGwei(): Gwei { + const { weiValue } = this.props; + const conversionRate = new BigNumber(10).pow(-9); + const gweiValue = weiValue.times(conversionRate); + + return Gwei.create(gweiValue); + } + + public toEther(): Ether { + const { weiValue } = this.props; + const conversionRate = new BigNumber(10).pow(-18); + const etherValue = weiValue.times(conversionRate); + + return Ether.create(etherValue); + } + + public static create(weiValue: BigNumber): Wei { + return new Wei({ weiValue }); + } +} diff --git a/src/domain/primitives/uuid/UUID.test.ts b/src/domain/primitives/uuid/UUID.test.ts new file mode 100644 index 0000000..adf3ae5 --- /dev/null +++ b/src/domain/primitives/uuid/UUID.test.ts @@ -0,0 +1,37 @@ +import { UUID } from './UUID'; + +describe('UUID', () => { + const validUUIDValue = '123e4567-e89b-12d3-a456-426614174000'; + const anotherValidUUIDValue = '123e4567-e89b-12d3-a456-426614174001'; + + it('getValue returns a lowercase string representation of the UUID', () => { + const uuid = UUID.create({ uuidValue: validUUIDValue.toUpperCase() }); + expect(uuid.getValue()).toBe(validUUIDValue.toLowerCase()); + }); + + describe('equals method', () => { + it('returns true for UUIDs with the same value', () => { + const uuid1 = UUID.create({ uuidValue: validUUIDValue }); + const uuid2 = UUID.create({ uuidValue: validUUIDValue }); + expect(uuid1.equals(uuid2)).toBe(true); + }); + + it('returns false for UUIDs with different values', () => { + const uuid1 = UUID.create({ uuidValue: validUUIDValue }); + const uuid2 = UUID.create({ uuidValue: anotherValidUUIDValue }); + expect(uuid1.equals(uuid2)).toBe(false); + }); + }); + + describe('create method', () => { + it('creates a UUID instance with valid props', () => { + const uuid = UUID.create({ uuidValue: validUUIDValue }); + expect(uuid).toBeInstanceOf(UUID); + expect(uuid.getValue()).toBe(validUUIDValue.toLowerCase()); + }); + + it('throws an error with invalid props', () => { + expect(() => UUID.create({ uuidValue: 'invalid-uuid' })).toThrow(); + }); + }); +}); diff --git a/src/domain/primitives/uuid/UUID.ts b/src/domain/primitives/uuid/UUID.ts new file mode 100644 index 0000000..91dcaa5 --- /dev/null +++ b/src/domain/primitives/uuid/UUID.ts @@ -0,0 +1,28 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; + +const UUIDPropsSchema = z.object({ + uuidValue: z.string().uuid('uuidValue must be a valid UUID string'), +}); + +type UUIDProps = z.infer; + +export class UUID extends ValueObject { + /** + * @returns A lower case string representation of the UUID. + */ + public getValue(): string { + return this.props.uuidValue.toLowerCase(); + } + + public equals(other: UUID): boolean { + return ( + this.props.uuidValue.toLowerCase() === other.props.uuidValue.toLowerCase() + ); + } + + public static create(props: UUIDProps): UUID { + const validatedProps = UUIDPropsSchema.parse(props); + return new UUID(validatedProps); + } +} diff --git a/src/domain/primitives/validation/ZodHexString.test.ts b/src/domain/primitives/validation/ZodHexString.test.ts new file mode 100644 index 0000000..9126661 --- /dev/null +++ b/src/domain/primitives/validation/ZodHexString.test.ts @@ -0,0 +1,59 @@ +import { assertHexString, type HexString, hexString } from './ZodHexString'; + +describe('hexString', () => { + it('validates a valid hex string', () => { + const schema = hexString(); + const validHex: HexString = '0xabcdef123456'; + expect(schema.parse(validHex)).toBe(validHex); + }); + + it('rejects an invalid hex string', () => { + const schema = hexString(); + expect(() => schema.parse('0xGHIJKL')).toThrow('Invalid hex string'); + expect(() => schema.parse('123456')).toThrow('Invalid hex string'); + expect(() => schema.parse('0Xabc')).toThrow('Invalid hex string'); + expect(() => schema.parse('')).toThrow('Invalid hex string'); + }); + + it('allows an empty hex string', () => { + const schema = hexString(); + expect(schema.parse('0x')).toBe('0x'); + }); + + it('allows capital letters in the hex string', () => { + const schema = hexString(); + expect(schema.parse('0xABCDEF123456')).toBe('0xABCDEF123456'); + }); +}); + +describe('assertHexString', () => { + it('does not throw for a valid hex string', () => { + const validHex: unknown = '0xabcdef123456'; + expect(() => assertHexString(validHex)).not.toThrow(); + }); + + it('throws for an invalid hex string', () => { + const invalidHex: unknown = '0xGHIJKL'; + expect(() => assertHexString(invalidHex)).toThrow('Invalid hex string'); + }); + + it('throws with a custom error message', () => { + const invalidHex: unknown = '123456'; + expect(() => assertHexString(invalidHex, 'Custom error message')).toThrow( + 'Custom error message', + ); + }); + + it('narrows the type to HexString for valid input', () => { + const validHex: unknown = '0xabcdef123456'; + assertHexString(validHex); + // After assertion, validHex is now typed as HexString + const hex: HexString = validHex; + expect(hex).toBe('0xabcdef123456'); + }); + + it('allows capital letters in the hex string', () => { + const validHex: unknown = '0xABCDEF123456'; + assertHexString(validHex); + }); +}); diff --git a/src/domain/primitives/validation/ZodHexString.ts b/src/domain/primitives/validation/ZodHexString.ts new file mode 100644 index 0000000..a4da885 --- /dev/null +++ b/src/domain/primitives/validation/ZodHexString.ts @@ -0,0 +1,25 @@ +import assert from 'node:assert'; +import { ZodString } from 'zod'; + +const hexStringRegex = /^0x[a-fA-F0-9]*$/; + +export type HexString = `0x${string}`; + +export function hexString() { + return ZodString.create().refine( + (data): data is HexString => hexStringRegex.test(data), + { + message: 'Invalid hex string', + }, + ); +} + +export function assertHexString( + data: unknown, + message?: string, +): asserts data is HexString { + assert(typeof data === 'string', 'Data must be a string'); + if (!hexStringRegex.test(data)) { + throw new Error(message ?? 'Invalid hex string'); + } +} diff --git a/src/domain/primitives/validation/index.test.ts b/src/domain/primitives/validation/index.test.ts new file mode 100644 index 0000000..07286dc --- /dev/null +++ b/src/domain/primitives/validation/index.test.ts @@ -0,0 +1,7 @@ +import { zExtended } from './index'; + +describe('zExtended', () => { + it('exports hexString as a function', () => { + expect(typeof zExtended.hexString).toBe('function'); + }); +}); diff --git a/src/domain/primitives/validation/index.ts b/src/domain/primitives/validation/index.ts new file mode 100644 index 0000000..a6487c8 --- /dev/null +++ b/src/domain/primitives/validation/index.ts @@ -0,0 +1,5 @@ +import { hexString } from './ZodHexString'; + +export const zExtended = { + hexString, +}; diff --git a/src/index.ts b/src/index.ts index beb9701..c28d983 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,7 @@ +/** biome-ignore-all assist/source/organizeImports: Sorting by category instead */ + // Aragon Subdomain — public API -// Exports will be added as domain objects, use cases, and controllers are implemented. +export { AragonController as AragonSubdomain } from './infrastructure/controllers/AragonController/AragonController'; + +// Infrastructure +export { EnvioClient } from './infrastructure/stores/EnvioClient'; diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts new file mode 100644 index 0000000..5692f23 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -0,0 +1,51 @@ +import type { HandlerDefinition } from 'ddd-core-ts'; +import { handleRequest } from 'ddd-core-ts'; +import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; +import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; +import { EnvioMemberProfileStore } from '@/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore'; +import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; +import { GetMemberProfileTextRecordsUseCase } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; +import type { MemberProfileTextRecordDTO } from './maps/domain/MemberProfileTextRecordMap'; +import * as MemberProfileTextRecordMap from './maps/domain/MemberProfileTextRecordMap'; +import type { GetMemberProfileTextRecordsRequestDTO } from './maps/use-cases/GetMemberProfileTextRecordsMap'; +import * as GetMemberProfileTextRecordsMap from './maps/use-cases/GetMemberProfileTextRecordsMap'; + +interface HandlersRecord { + getMemberProfileTextRecords: HandlerDefinition< + GetMemberProfileTextRecordsRequestDTO, + GetMemberProfileTextRecordsUseCaseProps, + MemberProfileTextRecord[], + MemberProfileTextRecordDTO[] + >; +} + +export class AragonController { + private constructor(private readonly handlers: HandlersRecord) {} + + static load(envioClient: EnvioClient): AragonController { + const memberProfileStore = new EnvioMemberProfileStore(envioClient); + const getMemberProfileTextRecordsUseCase = + new GetMemberProfileTextRecordsUseCase(memberProfileStore); + + const handlers: HandlersRecord = { + getMemberProfileTextRecords: { + requestMapper: GetMemberProfileTextRecordsMap, + responseMapper: MemberProfileTextRecordMap, + useCaseExecutor: getMemberProfileTextRecordsUseCase, + }, + }; + + return new AragonController(handlers); + } + + /** + * Looks up the live forward ENS text records attached to a member's `.aragon.eth` + * subdomain. Returns an empty list when the subdomain is unknown, has + * no resolver, or has no current records. + */ + public getMemberProfileTextRecords( + dto: GetMemberProfileTextRecordsRequestDTO, + ) { + return handleRequest(this.handlers.getMemberProfileTextRecords, dto); + } +} diff --git a/src/infrastructure/controllers/AragonController/maps/domain/MemberProfileTextRecordMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/MemberProfileTextRecordMap.ts new file mode 100644 index 0000000..885d7c0 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/domain/MemberProfileTextRecordMap.ts @@ -0,0 +1,15 @@ +import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; + +export interface MemberProfileTextRecordDTO { + key: string; + value: string; +} + +export function mapDomainToDTO( + records: MemberProfileTextRecord[], +): MemberProfileTextRecordDTO[] { + return records.map((record) => ({ + key: record.key, + value: record.value, + })); +} diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetMemberProfileTextRecordsMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetMemberProfileTextRecordsMap.ts new file mode 100644 index 0000000..b776c39 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetMemberProfileTextRecordsMap.ts @@ -0,0 +1,18 @@ +import { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAragonName'; +import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; + +export interface GetMemberProfileTextRecordsRequestDTO { + /** + * ENS name to look up. Must be a subdomain of `aragon.eth`. + * Multi-segment labels are allowed. + */ + subdomain: string; +} + +export function mapDTOToDomain( + dto: GetMemberProfileTextRecordsRequestDTO, +): GetMemberProfileTextRecordsUseCaseProps { + return { + subdomain: MemberProfileAragonName.fromString(dto.subdomain), + }; +} diff --git a/src/infrastructure/stores/EnvioClient.ts b/src/infrastructure/stores/EnvioClient.ts new file mode 100644 index 0000000..a46446c --- /dev/null +++ b/src/infrastructure/stores/EnvioClient.ts @@ -0,0 +1,30 @@ +import { GraphQLClient } from 'graphql-request'; + +/** + * GraphQL client for querying the Envio HyperIndex API. + */ +export class EnvioClient { + private readonly client: GraphQLClient; + + constructor(endpoint: string, apiToken?: string) { + const headers: Record = {}; + if (apiToken) { + headers.Authorization = `Bearer ${apiToken}`; + } + this.client = new GraphQLClient(endpoint, { headers }); + } + + /** + * Execute a GraphQL query. + */ + async query( + document: string, + variables?: Record, + ): Promise { + try { + return await this.client.request(document, variables); + } catch (cause) { + throw new Error('Envio GraphQL query failed', { cause }); + } + } +} diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts new file mode 100644 index 0000000..2d6e00a --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -0,0 +1,205 @@ +import { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAragonName'; +import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; +import { EnvioMemberProfileStore } from './EnvioMemberProfileStore'; + +const CHAIN_ID = '1'; +const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; +const NODE = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + +const textId = (version: string, key: string) => + `${CHAIN_ID}-${RESOLVER}-${NODE}-${version}-${key}`; + +interface QueryCall { + document: string; + variables: Record; +} + +interface MockClient { + envio: EnvioClient; + calls: QueryCall[]; +} + +function buildMockEnvioClient(responses: unknown[]): MockClient { + const calls: QueryCall[] = []; + const queue = [...responses]; + const envio = { + query: jest.fn( + async (document: string, variables: Record) => { + calls.push({ document, variables }); + return queue.shift(); + }, + ), + } as unknown as EnvioClient; + return { envio, calls }; +} + +describe('EnvioMemberProfileStore', () => { + const subdomain = MemberProfileAragonName.fromString('ea1.aragon.eth'); + + it('returns the live text records for a subdomain', async () => { + const { envio, calls } = buildMockEnvioClient([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: { + version: '0', + texts: [ + { id: textId('0', 'avatar'), key: 'avatar', value: 'ipfs://x' }, + { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + ], + }, + }, + ], + }, + ]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toHaveLength(2); + expect(result[0].key).toBe('avatar'); + expect(result[0].value).toBe('ipfs://x'); + expect(result[1].key).toBe('url'); + expect(result[1].value).toBe('https://aragon.org'); + + expect(calls).toHaveLength(1); + expect(calls[0].variables).toEqual({ name: 'ea1.aragon.eth' }); + }); + + it('queries the indexer by the subdomain canonical value', async () => { + const { envio, calls } = buildMockEnvioClient([{ Domain: [] }]); + const store = new EnvioMemberProfileStore(envio); + + const mixedCase = MemberProfileAragonName.fromString('EA1.Aragon.ETH'); + await store.findTextRecordsBySubdomain(mixedCase); + + expect(calls[0].variables).toEqual({ name: 'ea1.aragon.eth' }); + }); + + it('drops text rows superseded by a VersionChanged bump', async () => { + // Resolver is on version 1; the indexer still has the v0 row sitting + // there. The store must filter it out. + const { envio } = buildMockEnvioClient([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: { + version: '1', + texts: [ + { id: textId('0', 'avatar'), key: 'avatar', value: 'stale' }, + { id: textId('1', 'avatar'), key: 'avatar', value: 'fresh' }, + ], + }, + }, + ], + }, + ]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toHaveLength(1); + expect(result[0].value).toBe('fresh'); + }); + + it('drops cleared rows (value === null)', async () => { + const { envio } = buildMockEnvioClient([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: { + version: '0', + texts: [ + { id: textId('0', 'avatar'), key: 'avatar', value: null }, + { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + ], + }, + }, + ], + }, + ]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('url'); + }); + + it('preserves keys that contain hyphens', async () => { + // The key segment may contain `-`. Version-extraction must not be + // tripped up by it (we split on `-` and take parts[3]). + const { envio } = buildMockEnvioClient([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: { + version: '7', + texts: [ + { + id: textId('7', 'com.github-handle'), + key: 'com.github-handle', + value: 'aragon', + }, + ], + }, + }, + ], + }, + ]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toHaveLength(1); + expect(result[0].key).toBe('com.github-handle'); + expect(result[0].value).toBe('aragon'); + }); + + it('returns [] when the subdomain is not on the indexer', async () => { + const { envio } = buildMockEnvioClient([{ Domain: [] }]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toEqual([]); + }); + + it('returns [] when the subdomain has no resolver set yet', async () => { + const { envio } = buildMockEnvioClient([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: null, + }, + ], + }, + ]); + const store = new EnvioMemberProfileStore(envio); + + const result = await store.findTextRecordsBySubdomain(subdomain); + + expect(result).toEqual([]); + }); + + it('wraps GraphQL failures with a profile-level error', async () => { + const envio = { + query: jest.fn().mockRejectedValue(new Error('graphql exploded')), + } as unknown as EnvioClient; + const store = new EnvioMemberProfileStore(envio); + + await expect(store.findTextRecordsBySubdomain(subdomain)).rejects.toThrow( + 'Error querying member profile from Envio', + ); + }); +}); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts new file mode 100644 index 0000000..abf9024 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts @@ -0,0 +1,43 @@ +import type { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAragonName'; +import type { MemberProfileStore } from '@/domain/member-profile/MemberProfileStore'; +import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; +import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; +import * as MemberProfileTextRecordMap from './maps/MemberProfileTextRecordMap'; + +/** + * Looks up a Domain by name and pulls back its resolver's text rows + * in one round-trip. + */ +const FIND_TEXT_RECORDS_QUERY = ` + query FindMemberProfileTextRecords($name: String!) { + Domain(where: { name: { _eq: $name } }, limit: 1) { + id + name + resolver { + version + texts { + id + key + value + } + } + } + } +`; + +export class EnvioMemberProfileStore implements MemberProfileStore { + constructor(private readonly envio: EnvioClient) {} + + public async findTextRecordsBySubdomain( + subdomain: MemberProfileAragonName, + ): Promise { + try { + const raw = await this.envio.query(FIND_TEXT_RECORDS_QUERY, { + name: subdomain.toString(), + }); + return MemberProfileTextRecordMap.mapDTOToDomain(raw); + } catch (cause) { + throw new Error('Error querying member profile from Envio', { cause }); + } + } +} diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts new file mode 100644 index 0000000..5ac4913 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; +import { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; + +/** + * Shape of a single text-record row on the indexer. + * + * The `id` is structured as + * `${chainId}-${resolverAddress}-${domainNode}-${version}-${key}`. + * The first four segments are hyphen-free; the `key` segment (last) + * may itself contain hyphens. + */ +const TextRecordRowSchema = z.object({ + id: z.string(), + key: z.string(), + value: z.string().nullable(), +}); + +const ResolverSchema = z.object({ + version: z.string(), + texts: z.array(TextRecordRowSchema), +}); + +/** + * Shape of the `FindMemberProfileTextRecords` GraphQL response. The + * query selects up to one Domain by name; `Domain` is `[]` when no + * such row exists and `resolver` is null when the subdomain has no + * resolver assigned yet. + */ +const FindMemberProfileTextRecordsResponseSchema = z.object({ + Domain: z + .array( + z.object({ + id: z.string(), + name: z.string(), + resolver: ResolverSchema.nullable(), + }), + ) + .max(1), +}); + +export type TextRecordRowDTO = z.infer; +export type FindMemberProfileTextRecordsResponse = z.infer< + typeof FindMemberProfileTextRecordsResponseSchema +>; + +/** + * Extracts the `${version}` segment from a TextRecord id. The first + * four segments (chainId, resolverAddress, domainNode, version) never + * contain a hyphen, so `split('-')[3]` is safe. + */ +export function extractVersion(id: string): string { + return id.split('-')[3] ?? ''; +} + +/** + * Trust boundary for the indexer's `FindMemberProfileTextRecords` + * response. Asserts the shape via Zod, filters out cleared rows and + * rows superseded by `VersionChanged` (kept around on the indexer but + * logically invalid), and constructs the domain value objects. + * + * Throws a `ZodError` if the shape doesn't match what we expect. + */ +export function mapDTOToDomain(raw: unknown): MemberProfileTextRecord[] { + const parsed = FindMemberProfileTextRecordsResponseSchema.parse(raw); + const domain = parsed.Domain[0]; + + // No row for this name on the indexer. + if (!domain) return []; + // Domain exists but `setResolver` has not been called for it yet. + if (!domain.resolver) return []; + + const liveVersion = domain.resolver.version; + const live: MemberProfileTextRecord[] = []; + for (const text of domain.resolver.texts) { + if (text.value === null) continue; + if (extractVersion(text.id) !== liveVersion) continue; + live.push( + MemberProfileTextRecord.create({ key: text.key, value: text.value }), + ); + } + return live; +} diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts new file mode 100644 index 0000000..11a20e7 --- /dev/null +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts @@ -0,0 +1,62 @@ +import { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAragonName'; +import type { MemberProfileStore } from '@/domain/member-profile/MemberProfileStore'; +import { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; +import { GetMemberProfileTextRecordsUseCase } from './GetMemberProfileTextRecordsUseCase'; + +describe('GetMemberProfileTextRecordsUseCase', () => { + const subdomain = MemberProfileAragonName.fromString('ea1.aragon.eth'); + + const buildRecord = (key: string, value: string) => + MemberProfileTextRecord.create({ key, value }); + + const buildMockStore = ( + records: MemberProfileTextRecord[] = [], + ): MemberProfileStore => ({ + findTextRecordsBySubdomain: jest.fn().mockResolvedValue(records), + }); + + it('returns the text records from the store', async () => { + const records = [ + buildRecord('avatar', 'ipfs://x'), + buildRecord('url', 'https://aragon.org'), + ]; + const useCase = new GetMemberProfileTextRecordsUseCase( + buildMockStore(records), + ); + + const result = await useCase.execute({ subdomain }); + + expect(result).toBe(records); + }); + + it('returns an empty array when the store returns nothing', async () => { + const useCase = new GetMemberProfileTextRecordsUseCase(buildMockStore([])); + + const result = await useCase.execute({ subdomain }); + + expect(result).toEqual([]); + }); + + it('forwards the subdomain value object to the store unchanged', async () => { + const store = buildMockStore(); + const useCase = new GetMemberProfileTextRecordsUseCase(store); + + await useCase.execute({ subdomain }); + + expect(store.findTextRecordsBySubdomain).toHaveBeenCalledTimes(1); + expect(store.findTextRecordsBySubdomain).toHaveBeenCalledWith(subdomain); + }); + + it('wraps store errors with a use-case message', async () => { + const failingStore: MemberProfileStore = { + findTextRecordsBySubdomain: jest + .fn() + .mockRejectedValue(new Error('graphql exploded')), + }; + const useCase = new GetMemberProfileTextRecordsUseCase(failingStore); + + await expect(useCase.execute({ subdomain })).rejects.toThrow( + 'Error while getting member profile text records', + ); + }); +}); diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts new file mode 100644 index 0000000..7aabdbf --- /dev/null +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts @@ -0,0 +1,37 @@ +import type { UseCase } from 'ddd-core-ts'; +import type { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAragonName'; +import type { MemberProfileStore } from '@/domain/member-profile/MemberProfileStore'; +import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; + +export interface GetMemberProfileTextRecordsUseCaseProps { + subdomain: MemberProfileAragonName; +} + +/** + * Looks up the live forward ENS text records attached to a member's `.aragon.eth` + * subdomain. Returns an empty list when the subdomain is unknown, has + * no resolver, or has no current records. + */ +export class GetMemberProfileTextRecordsUseCase + implements + UseCase< + GetMemberProfileTextRecordsUseCaseProps, + MemberProfileTextRecord[] + > +{ + public readonly code = 'GetMemberProfileTextRecordsUseCase'; + + constructor(private readonly profileStore: MemberProfileStore) {} + + public async execute( + props: GetMemberProfileTextRecordsUseCaseProps, + ): Promise { + try { + return await this.profileStore.findTextRecordsBySubdomain(props.subdomain); + } catch (cause) { + throw new Error('Error while getting member profile text records', { + cause, + }); + } + } +} diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts new file mode 100644 index 0000000..579f822 --- /dev/null +++ b/test/AragonSubdomain.test.ts @@ -0,0 +1,120 @@ +import assert from 'node:assert'; +import { AragonSubdomain, EnvioClient } from '../src'; + +const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; +const NODE = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + +/** + * Builds an AragonSubdomain backed by an EnvioClient whose `query` + * method is replaced with a FIFO queue of canned responses. The + * underlying GraphQLClient is constructed but never used at the + * network layer. + */ +function buildController(responses: unknown[]): AragonSubdomain { + const envio = new EnvioClient('https://unused.example.invalid'); + const queue = [...responses]; + jest.spyOn(envio, 'query').mockImplementation(async () => { + if (queue.length === 0) { + throw new Error('EnvioClient.query called more times than expected'); + } + return queue.shift() as never; + }); + return AragonSubdomain.load(envio); +} + +describe('AragonSubdomain', () => { + describe('getMemberProfileTextRecords', () => { + const textId = (version: string, key: string) => + `1-${RESOLVER}-${NODE}-${version}-${key}`; + + it('returns the live text records as a DTO list', async () => { + const controller = buildController([ + { + Domain: [ + { + id: 'domain-id', + name: 'ea1.aragon.eth', + resolver: { + version: '0', + texts: [ + { + id: textId('0', 'avatar'), + key: 'avatar', + value: 'ipfs://x', + }, + { + id: textId('0', 'url'), + key: 'url', + value: 'https://aragon.org', + }, + ], + }, + }, + ], + }, + ]); + + const response = await controller.getMemberProfileTextRecords({ + subdomain: 'ea1.aragon.eth', + }); + + assert( + response.success, + 'expected getMemberProfileTextRecords to succeed', + ); + expect(response.result).toEqual([ + { key: 'avatar', value: 'ipfs://x' }, + { key: 'url', value: 'https://aragon.org' }, + ]); + }); + + it('returns [] when the subdomain is unknown to the indexer', async () => { + const controller = buildController([{ Domain: [] }]); + + const response = await controller.getMemberProfileTextRecords({ + subdomain: 'ea1.aragon.eth', + }); + + assert(response.success); + expect(response.result).toEqual([]); + }); + + it('returns [] when the subdomain has no resolver yet', async () => { + const controller = buildController([ + { + Domain: [ + { id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }, + ], + }, + ]); + + const response = await controller.getMemberProfileTextRecords({ + subdomain: 'ea1.aragon.eth', + }); + + assert(response.success); + expect(response.result).toEqual([]); + }); + + it('returns a failed response when the subdomain is not under .aragon.eth', async () => { + const controller = buildController([]); + + const response = await controller.getMemberProfileTextRecords({ + subdomain: 'vitalik.eth', + }); + + expect(response.success).toBe(false); + }); + + it('returns a failed response when the subdomain targets .aragonx.eth', async () => { + const controller = buildController([]); + + const response = await controller.getMemberProfileTextRecords({ + subdomain: 'ea1.aragonx.eth', + }); + + expect(response.success).toBe(false); + }); + }); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..d31185e --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "types": ["node"] + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/tsconfig.json b/tsconfig.json index 2df8c43..ddbfbe7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,16 +3,18 @@ "target": "es2022", "module": "commonjs", "lib": ["es2022"], - "outDir": "dist", - "rootDir": "src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true + "sourceMap": true, + "noEmit": true, + "types": ["jest", "node"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } }, - "include": ["src"], - "exclude": ["node_modules", "dist", "test"] + "include": ["src", "test"], + "exclude": ["node_modules", "dist"] } diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..ffcae41 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + sourcemap: true, + target: 'es2022', + tsconfig: 'tsconfig.build.json', +}); From 98a8d0017ad7785b39d753897effb4f2487e9efe Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 20:45:31 +0100 Subject: [PATCH 02/33] Implement GetERC20MembershipUseCase Adds the GetERC20MembershipUseCase along with its supporting infrastructure: Member, TokenVotingMember and VotingPower domain objects, the EnvioTokenVotingMemberStore that pages delegates by voting power (with the ReverseName ENS join for display), and the AragonController endpoint that fronts the use case. --- src/domain/member/Member.test.ts | 49 ++++++ src/domain/member/Member.ts | 65 +++++++ .../TokenVotingMember.test.ts | 52 ++++++ .../token-voting-member/TokenVotingMember.ts | 91 ++++++++++ .../TokenVotingMemberStore.ts | 15 ++ src/domain/voting-power/VotingPower.test.ts | 45 +++++ src/domain/voting-power/VotingPower.ts | 65 +++++++ .../AragonController/AragonController.ts | 34 ++++ .../AragonController/maps/domain/MemberMap.ts | 33 ++++ .../maps/domain/PaginatedMemberMap.ts | 26 +++ .../maps/use-cases/GetERC20MembershipMap.ts | 22 +++ .../EnvioTokenVotingMemberStore.test.ts | 165 ++++++++++++++++++ .../EnvioTokenVotingMemberStore.ts | 161 +++++++++++++++++ .../maps/ReverseNameMap.ts | 44 +++++ .../maps/TokenVotingMemberMap.ts | 142 +++++++++++++++ .../GetERC20MembershipUseCase.test.ts | 100 +++++++++++ src/use-cases/GetERC20MembershipUseCase.ts | 59 +++++++ test/AragonSubdomain.test.ts | 59 +++++++ 18 files changed, 1227 insertions(+) create mode 100644 src/domain/member/Member.test.ts create mode 100644 src/domain/member/Member.ts create mode 100644 src/domain/token-voting-member/TokenVotingMember.test.ts create mode 100644 src/domain/token-voting-member/TokenVotingMember.ts create mode 100644 src/domain/token-voting-member/TokenVotingMemberStore.ts create mode 100644 src/domain/voting-power/VotingPower.test.ts create mode 100644 src/domain/voting-power/VotingPower.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts create mode 100644 src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts create mode 100644 src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts create mode 100644 src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts create mode 100644 src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts create mode 100644 src/use-cases/GetERC20MembershipUseCase.test.ts create mode 100644 src/use-cases/GetERC20MembershipUseCase.ts diff --git a/src/domain/member/Member.test.ts b/src/domain/member/Member.test.ts new file mode 100644 index 0000000..6cb267f --- /dev/null +++ b/src/domain/member/Member.test.ts @@ -0,0 +1,49 @@ +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { Member } from './Member'; + +describe('Member', () => { + const address = Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ); + const firstActivityTimestamp = 1705320000; + const lastActivityTimestamp = 1718872200; + + const validProps = { + address, + ens: 'alice.eth', + votingPower: VotingPower.fromBigInt(1000000000000000000n), + firstActivityTimestamp, + lastActivityTimestamp, + delegationCount: 3, + }; + + it('creates a valid member', () => { + const member = Member.create(validProps); + expect(member.address.equals(address)).toBe(true); + expect(member.ens).toBe('alice.eth'); + expect(member.votingPower.isZero).toBe(false); + expect(member.firstActivityTimestamp).toBe(firstActivityTimestamp); + expect(member.lastActivityTimestamp).toBe(lastActivityTimestamp); + }); + + it('allows null ens', () => { + const member = Member.create({ ...validProps, ens: null }); + expect(member.ens).toBeNull(); + }); + + it('structural equality', () => { + const a = Member.create(validProps); + const b = Member.create(validProps); + expect(a.equals(b)).toBe(true); + }); + + it('structural inequality when votingPower differs', () => { + const a = Member.create(validProps); + const b = Member.create({ + ...validProps, + votingPower: VotingPower.zero(), + }); + expect(a.equals(b)).toBe(false); + }); +}); diff --git a/src/domain/member/Member.ts b/src/domain/member/Member.ts new file mode 100644 index 0000000..09aae6f --- /dev/null +++ b/src/domain/member/Member.ts @@ -0,0 +1,65 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; + +const MemberPropsSchema = z.object({ + address: z.instanceof(Address), + ens: z.string().nullable(), + votingPower: z.instanceof(VotingPower), + firstActivityTimestamp: z.number().int().nonnegative(), + lastActivityTimestamp: z.number().int().nonnegative(), + delegationCount: z.number().int().nonnegative(), +}); + +type MemberProps = z.infer; + +export class Member extends ValueObject { + /** + * The member's wallet address. + */ + get address(): Address { + return this.props.address; + } + + /** + * The member's ENS name, or null if not resolved. + */ + get ens(): string | null { + return this.props.ens; + } + + /** + * The member's voting power. + */ + get votingPower(): VotingPower { + return this.props.votingPower; + } + + /** + * Unix-seconds timestamp of the member's first observed activity. + */ + get firstActivityTimestamp(): number { + return this.props.firstActivityTimestamp; + } + + /** + * Unix-seconds timestamp of the member's most recent observed activity. + */ + get lastActivityTimestamp(): number { + return this.props.lastActivityTimestamp; + } + + /** + * Number of distinct accounts currently delegating their voting power + * to this member (counts self-delegation). + */ + get delegationCount(): number { + return this.props.delegationCount; + } + + static create(props: MemberProps): Member { + const validated = MemberPropsSchema.parse(props); + return new Member(validated); + } +} diff --git a/src/domain/token-voting-member/TokenVotingMember.test.ts b/src/domain/token-voting-member/TokenVotingMember.test.ts new file mode 100644 index 0000000..4a1c44f --- /dev/null +++ b/src/domain/token-voting-member/TokenVotingMember.test.ts @@ -0,0 +1,52 @@ +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { TokenVotingMember } from './TokenVotingMember'; + +describe('TokenVotingMember', () => { + const address = Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ); + const pluginAddress = Address.fromHexString( + '0x1111111111111111111111111111111111111111', + ); + const tokenContractAddress = Address.fromHexString( + '0xabcdef0123456789abcdef0123456789abcdef01', + ); + const votingPower = VotingPower.fromBigInt(1000000000000000000n); + const firstActivityTimestamp = 1705320000; + const lastActivityTimestamp = 1718872200; + + const validProps = { + address, + pluginAddress, + tokenContractAddress, + votingPower, + ens: null, + firstActivityTimestamp, + lastActivityTimestamp, + delegationCount: 7, + }; + + it('creates a valid member', () => { + const member = TokenVotingMember.create(validProps); + + expect(member.address.equals(address)).toBe(true); + expect(member.pluginAddress.equals(pluginAddress)).toBe(true); + expect(member.tokenContractAddress.equals(tokenContractAddress)).toBe(true); + expect(member.votingPower.equals(votingPower)).toBe(true); + expect(member.ens).toBeNull(); + expect(member.firstActivityTimestamp).toBe(firstActivityTimestamp); + expect(member.lastActivityTimestamp).toBe(lastActivityTimestamp); + }); + + it('carries an ENS name when present', () => { + const member = TokenVotingMember.create({ ...validProps, ens: 'alice.eth' }); + expect(member.ens).toBe('alice.eth'); + }); + + it('structural equality', () => { + const a = TokenVotingMember.create(validProps); + const b = TokenVotingMember.create(validProps); + expect(a.equals(b)).toBe(true); + }); +}); diff --git a/src/domain/token-voting-member/TokenVotingMember.ts b/src/domain/token-voting-member/TokenVotingMember.ts new file mode 100644 index 0000000..e845bf4 --- /dev/null +++ b/src/domain/token-voting-member/TokenVotingMember.ts @@ -0,0 +1,91 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; + +const TokenVotingMemberPropsSchema = z.object({ + address: z.instanceof(Address), + pluginAddress: z.instanceof(Address), + tokenContractAddress: z.instanceof(Address), + votingPower: z.instanceof(VotingPower), + ens: z.string().nullable(), + firstActivityTimestamp: z.number().int().nonnegative(), + lastActivityTimestamp: z.number().int().nonnegative(), + delegationCount: z.number().int().nonnegative(), +}); + +type TokenVotingMemberProps = z.infer; + +/** + * A member of a TokenVoting plugin: combines on-chain delegation state + * (voting power on the token contract) with per-plugin activity + * timestamps (delegation, votes, proposal creations) sourced from the + * indexer. Timestamps are unix seconds. + */ +export class TokenVotingMember extends ValueObject { + /** + * The member's wallet address. + */ + get address(): Address { + return this.props.address; + } + + /** + * The TokenVoting plugin this member belongs to. + */ + get pluginAddress(): Address { + return this.props.pluginAddress; + } + + /** + * The ERC20Votes token contract this member holds voting power on. + */ + get tokenContractAddress(): Address { + return this.props.tokenContractAddress; + } + + /** + * The member's aggregated voting power on the token contract. + */ + get votingPower(): VotingPower { + return this.props.votingPower; + } + + /** + * The member's primary ENS name, or null if none is registered. + * Sourced from the indexer's ReverseName entity. + */ + get ens(): string | null { + return this.props.ens; + } + + /** + * Unix-seconds timestamp of the member's first observed activity for + * this plugin. + */ + get firstActivityTimestamp(): number { + return this.props.firstActivityTimestamp; + } + + /** + * Unix-seconds timestamp of the member's most recent observed + * activity for this plugin. + */ + get lastActivityTimestamp(): number { + return this.props.lastActivityTimestamp; + } + + /** + * Number of distinct accounts currently delegating their voting + * power to this member on the underlying ERC20Votes token (counts + * self-delegation). Sourced from the indexer. + */ + get delegationCount(): number { + return this.props.delegationCount; + } + + static create(props: TokenVotingMemberProps): TokenVotingMember { + const validated = TokenVotingMemberPropsSchema.parse(props); + return new TokenVotingMember(validated); + } +} diff --git a/src/domain/token-voting-member/TokenVotingMemberStore.ts b/src/domain/token-voting-member/TokenVotingMemberStore.ts new file mode 100644 index 0000000..285ce0f --- /dev/null +++ b/src/domain/token-voting-member/TokenVotingMemberStore.ts @@ -0,0 +1,15 @@ +import type { Page } from '@/domain/primitives/pagination/Page'; +import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { TokenVotingMember } from './TokenVotingMember'; + +export interface TokenVotingMemberStore { + /** + * Finds members of a TokenVoting plugin scoped to its ERC20Votes token contract. + * Returns a page of members sorted by voting power descending. + */ + findMembersByPluginAndToken( + pluginAddress: string, + tokenContractAddress: string, + page: PageRequest, + ): Promise>; +} diff --git a/src/domain/voting-power/VotingPower.test.ts b/src/domain/voting-power/VotingPower.test.ts new file mode 100644 index 0000000..91a5af3 --- /dev/null +++ b/src/domain/voting-power/VotingPower.test.ts @@ -0,0 +1,45 @@ +import { VotingPower } from './VotingPower'; + +describe('VotingPower', () => { + it('creates from bigint', () => { + const vp = VotingPower.fromBigInt(1000000000000000000n); + expect(vp.value.toBigNumber().toString()).toBe('1000000000000000000'); + }); + + it('creates zero', () => { + const vp = VotingPower.zero(); + expect(vp.isZero).toBe(true); + }); + + it('detects non-zero', () => { + const vp = VotingPower.fromBigInt(1n); + expect(vp.isZero).toBe(false); + }); + + it('compares greater than or equal', () => { + const high = VotingPower.fromBigInt(100n); + const low = VotingPower.fromBigInt(50n); + const equal = VotingPower.fromBigInt(100n); + + expect(high.isGreaterThanOrEqual(low)).toBe(true); + expect(high.isGreaterThanOrEqual(equal)).toBe(true); + expect(low.isGreaterThanOrEqual(high)).toBe(false); + }); + + it('structural equality', () => { + const a = VotingPower.fromBigInt(100n); + const b = VotingPower.fromBigInt(100n); + expect(a.equals(b)).toBe(true); + }); + + it('structural inequality', () => { + const a = VotingPower.fromBigInt(100n); + const b = VotingPower.fromBigInt(200n); + expect(a.equals(b)).toBe(false); + }); + + it('converts to ether', () => { + const vp = VotingPower.fromBigInt(1230000000000000000n); + expect(vp.value.toEther().toBigNumber().toNumber()).toBeCloseTo(1.23); + }); +}); diff --git a/src/domain/voting-power/VotingPower.ts b/src/domain/voting-power/VotingPower.ts new file mode 100644 index 0000000..3d0454c --- /dev/null +++ b/src/domain/voting-power/VotingPower.ts @@ -0,0 +1,65 @@ +import BigNumber from 'bignumber.js'; +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { Wei } from '@/domain/primitives'; + +const VotingPowerPropsSchema = z.object({ + value: z.instanceof(Wei), +}); + +type VotingPowerProps = z.infer; + +export class VotingPower extends ValueObject { + /** + * The voting power in wei. + */ + get value(): Wei { + return this.props.value; + } + + /** + * Whether this voting power is zero. + */ + get isZero(): boolean { + return this.props.value.toBigNumber().isZero(); + } + + /** + * Structural equality based on the underlying wei value. + */ + equals(other?: VotingPower): boolean { + if (!other) { + return false; + } + return this.props.value.toBigNumber().isEqualTo(other.value.toBigNumber()); + } + + /** + * Compares this voting power to another. + * Returns true if this voting power is greater than or equal to the other. + */ + isGreaterThanOrEqual(other: VotingPower): boolean { + return this.props.value + .toBigNumber() + .isGreaterThanOrEqualTo(other.value.toBigNumber()); + } + + static create(value: Wei): VotingPower { + const validated = VotingPowerPropsSchema.parse({ value }); + return new VotingPower(validated); + } + + /** + * Creates a VotingPower from a bigint value in wei. + */ + static fromBigInt(wei: bigint): VotingPower { + return VotingPower.create(Wei.create(new BigNumber(wei.toString()))); + } + + /** + * Creates a zero voting power. + */ + static zero(): VotingPower { + return VotingPower.create(Wei.create(new BigNumber(0))); + } +} diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index 5692f23..ac857e2 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -1,16 +1,31 @@ import type { HandlerDefinition } from 'ddd-core-ts'; import { handleRequest } from 'ddd-core-ts'; +import type { Member } from '@/domain/member/Member'; import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; +import type { Page } from '@/domain/primitives/pagination/Page'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberProfileStore } from '@/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore'; +import { EnvioTokenVotingMemberStore } from '@/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore'; +import type { GetERC20MembershipUseCaseProps } from '@/use-cases/GetERC20MembershipUseCase'; +import { GetERC20MembershipUseCase } from '@/use-cases/GetERC20MembershipUseCase'; import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import { GetMemberProfileTextRecordsUseCase } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import type { MemberProfileTextRecordDTO } from './maps/domain/MemberProfileTextRecordMap'; import * as MemberProfileTextRecordMap from './maps/domain/MemberProfileTextRecordMap'; +import type { PaginatedMemberDTO } from './maps/domain/PaginatedMemberMap'; +import * as PaginatedMemberMap from './maps/domain/PaginatedMemberMap'; +import type { GetERC20MembershipRequestDTO } from './maps/use-cases/GetERC20MembershipMap'; +import * as GetERC20MembershipMap from './maps/use-cases/GetERC20MembershipMap'; import type { GetMemberProfileTextRecordsRequestDTO } from './maps/use-cases/GetMemberProfileTextRecordsMap'; import * as GetMemberProfileTextRecordsMap from './maps/use-cases/GetMemberProfileTextRecordsMap'; interface HandlersRecord { + getERC20Membership: HandlerDefinition< + GetERC20MembershipRequestDTO, + GetERC20MembershipUseCaseProps, + Page, + PaginatedMemberDTO + >; getMemberProfileTextRecords: HandlerDefinition< GetMemberProfileTextRecordsRequestDTO, GetMemberProfileTextRecordsUseCaseProps, @@ -23,11 +38,21 @@ export class AragonController { private constructor(private readonly handlers: HandlersRecord) {} static load(envioClient: EnvioClient): AragonController { + const memberStore = new EnvioTokenVotingMemberStore(envioClient); + const getERC20MembershipUseCase = new GetERC20MembershipUseCase( + memberStore, + ); + const memberProfileStore = new EnvioMemberProfileStore(envioClient); const getMemberProfileTextRecordsUseCase = new GetMemberProfileTextRecordsUseCase(memberProfileStore); const handlers: HandlersRecord = { + getERC20Membership: { + requestMapper: GetERC20MembershipMap, + responseMapper: PaginatedMemberMap, + useCaseExecutor: getERC20MembershipUseCase, + }, getMemberProfileTextRecords: { requestMapper: GetMemberProfileTextRecordsMap, responseMapper: MemberProfileTextRecordMap, @@ -38,6 +63,15 @@ export class AragonController { return new AragonController(handlers); } + /** + * Returns a page of members of an Aragon TokenVoting plugin, scoped + * to a specific ERC20Votes token contract and ordered by current + * voting power descending. + */ + public getMembership(dto: GetERC20MembershipRequestDTO) { + return handleRequest(this.handlers.getERC20Membership, dto); + } + /** * Looks up the live forward ENS text records attached to a member's `.aragon.eth` * subdomain. Returns an empty list when the subdomain is unknown, has diff --git a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts new file mode 100644 index 0000000..4dfe5fb --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts @@ -0,0 +1,33 @@ +import type { Member } from '@/domain/member/Member'; + +/** + * DTO matching the frontend's ITokenMember interface. + * + * Activity fields are unix-seconds timestamps. The frontend can render + * them directly without resolving block numbers via RPC. + */ +export interface MemberDTO { + address: string; + ens: string | null; + votingPower: string | null; + metrics: { + firstActivityTimestamp: number | null; + lastActivityTimestamp: number | null; + delegationCount: number; + }; +} + +export function mapDomainToDTO(member: Member): MemberDTO { + const first = member.firstActivityTimestamp; + const last = member.lastActivityTimestamp; + return { + address: member.address.toHexString(), + ens: member.ens, + votingPower: member.votingPower.value.toBigNumber().toFixed(0), + metrics: { + firstActivityTimestamp: first === 0 ? null : first, + lastActivityTimestamp: last === 0 ? null : last, + delegationCount: member.delegationCount, + }, + }; +} diff --git a/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts new file mode 100644 index 0000000..f84c49a --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts @@ -0,0 +1,26 @@ +import type { Member } from '@/domain/member/Member'; +import type { Page } from '@/domain/primitives/pagination/Page'; +import type { MemberDTO } from './MemberMap'; +import * as MemberMap from './MemberMap'; + +export interface PaginatedMemberDTO { + metadata: { + page: number; + pageSize: number; + totalPages: number; + totalRecords: number; + }; + data: MemberDTO[]; +} + +export function mapDomainToDTO(result: Page): PaginatedMemberDTO { + return { + metadata: { + page: result.page, + pageSize: result.pageSize, + totalPages: result.totalPages, + totalRecords: result.totalRecords, + }, + data: result.items.map(MemberMap.mapDomainToDTO), + }; +} diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts new file mode 100644 index 0000000..c58e373 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts @@ -0,0 +1,22 @@ +import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { GetERC20MembershipUseCaseProps } from '@/use-cases/GetERC20MembershipUseCase'; + +export interface GetERC20MembershipRequestDTO { + pluginAddress: string; + tokenContractAddress: string; + page?: number; + pageSize?: number; +} + +export function mapDTOToDomain( + dto: GetERC20MembershipRequestDTO, +): GetERC20MembershipUseCaseProps { + return { + pluginAddress: dto.pluginAddress, + tokenContractAddress: dto.tokenContractAddress, + page: PageRequest.create({ + page: dto.page ?? 1, + pageSize: dto.pageSize ?? 20, + }), + }; +} diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts new file mode 100644 index 0000000..c5ce609 --- /dev/null +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts @@ -0,0 +1,165 @@ +import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; +import { EnvioTokenVotingMemberStore } from './EnvioTokenVotingMemberStore'; + +const PLUGIN = '0x1111111111111111111111111111111111111111'; +const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + +const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; +const BOB = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + +const ETH_COIN_TYPE = '60'; +const DEFAULT_EVM_COIN_TYPE = '2147483648'; // 0x80000000 + +interface QueryCall { + document: string; + variables: Record; +} + +interface MockClient { + envio: EnvioClient; + calls: QueryCall[]; +} + +/** + * Builds an EnvioClient stub that returns each `response` in order + * for the corresponding call. The first call resolves with the + * members response; the second with the reverse-names response. + */ +function buildMockEnvioClient(responses: unknown[]): MockClient { + const calls: QueryCall[] = []; + const queue = [...responses]; + const envio = { + query: jest.fn(async (document: string, variables: Record) => { + calls.push({ document, variables }); + return queue.shift(); + }), + } as unknown as EnvioClient; + return { envio, calls }; +} + +const buildDelegate = (address: string, votingPower = '5000000000000000000') => ({ + id: `1-${TOKEN}-${address}`, + chainId: 1, + tokenContractAddress: TOKEN, + delegateAddress: address, + votingPower, + delegationCount: 1, + firstVotingPowerChangeTimestamp: '1700000000', + lastVotingPowerChangeTimestamp: '1700000100', +}); + +describe('EnvioTokenVotingMemberStore', () => { + const page = PageRequest.create({ page: 1, pageSize: 20 }); + + it('attaches ENS names from the indexer ReverseName entity to each member', async () => { + const { envio, calls } = buildMockEnvioClient([ + { + ERC20VotesDelegate: [buildDelegate(ALICE), buildDelegate(BOB)], + AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }], + MemberMetrics: [], + }, + { + ReverseName: [ + { address: ALICE, coinType: DEFAULT_EVM_COIN_TYPE, name: 'alice.eth' }, + ], + }, + ]); + const store = new EnvioTokenVotingMemberStore(envio); + + const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + + expect(result.items.map((m) => m.ens)).toEqual(['alice.eth', null]); + + // The reverse-name query was scoped to just the page's addresses. + expect(calls).toHaveLength(2); + expect(calls[1].variables).toEqual({ addresses: [ALICE, BOB] }); + }); + + it('prefers the ENSIP-19 default.reverse name (coinType 0x80000000) over the legacy coinType 60 entry', async () => { + // Indexer is ordered by coinType desc, so the modern row arrives + // first. Verifying that the dedupe keeps the first row per address. + const { envio } = buildMockEnvioClient([ + { + ERC20VotesDelegate: [buildDelegate(ALICE)], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], + }, + { + ReverseName: [ + { address: ALICE, coinType: DEFAULT_EVM_COIN_TYPE, name: 'alice-default.eth' }, + { address: ALICE, coinType: ETH_COIN_TYPE, name: 'alice-legacy.eth' }, + ], + }, + ]); + const store = new EnvioTokenVotingMemberStore(envio); + + const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + + expect(result.items[0].ens).toBe('alice-default.eth'); + }); + + it('falls back to legacy coinType 60 when no default.reverse row exists', async () => { + const { envio } = buildMockEnvioClient([ + { + ERC20VotesDelegate: [buildDelegate(ALICE)], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], + }, + { + ReverseName: [ + { address: ALICE, coinType: ETH_COIN_TYPE, name: 'alice-legacy.eth' }, + ], + }, + ]); + const store = new EnvioTokenVotingMemberStore(envio); + + const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + + expect(result.items[0].ens).toBe('alice-legacy.eth'); + }); + + it('skips the reverse-name query when the page is empty', async () => { + const { envio, calls } = buildMockEnvioClient([ + { + ERC20VotesDelegate: [], + AllERC20VotesDelegate: [], + MemberMetrics: [], + }, + ]); + const store = new EnvioTokenVotingMemberStore(envio); + + const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + + expect(result.items).toHaveLength(0); + expect(calls).toHaveLength(1); + }); + + it('lowercases the addresses passed to the reverse-name query', async () => { + const ALICE_MIXED = '0x0123456789ABCDEF0123456789ABCDEF01234567'; + const { envio, calls } = buildMockEnvioClient([ + { + ERC20VotesDelegate: [buildDelegate(ALICE_MIXED)], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], + }, + { ReverseName: [] }, + ]); + const store = new EnvioTokenVotingMemberStore(envio); + + await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + + expect(calls[1].variables).toEqual({ addresses: [ALICE] }); + }); + + it('wraps query failures with a member-level error', async () => { + const envio = { + query: jest.fn().mockRejectedValue(new Error('graphql exploded')), + } as unknown as EnvioClient; + const store = new EnvioTokenVotingMemberStore(envio); + + await expect( + store.findMembersByPluginAndToken(PLUGIN, TOKEN, page), + ).rejects.toThrow('Error querying members from Envio'); + }); +}); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts new file mode 100644 index 0000000..ec7e248 --- /dev/null +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts @@ -0,0 +1,161 @@ +import type { Page } from '@/domain/primitives/pagination/Page'; +import { createPage } from '@/domain/primitives/pagination/Page'; +import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; +import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; +import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; +import * as ReverseNameMap from './maps/ReverseNameMap'; +import * as TokenVotingMemberMap from './maps/TokenVotingMemberMap'; + +/** + * Fetches a page of delegates by token (ordered by VP desc), every + * delegate's id (for total-count), and the MemberMetrics for the + * plugin — all in one round-trip. + * + * The query straddles the indexer's generic / Aragon partition: + * - ERC20VotesDelegate is generic (per-token, chain-wide) + * - MemberMetrics is Aragon-specific (per-plugin) + * + * Hasura cannot express a cross-entity join here, so the pairing + * happens client-side. That client-side merge is also what makes the + * eventual indexer split a no-op for this file beyond pointing the + * two halves at separate GraphQL endpoints. + */ +const FIND_MEMBERS_QUERY = ` + query FindMembers( + $tokenContractAddress: String! + $pluginAddress: String! + $limit: Int! + $offset: Int! + ) { + ERC20VotesDelegate( + where: { + tokenContractAddress: { _eq: $tokenContractAddress } + votingPower: { _gt: "0" } + } + order_by: [{ votingPower: desc }] + limit: $limit + offset: $offset + ) { + id + chainId + tokenContractAddress + delegateAddress + votingPower + delegationCount + firstVotingPowerChangeTimestamp + lastVotingPowerChangeTimestamp + } + AllERC20VotesDelegate: ERC20VotesDelegate( + where: { + tokenContractAddress: { _eq: $tokenContractAddress } + votingPower: { _gt: "0" } + } + ) { + id + } + MemberMetrics( + where: { + pluginAddress: { _eq: $pluginAddress } + } + ) { + id + chainId + pluginAddress + memberAddress + firstActivityTimestamp + lastActivityTimestamp + } + } +`; + +/** + * Fetches every `ReverseName` row that matches the given page of + * delegate addresses. An address can have up to two rows on mainnet + * (legacy `coinType=60` and ENSIP-19 `coinType=0x80000000`); we pull + * both and pick precedence client-side via `ReverseNameMap`. + * + * Ordering by `coinType` descending puts `0x80000000` (2147483648) + * ahead of `60`, so the first row per address in the dedupe pass is + * the preferred one. + */ +const FIND_REVERSE_NAMES_QUERY = ` + query FindReverseNames($addresses: [String!]!) { + ReverseName( + where: { address: { _in: $addresses } } + order_by: [{ coinType: desc }] + ) { + address + coinType + name + } + } +`; + +export class EnvioTokenVotingMemberStore implements TokenVotingMemberStore { + constructor(private readonly envio: EnvioClient) {} + + public async findMembersByPluginAndToken( + pluginAddress: string, + tokenContractAddress: string, + request: PageRequest, + ): Promise> { + try { + const pluginAddressLower = pluginAddress.toLowerCase(); + const tokenAddressLower = tokenContractAddress.toLowerCase(); + + const rawMembers = await this.envio.query(FIND_MEMBERS_QUERY, { + tokenContractAddress: tokenAddressLower, + pluginAddress: pluginAddressLower, + limit: request.pageSize, + offset: request.offset, + }); + const data = TokenVotingMemberMap.parseFindMembersResponse(rawMembers); + + const metricsByMember = new Map( + data.MemberMetrics.map((m) => [m.memberAddress.toLowerCase(), m]), + ); + + const pageAddresses = data.ERC20VotesDelegate.map((d) => + d.delegateAddress.toLowerCase(), + ); + const ensByAddress = await this.fetchReverseNames(pageAddresses); + + const members = data.ERC20VotesDelegate.map((delegate) => { + const addressLower = delegate.delegateAddress.toLowerCase(); + return TokenVotingMemberMap.mapDTOToDomain( + delegate, + metricsByMember.get(addressLower), + ensByAddress.get(addressLower) ?? null, + pluginAddressLower, + ); + }); + + const totalRecords = data.AllERC20VotesDelegate.length; + + return createPage(members, request.page, request.pageSize, totalRecords); + } catch (cause) { + throw new Error('Error querying members from Envio', { cause }); + } + } + + /** + * Resolves a batch of lowercase addresses to their preferred ENS + * primary names via the indexer's `ReverseName` entity. + * + * Returns an empty map when given no addresses — avoids issuing an + * empty `_in` query. + */ + private async fetchReverseNames( + lowercaseAddresses: string[], + ): Promise> { + if (lowercaseAddresses.length === 0) { + return new Map(); + } + + const raw = await this.envio.query(FIND_REVERSE_NAMES_QUERY, { + addresses: lowercaseAddresses, + }); + return ReverseNameMap.mapDTOToDomain(raw); + } +} diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts new file mode 100644 index 0000000..4db7c24 --- /dev/null +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts @@ -0,0 +1,44 @@ +import { z } from 'zod'; + +/** + * Shape of a single `ReverseName` row. + * + * An address can have up to two rows on mainnet (legacy `coinType=60` + * and ENSIP-19 `coinType=0x80000000`). The query orders by `coinType` + * descending, so the first row per address in the dedupe pass is the + * preferred one. + */ +const ReverseNameRowSchema = z.object({ + address: z.string(), + coinType: z.string(), + name: z.string(), +}); + +const FindReverseNamesResponseSchema = z.object({ + ReverseName: z.array(ReverseNameRowSchema), +}); + +export type ReverseNameDTO = z.infer; +export type FindReverseNamesResponse = z.infer< + typeof FindReverseNamesResponseSchema +>; + +/** + * Trust boundary for the indexer's `FindReverseNames` response. + * Asserts shape via Zod, dedupes to one row per address (keeping the + * highest-coinType row since the query orders descending), and + * returns the result as a `Map`. + * + * Throws a `ZodError` if the shape doesn't match what we expect. + */ +export function mapDTOToDomain(raw: unknown): Map { + const parsed = FindReverseNamesResponseSchema.parse(raw); + const namesByAddress = new Map(); + for (const row of parsed.ReverseName) { + const address = row.address.toLowerCase(); + if (!namesByAddress.has(address)) { + namesByAddress.set(address, row.name); + } + } + return namesByAddress; +} diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts new file mode 100644 index 0000000..1b29ff2 --- /dev/null +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts @@ -0,0 +1,142 @@ +import { z } from 'zod'; +import { Address, assertHexString } from '@/domain/primitives'; +import { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; + +/** + * Shape of an `ERC20VotesDelegate` row on the indexer. + * + * `firstVotingPowerChangeTimestamp` / `lastVotingPowerChangeTimestamp` + * are nullable: a delegate row can exist without ever having had a + * DelegateVotesChanged event (e.g. created from a `+1` to + * delegationCount via a DelegateChanged event on a zero balance). + */ +const ERC20VotesDelegateSchema = z.object({ + id: z.string(), + chainId: z.number(), + tokenContractAddress: z.string(), + delegateAddress: z.string(), + votingPower: z.string(), + delegationCount: z.number(), + firstVotingPowerChangeTimestamp: z.string().nullable(), + lastVotingPowerChangeTimestamp: z.string().nullable(), +}); + +const MemberMetricsSchema = z.object({ + id: z.string(), + chainId: z.number(), + pluginAddress: z.string(), + memberAddress: z.string(), + firstActivityTimestamp: z.string(), + lastActivityTimestamp: z.string(), +}); + +/** + * Shape of the `FindMembers` GraphQL response. Three top-level lists: + * + * - `ERC20VotesDelegate` — the requested page of delegates + * - `AllERC20VotesDelegate` — every delegate id (used for total count) + * - `MemberMetrics` — all member-metrics rows for the plugin + * + * The merge of `ERC20VotesDelegate` (generic / chain-wide) with + * `MemberMetrics` (Aragon-specific) happens client-side; see the + * store for the orchestration. + */ +const FindMembersResponseSchema = z.object({ + ERC20VotesDelegate: z.array(ERC20VotesDelegateSchema), + AllERC20VotesDelegate: z.array(z.object({ id: z.string() })), + MemberMetrics: z.array(MemberMetricsSchema), +}); + +export type ERC20VotesDelegateDTO = z.infer; +export type MemberMetricsDTO = z.infer; +export type FindMembersResponse = z.infer; + +/** + * Trust boundary for the indexer's `FindMembers` response. Throws a + * `ZodError` if the shape doesn't match. + */ +export function parseFindMembersResponse(raw: unknown): FindMembersResponse { + return FindMembersResponseSchema.parse(raw); +} + +/** + * Smallest of the defined inputs (treats `null`/`undefined` as + * "no signal"). Returns `0` if neither side has a value. + */ +function minDefined(...values: Array): number { + let min: number | undefined; + for (const v of values) { + if (v == null) continue; + const n = Number(v); + if (min === undefined || n < min) min = n; + } + return min ?? 0; +} + +/** + * Largest of the defined inputs. Returns `0` if neither side has a + * value. + */ +function maxDefined(...values: Array): number { + let max: number | undefined; + for (const v of values) { + if (v == null) continue; + const n = Number(v); + if (max === undefined || n > max) max = n; + } + return max ?? 0; +} + +/** + * Maps a single `ERC20VotesDelegate` row (plus its companion + * `MemberMetrics` row and any attached ENS name) to a domain + * `TokenVotingMember`. Verifies that the address fields look like + * `0x`-prefixed hex before passing them through to the domain + * primitives — `Address.fromHexString` does the final shape check. + */ +export function mapDTOToDomain( + delegate: ERC20VotesDelegateDTO, + metrics: MemberMetricsDTO | undefined, + ens: string | null, + pluginAddress: string, +): TokenVotingMember { + assertHexString( + delegate.delegateAddress, + 'delegateAddress must be a 0x-prefixed hex string', + ); + assertHexString( + delegate.tokenContractAddress, + 'tokenContractAddress must be a 0x-prefixed hex string', + ); + assertHexString( + pluginAddress, + 'pluginAddress must be a 0x-prefixed hex string', + ); + + // Activity is the merge of two signals: + // - MemberMetrics: VoteCast / ProposalCreated activity in this plugin + // - ERC20VotesDelegate: voting-power changes for this delegate-token pair + // The indexer keeps these denormalized across the generic / Aragon + // partition; the merge happens here so that a future split into two + // indexers needs no schema change beyond two GraphQL endpoints. + const firstActivityTimestamp = minDefined( + metrics?.firstActivityTimestamp, + delegate.firstVotingPowerChangeTimestamp, + ); + const lastActivityTimestamp = maxDefined( + metrics?.lastActivityTimestamp, + delegate.lastVotingPowerChangeTimestamp, + ); + + return TokenVotingMember.create({ + address: Address.fromHexString(delegate.delegateAddress), + pluginAddress: Address.fromHexString(pluginAddress), + tokenContractAddress: Address.fromHexString(delegate.tokenContractAddress), + votingPower: VotingPower.fromBigInt(BigInt(delegate.votingPower)), + ens, + firstActivityTimestamp, + lastActivityTimestamp, + delegationCount: delegate.delegationCount, + }); +} diff --git a/src/use-cases/GetERC20MembershipUseCase.test.ts b/src/use-cases/GetERC20MembershipUseCase.test.ts new file mode 100644 index 0000000..51a4297 --- /dev/null +++ b/src/use-cases/GetERC20MembershipUseCase.test.ts @@ -0,0 +1,100 @@ +import { Address } from '@/domain/primitives'; +import { createPage } from '@/domain/primitives/pagination/Page'; +import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; +import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { GetERC20MembershipUseCase } from './GetERC20MembershipUseCase'; + +describe('GetERC20MembershipUseCase', () => { + const memberAddressValue = '0x1234567890abcdef1234567890abcdef12345678'; + const memberAddress = Address.fromHexString(memberAddressValue); + const pluginAddressValue = '0x1111111111111111111111111111111111111111'; + const pluginAddressObj = Address.fromHexString(pluginAddressValue); + const tokenContractAddressValue = + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const tokenContractAddressObj = Address.fromHexString( + tokenContractAddressValue, + ); + const firstActivityTimestamp = 1705320000; + const lastActivityTimestamp = 1718872200; + + const buildMember = (ens: string | null = null): TokenVotingMember => + TokenVotingMember.create({ + address: memberAddress, + pluginAddress: pluginAddressObj, + tokenContractAddress: tokenContractAddressObj, + votingPower: VotingPower.fromBigInt(5000000000000000000n), + ens, + firstActivityTimestamp, + lastActivityTimestamp, + delegationCount: 4, + }); + + const buildMockStore = ( + member: TokenVotingMember = buildMember(), + ): TokenVotingMemberStore => ({ + findMembersByPluginAndToken: jest + .fn() + .mockResolvedValue(createPage([member], 1, 20, 1)), + }); + + const page = PageRequest.create({ page: 1, pageSize: 20 }); + const pluginAddress = pluginAddressValue; + const tokenContractAddress = tokenContractAddressValue; + + it('returns a page of members', async () => { + const useCase = new GetERC20MembershipUseCase(buildMockStore()); + const result = await useCase.execute({ + pluginAddress, + tokenContractAddress, + page, + }); + + expect(result.items).toHaveLength(1); + expect(result.items[0].address.equals(memberAddress)).toBe(true); + expect(result.items[0].votingPower.isZero).toBe(false); + expect(result.items[0].ens).toBeNull(); + expect(result.items[0].firstActivityTimestamp).toBe(firstActivityTimestamp); + expect(result.items[0].lastActivityTimestamp).toBe(lastActivityTimestamp); + expect(result.totalRecords).toBe(1); + }); + + it('passes plugin address, token address, and page to store', async () => { + const store = buildMockStore(); + const useCase = new GetERC20MembershipUseCase(store); + await useCase.execute({ pluginAddress, tokenContractAddress, page }); + + expect(store.findMembersByPluginAndToken).toHaveBeenCalledWith( + pluginAddress, + tokenContractAddress, + page, + ); + }); + + it('forwards the ENS name attached to each TokenVotingMember', async () => { + const useCase = new GetERC20MembershipUseCase( + buildMockStore(buildMember('alice.eth')), + ); + const result = await useCase.execute({ + pluginAddress, + tokenContractAddress, + page, + }); + + expect(result.items[0].ens).toBe('alice.eth'); + }); + + it('wraps store errors', async () => { + const failingStore: TokenVotingMemberStore = { + findMembersByPluginAndToken: jest + .fn() + .mockRejectedValue(new Error('db down')), + }; + const useCase = new GetERC20MembershipUseCase(failingStore); + + await expect( + useCase.execute({ pluginAddress, tokenContractAddress, page }), + ).rejects.toThrow('Error while getting ERC20 membership'); + }); +}); diff --git a/src/use-cases/GetERC20MembershipUseCase.ts b/src/use-cases/GetERC20MembershipUseCase.ts new file mode 100644 index 0000000..9089083 --- /dev/null +++ b/src/use-cases/GetERC20MembershipUseCase.ts @@ -0,0 +1,59 @@ +import type { UseCase } from 'ddd-core-ts'; +import { Member } from '@/domain/member/Member'; +import type { Page } from '@/domain/primitives/pagination/Page'; +import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; +import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; + +export interface GetERC20MembershipUseCaseProps { + pluginAddress: string; + tokenContractAddress: string; + page: PageRequest; +} + +/** + * Returns a page of members of an Aragon TokenVoting plugin, scoped + * to a specific ERC20Votes token contract and ordered by current + * voting power descending. + */ +export class GetERC20MembershipUseCase + implements UseCase> +{ + public readonly code = 'GetERC20MembershipUseCase'; + + constructor(private readonly memberStore: TokenVotingMemberStore) {} + + public async execute( + props: GetERC20MembershipUseCaseProps, + ): Promise> { + try { + const membersPage = await this.memberStore.findMembersByPluginAndToken( + props.pluginAddress, + props.tokenContractAddress, + props.page, + ); + + const members = membersPage.items.map((member) => this.toMember(member)); + + return { ...membersPage, items: members }; + } catch (cause) { + throw new Error('Error while getting ERC20 membership', { cause }); + } + } + + /** + * Converts a raw indexer member into a Member domain object. + * ENS identity is already attached by the store via the indexer's + * ReverseName entity, so no further enrichment is required here. + */ + private toMember(member: TokenVotingMember): Member { + return Member.create({ + address: member.address, + ens: member.ens, + votingPower: member.votingPower, + firstActivityTimestamp: member.firstActivityTimestamp, + lastActivityTimestamp: member.lastActivityTimestamp, + delegationCount: member.delegationCount, + }); + } +} diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 579f822..3acaf2b 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -1,9 +1,13 @@ import assert from 'node:assert'; import { AragonSubdomain, EnvioClient } from '../src'; +const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; +const PLUGIN = '0x17a1688c56087ade762721180e1cc1e831c73719'; +const TOKEN = '0x0a830e9f2baa2ebaf8d33c0806283dea9c08952f'; const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; const NODE = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +const DEFAULT_EVM_COIN_TYPE = '2147483648'; // 0x80000000 /** * Builds an AragonSubdomain backed by an EnvioClient whose `query` @@ -24,6 +28,61 @@ function buildController(responses: unknown[]): AragonSubdomain { } describe('AragonSubdomain', () => { + describe('getMembership', () => { + it('returns a paginated DTO of ERC20 members', async () => { + const controller = buildController([ + { + ERC20VotesDelegate: [ + { + id: `1-${TOKEN}-${ALICE}`, + chainId: 1, + tokenContractAddress: TOKEN, + delegateAddress: ALICE, + votingPower: '5000000000000000000', + delegationCount: 2, + firstVotingPowerChangeTimestamp: '1700000000', + lastVotingPowerChangeTimestamp: '1700000100', + }, + ], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], + }, + { + ReverseName: [ + { + address: ALICE, + coinType: DEFAULT_EVM_COIN_TYPE, + name: 'alice.eth', + }, + ], + }, + ]); + + const response = await controller.getMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 15, + }); + + assert(response.success, 'expected getMembership to succeed'); + expect(response.result.metadata).toEqual({ + page: 1, + pageSize: 15, + totalPages: 1, + totalRecords: 1, + }); + expect(response.result.data).toHaveLength(1); + expect(response.result.data[0]).toEqual( + expect.objectContaining({ + ens: 'alice.eth', + votingPower: '5000000000000000000', + }), + ); + expect(response.result.data[0].address).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + }); + describe('getMemberProfileTextRecords', () => { const textId = (version: string, key: string) => `1-${RESOLVER}-${NODE}-${version}-${key}`; From 9168465f84e0cd88aab8307f7f413955818d9abb Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 20:57:07 +0100 Subject: [PATCH 03/33] Changeset --- .changeset/funky-streets-deny.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funky-streets-deny.md diff --git a/.changeset/funky-streets-deny.md b/.changeset/funky-streets-deny.md new file mode 100644 index 0000000..6481253 --- /dev/null +++ b/.changeset/funky-streets-deny.md @@ -0,0 +1,5 @@ +--- +"@aragon/aragon-subdomain": minor +--- + +Implement ERC20 token voting membership use case From 5e76da980dc6940b4676220ddc4523d21ba0d393 Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 20:57:41 +0100 Subject: [PATCH 04/33] Changeset --- .changeset/crisp-moose-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/crisp-moose-glow.md diff --git a/.changeset/crisp-moose-glow.md b/.changeset/crisp-moose-glow.md new file mode 100644 index 0000000..86fe07e --- /dev/null +++ b/.changeset/crisp-moose-glow.md @@ -0,0 +1,5 @@ +--- +"@aragon/aragon-subdomain": minor +--- + +Look up member profile ENS records From 516a4679f4c5a5b3c8647968db6dcc7ace4223d9 Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 21:49:02 +0100 Subject: [PATCH 05/33] Upgrade to vitest --- .changeset/config.json | 30 +- .github/actions/setup/action.yml | 2 +- .github/workflows/library-test.yml | 4 +- .github/workflows/scripts/readChangelog.js | 56 +- .gitignore | 1 + .npmrc | 2 +- .nvmrc | 2 +- biome.json | 26 +- jest.config.js | 18 - package.json | 14 +- pnpm-lock.yaml | 2687 +++++------------ .../member-profile/MemberProfileAragonName.ts | 4 +- .../member-profile/MemberProfileTextRecord.ts | 4 +- .../primitives/blockchain/Address.test.ts | 15 + .../events/SomethingWentWrong.test.ts | 16 + src/domain/primitives/pagination/Page.test.ts | 26 + src/domain/primitives/units/Ether.test.ts | 5 + src/domain/primitives/units/Gwei.test.ts | 10 + src/domain/primitives/units/Wei.test.ts | 5 + src/infrastructure/stores/EnvioClient.test.ts | 86 + .../EnvioMemberProfileStore.test.ts | 19 +- .../maps/MemberProfileTextRecordMap.test.ts | 22 + ...GetMemberProfileTextRecordsUseCase.test.ts | 4 +- .../GetMemberProfileTextRecordsUseCase.ts | 9 +- test/AragonSubdomain.test.ts | 6 +- tsconfig.json | 2 +- vitest.config.ts | 35 + 27 files changed, 1032 insertions(+), 2078 deletions(-) delete mode 100644 jest.config.js create mode 100644 src/domain/primitives/events/SomethingWentWrong.test.ts create mode 100644 src/domain/primitives/pagination/Page.test.ts create mode 100644 src/infrastructure/stores/EnvioClient.test.ts create mode 100644 src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts create mode 100644 vitest.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 8963b0d..19b5f72 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,15 +1,19 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", - "access": "public", - "baseBranch": "main", - "changedFilePatterns": ["src/**"], - "changelog": [ - "@changesets/changelog-github", - { "repo": "aragon/aragon-subdomain" } - ], - "commit": false, - "fixed": [], - "ignore": [], - "linked": [], - "updateInternalDependencies": "patch" + "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", + "access": "public", + "baseBranch": "main", + "changedFilePatterns": [ + "src/**" + ], + "changelog": [ + "@changesets/changelog-github", + { + "repo": "aragon/aragon-subdomain" + } + ], + "commit": false, + "fixed": [], + "ignore": [], + "linked": [], + "updateInternalDependencies": "patch" } diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index d85b7d3..bd1d4d6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,7 +20,7 @@ inputs: node-version: description: "The version of Node.js to use for the workflow" required: false - default: "22" + default: "24" node-cache: description: "Specifies which package manager to use for caching" required: false diff --git a/.github/workflows/library-test.yml b/.github/workflows/library-test.yml index 79a4fe5..813d14a 100644 --- a/.github/workflows/library-test.yml +++ b/.github/workflows/library-test.yml @@ -29,7 +29,9 @@ jobs: fetch-depth: 0 - name: Build library run: pnpm build - - name: Run tests + - name: Lint + run: pnpm lint:ci + - name: Run tests with coverage (fails below 100%) run: pnpm test - name: Check types run: pnpm type-check diff --git a/.github/workflows/scripts/readChangelog.js b/.github/workflows/scripts/readChangelog.js index b85172a..ec62167 100644 --- a/.github/workflows/scripts/readChangelog.js +++ b/.github/workflows/scripts/readChangelog.js @@ -1,40 +1,40 @@ const fs = require('node:fs'); module.exports = async ({ core }) => { - const { version, path } = process.env; + const { version, path } = process.env; - // Check if version and path are provided - if (!version || !path) { - core.setFailed('Missing required environment variables: version or path'); - return; - } + // Check if version and path are provided + if (!version || !path) { + core.setFailed('Missing required environment variables: version or path'); + return; + } - // Check if the changelog file exists - if (!fs.existsSync(path)) { - core.setFailed(`Changelog file not found at path: ${path}`); - return; - } + // Check if the changelog file exists + if (!fs.existsSync(path)) { + core.setFailed(`Changelog file not found at path: ${path}`); + return; + } - try { - core.info(`Reading changes in version ${version}.`); + try { + core.info(`Reading changes in version ${version}.`); - const changelog = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' }); + const changelog = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' }); - const versionChanges = changelog - .split(/(?=## \d+\.\d+\.\d+)/g) - .find((changes) => changes.startsWith(`## ${version}`)); + const versionChanges = changelog + .split(/(?=## \d+\.\d+\.\d+)/g) + .find((changes) => changes.startsWith(`## ${version}`)); - if (!versionChanges) { - core.warning(`No changes found for version ${version} in the changelog.`); - core.setOutput('changes', 'No changes.'); - return; - } + if (!versionChanges) { + core.warning(`No changes found for version ${version} in the changelog.`); + core.setOutput('changes', 'No changes.'); + return; + } - const parsedChanges = versionChanges.replace(`## ${version}`, '').trim(); + const parsedChanges = versionChanges.replace(`## ${version}`, '').trim(); - core.info(`Setting output: ${parsedChanges}.`); - core.setOutput('changes', parsedChanges); - } catch (error) { - core.setFailed(error); - } + core.info(`Setting output: ${parsedChanges}.`); + core.setOutput('changes', parsedChanges); + } catch (error) { + core.setFailed(error); + } }; diff --git a/.gitignore b/.gitignore index a55fb2d..4df1645 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .DS_Store .env *.tsbuildinfo +coverage/ diff --git a/.npmrc b/.npmrc index 60e78c0..cad39a2 100644 --- a/.npmrc +++ b/.npmrc @@ -13,5 +13,5 @@ ignore-scripts=true auto-install-types=true # Node version management by pnpm -node-version=22.20.0 +node-version=24.13.0 engine-strict=true diff --git a/.nvmrc b/.nvmrc index 442c758..3fe3b15 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.20.0 +24.13.0 diff --git a/biome.json b/biome.json index 836c691..3afc277 100644 --- a/biome.json +++ b/biome.json @@ -27,7 +27,7 @@ "trailingCommas": "all" }, "globals": [ - "jest", + "vi", "describe", "it", "test", @@ -130,10 +130,30 @@ }, { "includes": [ - "**/*.test.ts" + "**/*.test.ts", + "test/**/*.ts" ], "linter": { - "rules": {} + "rules": { + "suspicious": { + "useAwait": "off" + } + } + } + }, + { + "includes": [ + ".github/workflows/scripts/**" + ], + "linter": { + "rules": { + "style": { + "noCommonJs": "off" + }, + "suspicious": { + "useAwait": "off" + } + } } }, { diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index b3d0cba..0000000 --- a/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -/** @type {import('jest').Config} */ -// biome-ignore lint/style/noCommonJs: Jest requires CJS config -module.exports = { - testEnvironment: 'node', - roots: ['/src', '/test'], - forceExit: true, - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - tsconfig: 'tsconfig.json', - }, - ], - }, - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, -}; diff --git a/package.json b/package.json index 6eb84ad..f57a9ad 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "build:watch": "tsup --watch", "clean": "rm -rf dist", "lint": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", - "test": "jest --watchAll=false", - "test:watch": "jest --watch", + "lint:ci": "biome ci --no-errors-on-unmatched --files-ignore-unknown=true", + "test": "vitest run --coverage", + "test:watch": "vitest", "type-check": "tsc --noEmit", "changeset": "changeset" }, @@ -47,15 +48,14 @@ "@biomejs/biome": "2.3.11", "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.30.0", - "@types/jest": "29.5.0", "@types/node": "20.8.8", - "jest": "29.7.0", - "ts-jest": "29.2.0", + "@vitest/coverage-v8": "2.1.9", "tsup": "8.5.1", - "typescript": "5.4.5" + "typescript": "5.4.5", + "vitest": "2.1.9" }, "engines": { - "node": ">=22.0.0", + "node": ">=24.13.0", "pnpm": ">=10.0.0" }, "packageManager": "pnpm@10.33.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335f8c1..f6e20ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,64 +36,27 @@ importers: '@changesets/cli': specifier: ^2.30.0 version: 2.31.0(@types/node@20.8.8) - '@types/jest': - specifier: 29.5.0 - version: 29.5.0 '@types/node': specifier: 20.8.8 version: 20.8.8 - jest: - specifier: 29.7.0 - version: 29.7.0(@types/node@20.8.8) - ts-jest: - specifier: 29.2.0 - version: 29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5) + '@vitest/coverage-v8': + specifier: 2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@20.8.8)) tsup: specifier: 8.5.1 - version: 8.5.1(typescript@5.4.5) + version: 8.5.1(postcss@8.5.15)(typescript@5.4.5) typescript: specifier: 5.4.5 version: 5.4.5 + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@20.8.8) packages: - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} @@ -103,122 +66,15 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -344,102 +200,204 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} @@ -452,6 +410,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} @@ -464,6 +428,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} @@ -476,24 +446,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} @@ -514,86 +508,17 @@ packages: '@types/node': optional: true - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} '@istanbuljs/schema@0.1.6': resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -622,6 +547,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] @@ -760,59 +689,52 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} - - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@types/node@20.8.8': + resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@types/jest@29.5.0': - resolution: {integrity: sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@types/node@20.8.8': - resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -823,29 +745,25 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -856,38 +774,16 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} - engines: {node: '>=6.0.0'} - hasBin: true + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -896,28 +792,17 @@ packages: bignumber.js@11.0.0: resolution: {integrity: sha512-pkOSwKRi2HLwEbmTNf7C0M9lw3Z5dXQ1irbanJvySh/nJQBcFsMxF+g5o2JEn5BPFmfAqYalrhOwoFdLJXohEg==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -928,54 +813,21 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.3: - resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -987,9 +839,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -997,14 +846,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1024,30 +865,14 @@ packages: supports-color: optional: true - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1056,56 +881,43 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} - electron-to-chromium@1.5.355: - resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1114,15 +926,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1143,6 +949,10 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1151,40 +961,19 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -1206,10 +995,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1217,10 +1002,6 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1229,29 +1010,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1260,10 +1018,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1272,10 +1026,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1291,154 +1041,20 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -1447,9 +1063,6 @@ packages: js-sha3@0.9.3: resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1458,30 +1071,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1497,31 +1089,25 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1530,12 +1116,17 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1550,8 +1141,10 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -1562,31 +1155,10 @@ packages: encoding: optional: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1598,10 +1170,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -1614,35 +1182,38 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1662,10 +1233,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1687,31 +1254,21 @@ packages: yaml: optional: true + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -1720,27 +1277,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1756,10 +1296,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - semver@7.8.0: resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} @@ -1776,25 +1312,19 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} source-map@0.7.6: @@ -1807,38 +1337,32 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1848,21 +1372,13 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -1871,6 +1387,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1878,8 +1397,17 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -1895,30 +1423,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.2.0: - resolution: {integrity: sha512-eFmkE9MG0+oT6nqSOcUwL+2UUmK2IvhhUV8hFDsCHnc++v2WCCbQQZh5vvjsa8sgOY/g9T0325hmkEmi6rninA==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -1938,14 +1442,6 @@ packages: typescript: optional: true - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -1961,18 +1457,66 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -1985,224 +1529,38 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - zod@3.23.0: resolution: {integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==} snapshots: - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': + '@ampproject/remapping@2.3.0': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/runtime@7.29.2': {} - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': dependencies: @@ -2404,81 +1762,150 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true @@ -2493,188 +1920,22 @@ snapshots: optionalDependencies: '@types/node': 20.8.8 - '@istanbuljs/load-nyc-config@1.1.0': + '@isaacs/cliui@8.0.2': dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 '@istanbuljs/schema@0.1.6': {} - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.8.8) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.8.8 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 20.8.8 - chalk: 4.1.2 - collect-v8-coverage: 1.0.3 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.10 - - '@jest/source-map@29.6.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.3 - - '@jest/test-sequencer@29.7.0': - dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 - - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.29.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 20.8.8 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2712,6 +1973,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true @@ -2787,95 +2051,88 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true - '@sinclair/typebox@0.27.10': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 + '@types/estree@1.0.8': {} - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@types/node@12.20.55': {} - '@types/babel__generator@7.27.0': + '@types/node@20.8.8': dependencies: - '@babel/types': 7.29.0 + undici-types: 5.25.3 - '@types/babel__template@7.4.4': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.8.8))': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@20.8.8) + transitivePeerDependencies: + - supports-color - '@types/babel__traverse@7.28.0': + '@vitest/expect@2.1.9': dependencies: - '@babel/types': 7.29.0 - - '@types/estree@1.0.8': {} + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 - '@types/graceful-fs@4.1.9': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.8.8))': dependencies: - '@types/node': 20.8.8 - - '@types/istanbul-lib-coverage@2.0.6': {} + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.8.8) - '@types/istanbul-lib-report@3.0.3': + '@vitest/pretty-format@2.1.9': dependencies: - '@types/istanbul-lib-coverage': 2.0.6 + tinyrainbow: 1.2.0 - '@types/istanbul-reports@3.0.4': + '@vitest/runner@2.1.9': dependencies: - '@types/istanbul-lib-report': 3.0.3 + '@vitest/utils': 2.1.9 + pathe: 1.1.2 - '@types/jest@29.5.0': + '@vitest/snapshot@2.1.9': dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/node@12.20.55': {} + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 - '@types/node@20.8.8': + '@vitest/spy@2.1.9': dependencies: - undici-types: 5.25.3 + tinyspy: 3.0.2 - '@types/stack-utils@2.0.3': {} - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': + '@vitest/utils@2.1.9': dependencies: - '@types/yargs-parser': 21.0.3 + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 acorn@8.16.0: {} ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -2884,64 +2141,11 @@ snapshots: array-union@2.1.0: {} - babel-jest@29.7.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.28.6 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + assertion-error@2.0.1: {} balanced-match@1.0.2: {} - baseline-browser-mapping@2.10.29: {} + balanced-match@4.0.4: {} better-path-resolve@1.0.0: dependencies: @@ -2949,32 +2153,17 @@ snapshots: bignumber.js@11.0.0: {} - brace-expansion@1.1.14: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.2: + brace-expansion@5.0.6: dependencies: - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.355 - node-releases: 2.0.44 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + balanced-match: 4.0.4 - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: + braces@3.0.3: dependencies: - node-int64: 0.4.0 - - buffer-from@1.1.2: {} + fill-range: 7.1.1 bundle-require@5.1.0(esbuild@0.27.7): dependencies: @@ -2983,41 +2172,22 @@ snapshots: cac@6.7.14: {} - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001792: {} - - chalk@4.1.2: + chai@5.3.3: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 chardet@2.1.1: {} + check-error@2.1.3: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - co@4.6.0: {} - - collect-v8-coverage@1.0.3: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3026,30 +2196,11 @@ snapshots: commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} consola@3.4.2: {} - convert-source-map@2.0.0: {} - - create-jest@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - cross-spawn@7.0.6: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -3065,38 +2216,54 @@ snapshots: dependencies: ms: 2.1.3 - dedent@1.7.2: {} - - deepmerge@4.3.1: {} + deep-eql@5.0.2: {} detect-indent@6.1.0: {} - detect-newline@3.1.0: {} - - diff-sequences@29.6.3: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 dotenv@8.6.0: {} - electron-to-chromium@1.5.355: {} - - emittery@0.13.1: {} + eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 + es-module-lexer@1.7.0: {} - es-errors@1.3.0: {} + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 esbuild@0.27.7: optionalDependencies: @@ -3127,33 +2294,13 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} - - escape-string-regexp@2.0.0: {} - esprima@4.0.1: {} - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - - expect@29.7.0: + estree-walker@3.0.3: dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} extendable-error@0.1.7: {} @@ -3165,16 +2312,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -3194,6 +2335,11 @@ snapshots: mlly: 1.8.2 rollup: 4.60.2 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -3206,33 +2352,21 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-package-type@0.1.0: {} - - get-stream@6.0.1: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - glob@7.2.3: + glob@10.5.0: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 globby@11.1.0: dependencies: @@ -3254,56 +2388,26 @@ snapshots: has-flag@4.0.0: {} - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - html-escaper@2.0.2: {} human-id@4.1.3: {} - human-signals@2.1.0: {} - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 ignore@5.3.2: {} - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-arrayish@0.2.1: {} - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.3 - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} - is-generator-fn@2.1.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-number@7.0.0: {} - is-stream@2.0.1: {} - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -3314,37 +2418,17 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: + istanbul-lib-source-maps@5.0.6: dependencies: + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -3353,320 +2437,16 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jest-changed-files@29.7.0: + jackspeak@3.4.3: dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.2 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.8.8) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@20.8.8): - dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.8.8 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 20.8.8 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 + '@isaacs/cliui': 8.0.2 optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.12 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.3 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.2 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 20.8.8 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.8.8) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + '@pkgjs/parseargs': 0.11.0 joycon@3.1.1: {} js-sha3@0.9.3: {} - js-tokens@4.0.0: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -3676,20 +2456,10 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.1.0: {} - - json-parse-even-better-errors@2.3.1: {} - - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - kleur@3.0.3: {} - - leven@3.1.0: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -3700,29 +2470,25 @@ snapshots: dependencies: p-locate: 4.1.0 - lodash.memoize@4.1.2: {} - lodash.startcase@4.4.0: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 + loupe@3.2.1: {} + + lru-cache@10.4.3: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@4.0.0: + magicast@0.3.5: dependencies: - semver: 7.8.0 - - make-error@1.3.6: {} + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 - makeerror@1.0.12: + make-dir@4.0.0: dependencies: - tmpl: 1.0.5 - - merge-stream@2.0.0: {} + semver: 7.8.0 merge2@1.4.1: {} @@ -3731,11 +2497,15 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mimic-fn@2.1.0: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 - minimatch@3.1.5: + minimatch@9.0.9: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 2.1.0 + + minipass@7.1.3: {} mlly@1.8.2: dependencies: @@ -3754,32 +2524,14 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - natural-compare@1.4.0: {} + nanoid@3.3.12: {} node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - node-int64@0.4.0: {} - - node-releases@2.0.44: {} - - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - object-assign@4.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - outdent@0.5.0: {} p-filter@2.1.0: @@ -3790,10 +2542,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -3802,29 +2550,29 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} - path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3835,41 +2583,30 @@ snapshots: pirates@4.0.7: {} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1: + postcss-load-config@6.0.1(postcss@8.5.15): dependencies: lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.15 - prettier@2.8.8: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - prompts@2.4.2: + postcss@8.5.15: dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 - pure-rand@6.1.0: {} + prettier@2.8.8: {} quansync@0.2.11: {} queue-microtask@1.2.3: {} - react-is@18.3.1: {} - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -3879,23 +2616,8 @@ snapshots: readdirp@4.1.2: {} - require-directory@2.1.1: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - resolve-from@5.0.0: {} - resolve.exports@2.0.3: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} rollup@4.60.2: @@ -3935,8 +2657,6 @@ snapshots: safer-buffer@2.1.2: {} - semver@6.3.1: {} - semver@7.8.0: {} shallow-equal-object@1.1.1: {} @@ -3947,20 +2667,13 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@3.0.7: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} - sisteransi@1.0.5: {} - slash@3.0.0: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} + source-map-js@1.2.1: {} source-map@0.7.6: {} @@ -3971,14 +2684,9 @@ snapshots: sprintf-js@1.0.3: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + stackback@0.0.2: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + std-env@3.10.0: {} string-width@4.2.3: dependencies: @@ -3986,17 +2694,21 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-bom@3.0.0: {} - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} + strip-bom@3.0.0: {} sucrase@3.35.1: dependencies: @@ -4012,19 +2724,13 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - term-size@2.2.1: {} - test-exclude@6.0.0: + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 - glob: 7.2.3 - minimatch: 3.1.5 + glob: 10.5.0 + minimatch: 10.2.5 thenify-all@1.6.0: dependencies: @@ -4034,6 +2740,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: @@ -4041,7 +2749,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmpl@1.0.5: {} + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} to-regex-range@5.0.1: dependencies: @@ -4053,25 +2765,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.8.0 - typescript: 5.4.5 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - - tsup@8.5.1(typescript@5.4.5): + tsup@8.5.1(postcss@8.5.15)(typescript@5.4.5): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -4082,7 +2776,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(postcss@8.5.15) resolve-from: 5.0.0 rollup: 4.60.2 source-map: 0.7.6 @@ -4091,6 +2785,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: + postcss: 8.5.15 typescript: 5.4.5 transitivePeerDependencies: - jiti @@ -4098,10 +2793,6 @@ snapshots: - tsx - yaml - type-detect@4.0.8: {} - - type-fest@0.21.3: {} - typescript@5.4.5: {} ufo@1.6.3: {} @@ -4110,21 +2801,67 @@ snapshots: universalify@0.1.2: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + vite-node@2.1.9(@types/node@20.8.8): dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.8.8) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser - v8-to-istanbul@9.3.0: + vite@5.4.21(@types/node@20.8.8): dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.2 + optionalDependencies: + '@types/node': 20.8.8 + fsevents: 2.3.3 - walker@1.0.8: + vitest@2.1.9(@types/node@20.8.8): dependencies: - makeerror: 1.0.12 + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.8.8)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.8.8) + vite-node: 2.1.9(@types/node@20.8.8) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.8.8 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser webidl-conversions@3.0.1: {} @@ -4137,35 +2874,21 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: + wrap-ansi@8.1.0: dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 zod@3.23.0: {} diff --git a/src/domain/member-profile/MemberProfileAragonName.ts b/src/domain/member-profile/MemberProfileAragonName.ts index 5214d16..4ebb7d8 100644 --- a/src/domain/member-profile/MemberProfileAragonName.ts +++ b/src/domain/member-profile/MemberProfileAragonName.ts @@ -40,9 +40,7 @@ export class MemberProfileAragonName extends ValueObject { @@ -58,4 +59,18 @@ describe('Address', () => { expect(address1.equals(address2)).toBe(false); expect(address2.equals(address3)).toBe(true); }); + + it('exposes the underlying HexNumber via toHexNumber()', () => { + expect(address.toHexNumber()).toBe(addressValue); + }); + + it('constructs from a HexString', () => { + const hex = '0x0123456789abcdef0123456789abcdef01234567' as HexString; + const fromHex = Address.fromHexString(hex); + + // Internally normalizes to checksum casing; structural equality is + // by checksum hex. + expect(fromHex.equals(address)).toBe(true); + expect(fromHex.toHexNumber().toHexString()).toBe(hex); + }); }); diff --git a/src/domain/primitives/events/SomethingWentWrong.test.ts b/src/domain/primitives/events/SomethingWentWrong.test.ts new file mode 100644 index 0000000..862159d --- /dev/null +++ b/src/domain/primitives/events/SomethingWentWrong.test.ts @@ -0,0 +1,16 @@ +import { SomethingWentWrong } from './SomethingWentWrong'; + +describe('SomethingWentWrong', () => { + it('constructs an event carrying the originating Error', () => { + const cause = new Error('boom'); + const event = new SomethingWentWrong(cause); + + expect(event.error).toBe(cause); + expect(event.code).toBe('SomethingWentWrong'); + expect(event.occurredAt).toBeInstanceOf(Date); + }); + + it('exposes the static code on the class', () => { + expect(SomethingWentWrong.code).toBe('SomethingWentWrong'); + }); +}); diff --git a/src/domain/primitives/pagination/Page.test.ts b/src/domain/primitives/pagination/Page.test.ts new file mode 100644 index 0000000..5681505 --- /dev/null +++ b/src/domain/primitives/pagination/Page.test.ts @@ -0,0 +1,26 @@ +import { createPage } from './Page'; + +describe('createPage', () => { + it('builds a Page from items and totals', () => { + const items = ['a', 'b', 'c']; + const page = createPage(items, 1, 10, 23); + + expect(page).toEqual({ + items, + page: 1, + pageSize: 10, + totalPages: 3, + totalRecords: 23, + }); + }); + + it('rounds totalPages up for partial final pages', () => { + const page = createPage([], 1, 7, 15); + expect(page.totalPages).toBe(3); + }); + + it('produces 0 totalPages when there are no records', () => { + const page = createPage([], 1, 10, 0); + expect(page.totalPages).toBe(0); + }); +}); diff --git a/src/domain/primitives/units/Ether.test.ts b/src/domain/primitives/units/Ether.test.ts index 7a932ba..e44e889 100644 --- a/src/domain/primitives/units/Ether.test.ts +++ b/src/domain/primitives/units/Ether.test.ts @@ -28,4 +28,9 @@ describe('Ether', () => { const token = Ether.create(value); expect(token.toBigNumber()).toEqual(value); }); + + it('toEther() returns the same instance (identity conversion)', () => { + const token = Ether.create(new BigNumber(1.23)); + expect(token.toEther()).toBe(token); + }); }); diff --git a/src/domain/primitives/units/Gwei.test.ts b/src/domain/primitives/units/Gwei.test.ts index 331d316..bd50704 100644 --- a/src/domain/primitives/units/Gwei.test.ts +++ b/src/domain/primitives/units/Gwei.test.ts @@ -43,4 +43,14 @@ describe('Gwei', () => { const value2 = new BigNumber(123); expect(token1.times(value2).toBigNumber().toNumber()).toEqual(39_483); }); + + it('toGwei() returns the same instance (identity conversion)', () => { + const token = Gwei.create(new BigNumber(1_230_000_000)); + expect(token.toGwei()).toBe(token); + }); + + it('deserialize() parses a decimal string', () => { + const token = Gwei.deserialize('1230000000'); + expect(token.toBigNumber().toNumber()).toEqual(1_230_000_000); + }); }); diff --git a/src/domain/primitives/units/Wei.test.ts b/src/domain/primitives/units/Wei.test.ts index 1df52b8..b048034 100644 --- a/src/domain/primitives/units/Wei.test.ts +++ b/src/domain/primitives/units/Wei.test.ts @@ -26,4 +26,9 @@ describe('Wei', () => { const token = Wei.create(value); expect(token.toBigNumber()).toEqual(value); }); + + it('toWei() returns the same instance (identity conversion)', () => { + const token = Wei.create(new BigNumber('1230000000000000000')); + expect(token.toWei()).toBe(token); + }); }); diff --git a/src/infrastructure/stores/EnvioClient.test.ts b/src/infrastructure/stores/EnvioClient.test.ts new file mode 100644 index 0000000..cdfa555 --- /dev/null +++ b/src/infrastructure/stores/EnvioClient.test.ts @@ -0,0 +1,86 @@ +import { GraphQLClient } from 'graphql-request'; +import { EnvioClient } from './EnvioClient'; + +describe('EnvioClient', () => { + const ENDPOINT = 'https://indexer.example.invalid/v1/graphql'; + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('constructs without auth headers when no token is supplied', () => { + const ctorSpy = vi.spyOn(GraphQLClient.prototype, 'setHeader'); + + // Just constructing should not call setHeader; we look at the + // GraphQLClient internals instead. + const client = new EnvioClient(ENDPOINT); + // Cast through unknown to inspect the private GraphQLClient. + const inner = ( + client as unknown as { client: { requestConfig: { headers?: unknown } } } + ).client; + + expect(inner.requestConfig.headers).toEqual({}); + // No setHeader is invoked. + expect(ctorSpy).not.toHaveBeenCalled(); + }); + + it('sets a Bearer authorization header when an apiToken is supplied', () => { + const client = new EnvioClient(ENDPOINT, 'secret-token'); + const inner = ( + client as unknown as { + client: { requestConfig: { headers?: Record } }; + } + ).client; + + expect(inner.requestConfig.headers).toEqual({ + Authorization: 'Bearer secret-token', + }); + }); + + it('delegates query() to the underlying GraphQLClient and returns the result', async () => { + const payload = { Domain: [] }; + const requestSpy = vi + .spyOn(GraphQLClient.prototype, 'request') + .mockResolvedValue(payload); + + const client = new EnvioClient(ENDPOINT); + const result = await client.query('query { Domain { id } }', { + name: 'x.aragon.eth', + }); + + expect(result).toBe(payload); + expect(requestSpy).toHaveBeenCalledTimes(1); + expect(requestSpy).toHaveBeenCalledWith('query { Domain { id } }', { + name: 'x.aragon.eth', + }); + }); + + it('forwards undefined variables when none are supplied', async () => { + const requestSpy = vi + .spyOn(GraphQLClient.prototype, 'request') + .mockResolvedValue({}); + + const client = new EnvioClient(ENDPOINT); + await client.query('query { __typename }'); + + expect(requestSpy).toHaveBeenCalledWith('query { __typename }', undefined); + }); + + it('wraps network/GraphQL failures in a descriptive Error', async () => { + const cause = new Error('network down'); + vi.spyOn(GraphQLClient.prototype, 'request').mockRejectedValue(cause); + + const client = new EnvioClient(ENDPOINT); + + await expect(client.query('query { x }')).rejects.toThrow( + 'Envio GraphQL query failed', + ); + + // The original error is preserved on `.cause`. + try { + await client.query('query { x }'); + } catch (err) { + expect((err as Error).cause).toBe(cause); + } + }); +}); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts index 2d6e00a..ef899ae 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -4,7 +4,8 @@ import { EnvioMemberProfileStore } from './EnvioMemberProfileStore'; const CHAIN_ID = '1'; const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; -const NODE = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +const NODE = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const textId = (version: string, key: string) => `${CHAIN_ID}-${RESOLVER}-${NODE}-${version}-${key}`; @@ -23,7 +24,7 @@ function buildMockEnvioClient(responses: unknown[]): MockClient { const calls: QueryCall[] = []; const queue = [...responses]; const envio = { - query: jest.fn( + query: vi.fn( async (document: string, variables: Record) => { calls.push({ document, variables }); return queue.shift(); @@ -47,7 +48,11 @@ describe('EnvioMemberProfileStore', () => { version: '0', texts: [ { id: textId('0', 'avatar'), key: 'avatar', value: 'ipfs://x' }, - { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + { + id: textId('0', 'url'), + key: 'url', + value: 'https://aragon.org', + }, ], }, }, @@ -117,7 +122,11 @@ describe('EnvioMemberProfileStore', () => { version: '0', texts: [ { id: textId('0', 'avatar'), key: 'avatar', value: null }, - { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + { + id: textId('0', 'url'), + key: 'url', + value: 'https://aragon.org', + }, ], }, }, @@ -194,7 +203,7 @@ describe('EnvioMemberProfileStore', () => { it('wraps GraphQL failures with a profile-level error', async () => { const envio = { - query: jest.fn().mockRejectedValue(new Error('graphql exploded')), + query: vi.fn().mockRejectedValue(new Error('graphql exploded')), } as unknown as EnvioClient; const store = new EnvioMemberProfileStore(envio); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts new file mode 100644 index 0000000..5423dd8 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts @@ -0,0 +1,22 @@ +import { extractVersion, mapDTOToDomain } from './MemberProfileTextRecordMap'; + +describe('extractVersion', () => { + it('returns the 4th hyphen-separated segment of a well-formed id', () => { + expect(extractVersion('1-0xresolver-0xnode-7-com.github-handle')).toBe('7'); + }); + + it('returns an empty string when the id has fewer than 4 segments', () => { + // This is the safety fallback for malformed indexer ids — the + // production code path never produces these, but we still treat + // them as "non-live" rather than crashing. + expect(extractVersion('')).toBe(''); + expect(extractVersion('a-b-c')).toBe(''); + }); +}); + +describe('mapDTOToDomain', () => { + it('rejects responses whose shape does not match the schema', () => { + expect(() => mapDTOToDomain({ Domain: 'nope' })).toThrow(); + expect(() => mapDTOToDomain(null)).toThrow(); + }); +}); diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts index 11a20e7..78e3489 100644 --- a/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts @@ -12,7 +12,7 @@ describe('GetMemberProfileTextRecordsUseCase', () => { const buildMockStore = ( records: MemberProfileTextRecord[] = [], ): MemberProfileStore => ({ - findTextRecordsBySubdomain: jest.fn().mockResolvedValue(records), + findTextRecordsBySubdomain: vi.fn().mockResolvedValue(records), }); it('returns the text records from the store', async () => { @@ -49,7 +49,7 @@ describe('GetMemberProfileTextRecordsUseCase', () => { it('wraps store errors with a use-case message', async () => { const failingStore: MemberProfileStore = { - findTextRecordsBySubdomain: jest + findTextRecordsBySubdomain: vi .fn() .mockRejectedValue(new Error('graphql exploded')), }; diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts index 7aabdbf..62f0915 100644 --- a/src/use-cases/GetMemberProfileTextRecordsUseCase.ts +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts @@ -14,10 +14,7 @@ export interface GetMemberProfileTextRecordsUseCaseProps { */ export class GetMemberProfileTextRecordsUseCase implements - UseCase< - GetMemberProfileTextRecordsUseCaseProps, - MemberProfileTextRecord[] - > + UseCase { public readonly code = 'GetMemberProfileTextRecordsUseCase'; @@ -27,7 +24,9 @@ export class GetMemberProfileTextRecordsUseCase props: GetMemberProfileTextRecordsUseCaseProps, ): Promise { try { - return await this.profileStore.findTextRecordsBySubdomain(props.subdomain); + return await this.profileStore.findTextRecordsBySubdomain( + props.subdomain, + ); } catch (cause) { throw new Error('Error while getting member profile text records', { cause, diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 579f822..7d593b0 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -14,7 +14,7 @@ const NODE = function buildController(responses: unknown[]): AragonSubdomain { const envio = new EnvioClient('https://unused.example.invalid'); const queue = [...responses]; - jest.spyOn(envio, 'query').mockImplementation(async () => { + vi.spyOn(envio, 'query').mockImplementation(async () => { if (queue.length === 0) { throw new Error('EnvioClient.query called more times than expected'); } @@ -83,9 +83,7 @@ describe('AragonSubdomain', () => { it('returns [] when the subdomain has no resolver yet', async () => { const controller = buildController([ { - Domain: [ - { id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }, - ], + Domain: [{ id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }], }, ]); diff --git a/tsconfig.json b/tsconfig.json index ddbfbe7..f1be642 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "resolveJsonModule": true, "sourceMap": true, "noEmit": true, - "types": ["jest", "node"], + "types": ["node", "vitest/globals"], "baseUrl": ".", "paths": { "@/*": ["src/*"] diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a78ef13 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,35 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'text-summary', 'html', 'lcov'], + include: ['src/**/*.ts'], + exclude: [ + 'src/**/*.test.ts', + // Pure type-only modules — no executable JS is emitted, so + // including them in coverage just produces phantom uncovered + // lines. + 'src/**/index.ts', + 'src/domain/member-profile/MemberProfileStore.ts', + 'src/domain/primitives/units/EVMUnit.ts', + ], + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + }, +}); From f48879bebafa2bdf499957d846d51b784d3d8654 Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 20:57:41 +0100 Subject: [PATCH 06/33] Changeset --- .changeset/crisp-moose-glow.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/crisp-moose-glow.md diff --git a/.changeset/crisp-moose-glow.md b/.changeset/crisp-moose-glow.md new file mode 100644 index 0000000..86fe07e --- /dev/null +++ b/.changeset/crisp-moose-glow.md @@ -0,0 +1,5 @@ +--- +"@aragon/aragon-subdomain": minor +--- + +Look up member profile ENS records From 2ac58e343154e007807df0c0fb600d557c2e6627 Mon Sep 17 00:00:00 2001 From: Stephen Horvath <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 21:49:02 +0100 Subject: [PATCH 07/33] Upgrade to vitest --- .changeset/config.json | 30 +- .github/actions/setup/action.yml | 2 +- .github/workflows/library-test.yml | 4 +- .github/workflows/scripts/readChangelog.js | 56 +- .gitignore | 1 + .npmrc | 2 +- .nvmrc | 2 +- biome.json | 26 +- jest.config.js | 18 - package.json | 14 +- pnpm-lock.yaml | 2687 +++++------------ .../member-profile/MemberProfileAragonName.ts | 4 +- .../member-profile/MemberProfileTextRecord.ts | 4 +- .../primitives/blockchain/Address.test.ts | 15 + .../events/SomethingWentWrong.test.ts | 16 + src/domain/primitives/pagination/Page.test.ts | 26 + src/domain/primitives/units/Ether.test.ts | 5 + src/domain/primitives/units/Gwei.test.ts | 10 + src/domain/primitives/units/Wei.test.ts | 5 + src/infrastructure/stores/EnvioClient.test.ts | 86 + .../EnvioMemberProfileStore.test.ts | 19 +- .../maps/MemberProfileTextRecordMap.test.ts | 22 + ...GetMemberProfileTextRecordsUseCase.test.ts | 4 +- .../GetMemberProfileTextRecordsUseCase.ts | 9 +- test/AragonSubdomain.test.ts | 6 +- tsconfig.json | 2 +- vitest.config.ts | 35 + 27 files changed, 1032 insertions(+), 2078 deletions(-) delete mode 100644 jest.config.js create mode 100644 src/domain/primitives/events/SomethingWentWrong.test.ts create mode 100644 src/domain/primitives/pagination/Page.test.ts create mode 100644 src/infrastructure/stores/EnvioClient.test.ts create mode 100644 src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts create mode 100644 vitest.config.ts diff --git a/.changeset/config.json b/.changeset/config.json index 8963b0d..19b5f72 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,15 +1,19 @@ { - "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", - "access": "public", - "baseBranch": "main", - "changedFilePatterns": ["src/**"], - "changelog": [ - "@changesets/changelog-github", - { "repo": "aragon/aragon-subdomain" } - ], - "commit": false, - "fixed": [], - "ignore": [], - "linked": [], - "updateInternalDependencies": "patch" + "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", + "access": "public", + "baseBranch": "main", + "changedFilePatterns": [ + "src/**" + ], + "changelog": [ + "@changesets/changelog-github", + { + "repo": "aragon/aragon-subdomain" + } + ], + "commit": false, + "fixed": [], + "ignore": [], + "linked": [], + "updateInternalDependencies": "patch" } diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index d85b7d3..bd1d4d6 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -20,7 +20,7 @@ inputs: node-version: description: "The version of Node.js to use for the workflow" required: false - default: "22" + default: "24" node-cache: description: "Specifies which package manager to use for caching" required: false diff --git a/.github/workflows/library-test.yml b/.github/workflows/library-test.yml index 79a4fe5..813d14a 100644 --- a/.github/workflows/library-test.yml +++ b/.github/workflows/library-test.yml @@ -29,7 +29,9 @@ jobs: fetch-depth: 0 - name: Build library run: pnpm build - - name: Run tests + - name: Lint + run: pnpm lint:ci + - name: Run tests with coverage (fails below 100%) run: pnpm test - name: Check types run: pnpm type-check diff --git a/.github/workflows/scripts/readChangelog.js b/.github/workflows/scripts/readChangelog.js index b85172a..ec62167 100644 --- a/.github/workflows/scripts/readChangelog.js +++ b/.github/workflows/scripts/readChangelog.js @@ -1,40 +1,40 @@ const fs = require('node:fs'); module.exports = async ({ core }) => { - const { version, path } = process.env; + const { version, path } = process.env; - // Check if version and path are provided - if (!version || !path) { - core.setFailed('Missing required environment variables: version or path'); - return; - } + // Check if version and path are provided + if (!version || !path) { + core.setFailed('Missing required environment variables: version or path'); + return; + } - // Check if the changelog file exists - if (!fs.existsSync(path)) { - core.setFailed(`Changelog file not found at path: ${path}`); - return; - } + // Check if the changelog file exists + if (!fs.existsSync(path)) { + core.setFailed(`Changelog file not found at path: ${path}`); + return; + } - try { - core.info(`Reading changes in version ${version}.`); + try { + core.info(`Reading changes in version ${version}.`); - const changelog = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' }); + const changelog = fs.readFileSync(path, { encoding: 'utf8', flag: 'r' }); - const versionChanges = changelog - .split(/(?=## \d+\.\d+\.\d+)/g) - .find((changes) => changes.startsWith(`## ${version}`)); + const versionChanges = changelog + .split(/(?=## \d+\.\d+\.\d+)/g) + .find((changes) => changes.startsWith(`## ${version}`)); - if (!versionChanges) { - core.warning(`No changes found for version ${version} in the changelog.`); - core.setOutput('changes', 'No changes.'); - return; - } + if (!versionChanges) { + core.warning(`No changes found for version ${version} in the changelog.`); + core.setOutput('changes', 'No changes.'); + return; + } - const parsedChanges = versionChanges.replace(`## ${version}`, '').trim(); + const parsedChanges = versionChanges.replace(`## ${version}`, '').trim(); - core.info(`Setting output: ${parsedChanges}.`); - core.setOutput('changes', parsedChanges); - } catch (error) { - core.setFailed(error); - } + core.info(`Setting output: ${parsedChanges}.`); + core.setOutput('changes', parsedChanges); + } catch (error) { + core.setFailed(error); + } }; diff --git a/.gitignore b/.gitignore index a55fb2d..4df1645 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .DS_Store .env *.tsbuildinfo +coverage/ diff --git a/.npmrc b/.npmrc index 60e78c0..cad39a2 100644 --- a/.npmrc +++ b/.npmrc @@ -13,5 +13,5 @@ ignore-scripts=true auto-install-types=true # Node version management by pnpm -node-version=22.20.0 +node-version=24.13.0 engine-strict=true diff --git a/.nvmrc b/.nvmrc index 442c758..3fe3b15 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.20.0 +24.13.0 diff --git a/biome.json b/biome.json index 836c691..3afc277 100644 --- a/biome.json +++ b/biome.json @@ -27,7 +27,7 @@ "trailingCommas": "all" }, "globals": [ - "jest", + "vi", "describe", "it", "test", @@ -130,10 +130,30 @@ }, { "includes": [ - "**/*.test.ts" + "**/*.test.ts", + "test/**/*.ts" ], "linter": { - "rules": {} + "rules": { + "suspicious": { + "useAwait": "off" + } + } + } + }, + { + "includes": [ + ".github/workflows/scripts/**" + ], + "linter": { + "rules": { + "style": { + "noCommonJs": "off" + }, + "suspicious": { + "useAwait": "off" + } + } } }, { diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index b3d0cba..0000000 --- a/jest.config.js +++ /dev/null @@ -1,18 +0,0 @@ -/** @type {import('jest').Config} */ -// biome-ignore lint/style/noCommonJs: Jest requires CJS config -module.exports = { - testEnvironment: 'node', - roots: ['/src', '/test'], - forceExit: true, - transform: { - '^.+\\.tsx?$': [ - 'ts-jest', - { - tsconfig: 'tsconfig.json', - }, - ], - }, - moduleNameMapper: { - '^@/(.*)$': '/src/$1', - }, -}; diff --git a/package.json b/package.json index 6eb84ad..f57a9ad 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "build:watch": "tsup --watch", "clean": "rm -rf dist", "lint": "biome check --write --no-errors-on-unmatched --files-ignore-unknown=true", - "test": "jest --watchAll=false", - "test:watch": "jest --watch", + "lint:ci": "biome ci --no-errors-on-unmatched --files-ignore-unknown=true", + "test": "vitest run --coverage", + "test:watch": "vitest", "type-check": "tsc --noEmit", "changeset": "changeset" }, @@ -47,15 +48,14 @@ "@biomejs/biome": "2.3.11", "@changesets/changelog-github": "^0.6.0", "@changesets/cli": "^2.30.0", - "@types/jest": "29.5.0", "@types/node": "20.8.8", - "jest": "29.7.0", - "ts-jest": "29.2.0", + "@vitest/coverage-v8": "2.1.9", "tsup": "8.5.1", - "typescript": "5.4.5" + "typescript": "5.4.5", + "vitest": "2.1.9" }, "engines": { - "node": ">=22.0.0", + "node": ">=24.13.0", "pnpm": ">=10.0.0" }, "packageManager": "pnpm@10.33.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335f8c1..f6e20ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,64 +36,27 @@ importers: '@changesets/cli': specifier: ^2.30.0 version: 2.31.0(@types/node@20.8.8) - '@types/jest': - specifier: 29.5.0 - version: 29.5.0 '@types/node': specifier: 20.8.8 version: 20.8.8 - jest: - specifier: 29.7.0 - version: 29.7.0(@types/node@20.8.8) - ts-jest: - specifier: 29.2.0 - version: 29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5) + '@vitest/coverage-v8': + specifier: 2.1.9 + version: 2.1.9(vitest@2.1.9(@types/node@20.8.8)) tsup: specifier: 8.5.1 - version: 8.5.1(typescript@5.4.5) + version: 8.5.1(postcss@8.5.15)(typescript@5.4.5) typescript: specifier: 5.4.5 version: 5.4.5 + vitest: + specifier: 2.1.9 + version: 2.1.9(@types/node@20.8.8) packages: - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} @@ -103,122 +66,15 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - '@babel/parser@7.29.3': resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/runtime@7.29.2': resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -344,102 +200,204 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.27.7': resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.27.7': resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.27.7': resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.27.7': resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.27.7': resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.27.7': resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.27.7': resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.27.7': resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.27.7': resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.27.7': resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.27.7': resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.27.7': resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.27.7': resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.27.7': resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.27.7': resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.27.7': resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} @@ -452,6 +410,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.27.7': resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} @@ -464,6 +428,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.27.7': resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} @@ -476,24 +446,48 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.27.7': resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.27.7': resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.27.7': resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.27.7': resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} @@ -514,86 +508,17 @@ packages: '@types/node': optional: true - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} '@istanbuljs/schema@0.1.6': resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} engines: {node: '>=8'} - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -622,6 +547,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] @@ -760,59 +689,52 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} - - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + '@types/node@20.8.8': + resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} - '@types/jest@29.5.0': - resolution: {integrity: sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==} + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} - '@types/node@20.8.8': - resolution: {integrity: sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -823,29 +745,25 @@ packages: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -856,38 +774,16 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} - engines: {node: '>=6.0.0'} - hasBin: true + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -896,28 +792,17 @@ packages: bignumber.js@11.0.0: resolution: {integrity: sha512-pkOSwKRi2HLwEbmTNf7C0M9lw3Z5dXQ1irbanJvySh/nJQBcFsMxF+g5o2JEn5BPFmfAqYalrhOwoFdLJXohEg==} - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -928,54 +813,21 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.3: - resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -987,9 +839,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -997,14 +846,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1024,30 +865,14 @@ packages: supports-color: optional: true - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1056,56 +881,43 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} - electron-to-chromium@1.5.355: - resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} extendable-error@0.1.7: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} @@ -1114,15 +926,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1143,6 +949,10 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fs-extra@7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} engines: {node: '>=6 <7 || >=8'} @@ -1151,40 +961,19 @@ packages: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -1206,10 +995,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -1217,10 +1002,6 @@ packages: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} hasBin: true - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1229,29 +1010,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-core-module@2.16.2: - resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} - engines: {node: '>= 0.4'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1260,10 +1018,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1272,10 +1026,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-subdir@1.2.0: resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} engines: {node: '>=4'} @@ -1291,154 +1041,20 @@ packages: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} engines: {node: '>=10'} istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -1447,9 +1063,6 @@ packages: js-sha3@0.9.3: resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -1458,30 +1071,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1497,31 +1089,25 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1530,12 +1116,17 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -1550,8 +1141,10 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} @@ -1562,31 +1155,10 @@ packages: encoding: optional: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - - node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1598,10 +1170,6 @@ packages: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -1614,35 +1182,38 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1662,10 +1233,6 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1687,31 +1254,21 @@ packages: yaml: optional: true + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} hasBin: true - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - read-yaml-file@1.1.0: resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} engines: {node: '>=6'} @@ -1720,27 +1277,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - - resolve@1.22.12: - resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} - engines: {node: '>= 0.4'} - hasBin: true - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -1756,10 +1296,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - semver@7.8.0: resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} @@ -1776,25 +1312,19 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} source-map@0.7.6: @@ -1807,38 +1337,32 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} @@ -1848,21 +1372,13 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -1871,6 +1387,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -1878,8 +1397,17 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -1895,30 +1423,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-jest@29.2.0: - resolution: {integrity: sha512-eFmkE9MG0+oT6nqSOcUwL+2UUmK2IvhhUV8hFDsCHnc++v2WCCbQQZh5vvjsa8sgOY/g9T0325hmkEmi6rninA==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - tsup@8.5.1: resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} engines: {node: '>=18'} @@ -1938,14 +1442,6 @@ packages: typescript: optional: true - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - typescript@5.4.5: resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==} engines: {node: '>=14.17'} @@ -1961,18 +1457,66 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -1985,224 +1529,38 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - zod@3.23.0: resolution: {integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==} snapshots: - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.3': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': + '@ampproject/remapping@2.3.0': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.2 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-plugin-utils@7.28.6': {} '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.29.2': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@babel/parser@7.29.3': dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/runtime@7.29.2': {} - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color + '@babel/runtime@7.29.2': {} '@babel/types@7.29.0': dependencies: @@ -2404,81 +1762,150 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 + '@esbuild/aix-ppc64@0.21.5': + optional: true + '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/android-arm64@0.21.5': + optional: true + '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm@0.21.5': + optional: true + '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-x64@0.21.5': + optional: true + '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.21.5': + optional: true + '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-x64@0.21.5': + optional: true + '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.21.5': + optional: true + '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.21.5': + optional: true + '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/linux-arm64@0.21.5': + optional: true + '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm@0.21.5': + optional: true + '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-ia32@0.21.5': + optional: true + '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-loong64@0.21.5': + optional: true + '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-mips64el@0.21.5': + optional: true + '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-ppc64@0.21.5': + optional: true + '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.21.5': + optional: true + '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-s390x@0.21.5': + optional: true + '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-x64@0.21.5': + optional: true + '@esbuild/linux-x64@0.27.7': optional: true '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.21.5': + optional: true + '@esbuild/netbsd-x64@0.27.7': optional: true '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.21.5': + optional: true + '@esbuild/openbsd-x64@0.27.7': optional: true '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/sunos-x64@0.21.5': + optional: true + '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/win32-arm64@0.21.5': + optional: true + '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-ia32@0.21.5': + optional: true + '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-x64@0.21.5': + optional: true + '@esbuild/win32-x64@0.27.7': optional: true @@ -2493,188 +1920,22 @@ snapshots: optionalDependencies: '@types/node': 20.8.8 - '@istanbuljs/load-nyc-config@1.1.0': + '@isaacs/cliui@8.0.2': dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 '@istanbuljs/schema@0.1.6': {} - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.8.8) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 20.8.8 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 20.8.8 - chalk: 4.1.2 - collect-v8-coverage: 1.0.3 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.10 - - '@jest/source-map@29.6.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.3 - - '@jest/test-sequencer@29.7.0': - dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 - - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.29.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 20.8.8 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2712,6 +1973,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@pkgjs/parseargs@0.11.0': + optional: true + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true @@ -2787,95 +2051,88 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true - '@sinclair/typebox@0.27.10': {} - - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 + '@types/estree@1.0.8': {} - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 + '@types/node@12.20.55': {} - '@types/babel__generator@7.27.0': + '@types/node@20.8.8': dependencies: - '@babel/types': 7.29.0 + undici-types: 5.25.3 - '@types/babel__template@7.4.4': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.8.8))': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@20.8.8) + transitivePeerDependencies: + - supports-color - '@types/babel__traverse@7.28.0': + '@vitest/expect@2.1.9': dependencies: - '@babel/types': 7.29.0 - - '@types/estree@1.0.8': {} + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 - '@types/graceful-fs@4.1.9': + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@20.8.8))': dependencies: - '@types/node': 20.8.8 - - '@types/istanbul-lib-coverage@2.0.6': {} + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@20.8.8) - '@types/istanbul-lib-report@3.0.3': + '@vitest/pretty-format@2.1.9': dependencies: - '@types/istanbul-lib-coverage': 2.0.6 + tinyrainbow: 1.2.0 - '@types/istanbul-reports@3.0.4': + '@vitest/runner@2.1.9': dependencies: - '@types/istanbul-lib-report': 3.0.3 + '@vitest/utils': 2.1.9 + pathe: 1.1.2 - '@types/jest@29.5.0': + '@vitest/snapshot@2.1.9': dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/node@12.20.55': {} + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 - '@types/node@20.8.8': + '@vitest/spy@2.1.9': dependencies: - undici-types: 5.25.3 + tinyspy: 3.0.2 - '@types/stack-utils@2.0.3': {} - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': + '@vitest/utils@2.1.9': dependencies: - '@types/yargs-parser': 21.0.3 + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 acorn@8.16.0: {} ansi-colors@4.1.3: {} - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} any-promise@1.3.0: {} - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -2884,64 +2141,11 @@ snapshots: array-union@2.1.0: {} - babel-jest@29.7.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.28.6 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + assertion-error@2.0.1: {} balanced-match@1.0.2: {} - baseline-browser-mapping@2.10.29: {} + balanced-match@4.0.4: {} better-path-resolve@1.0.0: dependencies: @@ -2949,32 +2153,17 @@ snapshots: bignumber.js@11.0.0: {} - brace-expansion@1.1.14: + brace-expansion@2.1.0: dependencies: balanced-match: 1.0.2 - concat-map: 0.0.1 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.2: + brace-expansion@5.0.6: dependencies: - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.355 - node-releases: 2.0.44 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + balanced-match: 4.0.4 - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: + braces@3.0.3: dependencies: - node-int64: 0.4.0 - - buffer-from@1.1.2: {} + fill-range: 7.1.1 bundle-require@5.1.0(esbuild@0.27.7): dependencies: @@ -2983,41 +2172,22 @@ snapshots: cac@6.7.14: {} - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001792: {} - - chalk@4.1.2: + chai@5.3.3: dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 chardet@2.1.1: {} + check-error@2.1.3: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 - ci-info@3.9.0: {} - - cjs-module-lexer@1.4.3: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - co@4.6.0: {} - - collect-v8-coverage@1.0.3: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3026,30 +2196,11 @@ snapshots: commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} consola@3.4.2: {} - convert-source-map@2.0.0: {} - - create-jest@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - cross-spawn@7.0.6: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -3065,38 +2216,54 @@ snapshots: dependencies: ms: 2.1.3 - dedent@1.7.2: {} - - deepmerge@4.3.1: {} + deep-eql@5.0.2: {} detect-indent@6.1.0: {} - detect-newline@3.1.0: {} - - diff-sequences@29.6.3: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 dotenv@8.6.0: {} - electron-to-chromium@1.5.355: {} - - emittery@0.13.1: {} + eastasianwidth@0.2.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 + es-module-lexer@1.7.0: {} - es-errors@1.3.0: {} + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 esbuild@0.27.7: optionalDependencies: @@ -3127,33 +2294,13 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 - escalade@3.2.0: {} - - escape-string-regexp@2.0.0: {} - esprima@4.0.1: {} - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - - expect@29.7.0: + estree-walker@3.0.3: dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} extendable-error@0.1.7: {} @@ -3165,16 +2312,10 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - fastq@1.20.1: dependencies: reusify: 1.1.0 - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -3194,6 +2335,11 @@ snapshots: mlly: 1.8.2 rollup: 4.60.2 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 @@ -3206,33 +2352,21 @@ snapshots: jsonfile: 4.0.0 universalify: 0.1.2 - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-package-type@0.1.0: {} - - get-stream@6.0.1: {} - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - glob@7.2.3: + glob@10.5.0: dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 globby@11.1.0: dependencies: @@ -3254,56 +2388,26 @@ snapshots: has-flag@4.0.0: {} - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - html-escaper@2.0.2: {} human-id@4.1.3: {} - human-signals@2.1.0: {} - iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 ignore@5.3.2: {} - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - - inherits@2.0.4: {} - - is-arrayish@0.2.1: {} - - is-core-module@2.16.2: - dependencies: - hasown: 2.0.3 - is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} - is-generator-fn@2.1.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 is-number@7.0.0: {} - is-stream@2.0.1: {} - is-subdir@1.2.0: dependencies: better-path-resolve: 1.0.0 @@ -3314,37 +2418,17 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: + istanbul-lib-source-maps@5.0.6: dependencies: + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 transitivePeerDependencies: - supports-color @@ -3353,320 +2437,16 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - jest-changed-files@29.7.0: + jackspeak@3.4.3: dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.2 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/core': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.8.8) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@20.8.8): - dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.8.8 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 20.8.8 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 + '@isaacs/cliui': 8.0.2 optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.12 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.3 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.2 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.8.8 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 20.8.8 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@20.8.8): - dependencies: - '@jest/core': 29.7.0 - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.8.8) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + '@pkgjs/parseargs': 0.11.0 joycon@3.1.1: {} js-sha3@0.9.3: {} - js-tokens@4.0.0: {} - js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -3676,20 +2456,10 @@ snapshots: dependencies: argparse: 2.0.1 - jsesc@3.1.0: {} - - json-parse-even-better-errors@2.3.1: {} - - json5@2.2.3: {} - jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - kleur@3.0.3: {} - - leven@3.1.0: {} - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -3700,29 +2470,25 @@ snapshots: dependencies: p-locate: 4.1.0 - lodash.memoize@4.1.2: {} - lodash.startcase@4.4.0: {} - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 + loupe@3.2.1: {} + + lru-cache@10.4.3: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - make-dir@4.0.0: + magicast@0.3.5: dependencies: - semver: 7.8.0 - - make-error@1.3.6: {} + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 - makeerror@1.0.12: + make-dir@4.0.0: dependencies: - tmpl: 1.0.5 - - merge-stream@2.0.0: {} + semver: 7.8.0 merge2@1.4.1: {} @@ -3731,11 +2497,15 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mimic-fn@2.1.0: {} + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 - minimatch@3.1.5: + minimatch@9.0.9: dependencies: - brace-expansion: 1.1.14 + brace-expansion: 2.1.0 + + minipass@7.1.3: {} mlly@1.8.2: dependencies: @@ -3754,32 +2524,14 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - natural-compare@1.4.0: {} + nanoid@3.3.12: {} node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - node-int64@0.4.0: {} - - node-releases@2.0.44: {} - - normalize-path@3.0.0: {} - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - object-assign@4.1.1: {} - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - outdent@0.5.0: {} p-filter@2.1.0: @@ -3790,10 +2542,6 @@ snapshots: dependencies: p-try: 2.2.0 - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - p-locate@4.1.0: dependencies: p-limit: 2.3.0 @@ -3802,29 +2550,29 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - path-exists@4.0.0: {} - path-is-absolute@1.0.1: {} - path-key@3.1.1: {} - path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} @@ -3835,41 +2583,30 @@ snapshots: pirates@4.0.7: {} - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 mlly: 1.8.2 pathe: 2.0.3 - postcss-load-config@6.0.1: + postcss-load-config@6.0.1(postcss@8.5.15): dependencies: lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.15 - prettier@2.8.8: {} - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - prompts@2.4.2: + postcss@8.5.15: dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 - pure-rand@6.1.0: {} + prettier@2.8.8: {} quansync@0.2.11: {} queue-microtask@1.2.3: {} - react-is@18.3.1: {} - read-yaml-file@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -3879,23 +2616,8 @@ snapshots: readdirp@4.1.2: {} - require-directory@2.1.1: {} - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - resolve-from@5.0.0: {} - resolve.exports@2.0.3: {} - - resolve@1.22.12: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} rollup@4.60.2: @@ -3935,8 +2657,6 @@ snapshots: safer-buffer@2.1.2: {} - semver@6.3.1: {} - semver@7.8.0: {} shallow-equal-object@1.1.1: {} @@ -3947,20 +2667,13 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@3.0.7: {} + siginfo@2.0.0: {} signal-exit@4.1.0: {} - sisteransi@1.0.5: {} - slash@3.0.0: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} + source-map-js@1.2.1: {} source-map@0.7.6: {} @@ -3971,14 +2684,9 @@ snapshots: sprintf-js@1.0.3: {} - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 + stackback@0.0.2: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 + std-env@3.10.0: {} string-width@4.2.3: dependencies: @@ -3986,17 +2694,21 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-bom@3.0.0: {} - - strip-bom@4.0.0: {} - - strip-final-newline@2.0.0: {} + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 - strip-json-comments@3.1.1: {} + strip-bom@3.0.0: {} sucrase@3.35.1: dependencies: @@ -4012,19 +2724,13 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - term-size@2.2.1: {} - test-exclude@6.0.0: + test-exclude@7.0.2: dependencies: '@istanbuljs/schema': 0.1.6 - glob: 7.2.3 - minimatch: 3.1.5 + glob: 10.5.0 + minimatch: 10.2.5 thenify-all@1.6.0: dependencies: @@ -4034,6 +2740,8 @@ snapshots: dependencies: any-promise: 1.3.0 + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.16: @@ -4041,7 +2749,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - tmpl@1.0.5: {} + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} to-regex-range@5.0.1: dependencies: @@ -4053,25 +2765,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.0(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest@29.7.0(@types/node@20.8.8))(typescript@5.4.5): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.8.8) - jest-util: 29.7.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.8.0 - typescript: 5.4.5 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - - tsup@8.5.1(typescript@5.4.5): + tsup@8.5.1(postcss@8.5.15)(typescript@5.4.5): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 @@ -4082,7 +2776,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1 + postcss-load-config: 6.0.1(postcss@8.5.15) resolve-from: 5.0.0 rollup: 4.60.2 source-map: 0.7.6 @@ -4091,6 +2785,7 @@ snapshots: tinyglobby: 0.2.16 tree-kill: 1.2.2 optionalDependencies: + postcss: 8.5.15 typescript: 5.4.5 transitivePeerDependencies: - jiti @@ -4098,10 +2793,6 @@ snapshots: - tsx - yaml - type-detect@4.0.8: {} - - type-fest@0.21.3: {} - typescript@5.4.5: {} ufo@1.6.3: {} @@ -4110,21 +2801,67 @@ snapshots: universalify@0.1.2: {} - update-browserslist-db@1.2.3(browserslist@4.28.2): + vite-node@2.1.9(@types/node@20.8.8): dependencies: - browserslist: 4.28.2 - escalade: 3.2.0 - picocolors: 1.1.1 + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@20.8.8) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser - v8-to-istanbul@9.3.0: + vite@5.4.21(@types/node@20.8.8): dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.2 + optionalDependencies: + '@types/node': 20.8.8 + fsevents: 2.3.3 - walker@1.0.8: + vitest@2.1.9(@types/node@20.8.8): dependencies: - makeerror: 1.0.12 + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@20.8.8)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@20.8.8) + vite-node: 2.1.9(@types/node@20.8.8) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.8.8 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser webidl-conversions@3.0.1: {} @@ -4137,35 +2874,21 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - wrappy@1.0.2: {} - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: + wrap-ansi@8.1.0: dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - yocto-queue@0.1.0: {} + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 zod@3.23.0: {} diff --git a/src/domain/member-profile/MemberProfileAragonName.ts b/src/domain/member-profile/MemberProfileAragonName.ts index 5214d16..4ebb7d8 100644 --- a/src/domain/member-profile/MemberProfileAragonName.ts +++ b/src/domain/member-profile/MemberProfileAragonName.ts @@ -40,9 +40,7 @@ export class MemberProfileAragonName extends ValueObject { @@ -58,4 +59,18 @@ describe('Address', () => { expect(address1.equals(address2)).toBe(false); expect(address2.equals(address3)).toBe(true); }); + + it('exposes the underlying HexNumber via toHexNumber()', () => { + expect(address.toHexNumber()).toBe(addressValue); + }); + + it('constructs from a HexString', () => { + const hex = '0x0123456789abcdef0123456789abcdef01234567' as HexString; + const fromHex = Address.fromHexString(hex); + + // Internally normalizes to checksum casing; structural equality is + // by checksum hex. + expect(fromHex.equals(address)).toBe(true); + expect(fromHex.toHexNumber().toHexString()).toBe(hex); + }); }); diff --git a/src/domain/primitives/events/SomethingWentWrong.test.ts b/src/domain/primitives/events/SomethingWentWrong.test.ts new file mode 100644 index 0000000..862159d --- /dev/null +++ b/src/domain/primitives/events/SomethingWentWrong.test.ts @@ -0,0 +1,16 @@ +import { SomethingWentWrong } from './SomethingWentWrong'; + +describe('SomethingWentWrong', () => { + it('constructs an event carrying the originating Error', () => { + const cause = new Error('boom'); + const event = new SomethingWentWrong(cause); + + expect(event.error).toBe(cause); + expect(event.code).toBe('SomethingWentWrong'); + expect(event.occurredAt).toBeInstanceOf(Date); + }); + + it('exposes the static code on the class', () => { + expect(SomethingWentWrong.code).toBe('SomethingWentWrong'); + }); +}); diff --git a/src/domain/primitives/pagination/Page.test.ts b/src/domain/primitives/pagination/Page.test.ts new file mode 100644 index 0000000..5681505 --- /dev/null +++ b/src/domain/primitives/pagination/Page.test.ts @@ -0,0 +1,26 @@ +import { createPage } from './Page'; + +describe('createPage', () => { + it('builds a Page from items and totals', () => { + const items = ['a', 'b', 'c']; + const page = createPage(items, 1, 10, 23); + + expect(page).toEqual({ + items, + page: 1, + pageSize: 10, + totalPages: 3, + totalRecords: 23, + }); + }); + + it('rounds totalPages up for partial final pages', () => { + const page = createPage([], 1, 7, 15); + expect(page.totalPages).toBe(3); + }); + + it('produces 0 totalPages when there are no records', () => { + const page = createPage([], 1, 10, 0); + expect(page.totalPages).toBe(0); + }); +}); diff --git a/src/domain/primitives/units/Ether.test.ts b/src/domain/primitives/units/Ether.test.ts index 7a932ba..e44e889 100644 --- a/src/domain/primitives/units/Ether.test.ts +++ b/src/domain/primitives/units/Ether.test.ts @@ -28,4 +28,9 @@ describe('Ether', () => { const token = Ether.create(value); expect(token.toBigNumber()).toEqual(value); }); + + it('toEther() returns the same instance (identity conversion)', () => { + const token = Ether.create(new BigNumber(1.23)); + expect(token.toEther()).toBe(token); + }); }); diff --git a/src/domain/primitives/units/Gwei.test.ts b/src/domain/primitives/units/Gwei.test.ts index 331d316..bd50704 100644 --- a/src/domain/primitives/units/Gwei.test.ts +++ b/src/domain/primitives/units/Gwei.test.ts @@ -43,4 +43,14 @@ describe('Gwei', () => { const value2 = new BigNumber(123); expect(token1.times(value2).toBigNumber().toNumber()).toEqual(39_483); }); + + it('toGwei() returns the same instance (identity conversion)', () => { + const token = Gwei.create(new BigNumber(1_230_000_000)); + expect(token.toGwei()).toBe(token); + }); + + it('deserialize() parses a decimal string', () => { + const token = Gwei.deserialize('1230000000'); + expect(token.toBigNumber().toNumber()).toEqual(1_230_000_000); + }); }); diff --git a/src/domain/primitives/units/Wei.test.ts b/src/domain/primitives/units/Wei.test.ts index 1df52b8..b048034 100644 --- a/src/domain/primitives/units/Wei.test.ts +++ b/src/domain/primitives/units/Wei.test.ts @@ -26,4 +26,9 @@ describe('Wei', () => { const token = Wei.create(value); expect(token.toBigNumber()).toEqual(value); }); + + it('toWei() returns the same instance (identity conversion)', () => { + const token = Wei.create(new BigNumber('1230000000000000000')); + expect(token.toWei()).toBe(token); + }); }); diff --git a/src/infrastructure/stores/EnvioClient.test.ts b/src/infrastructure/stores/EnvioClient.test.ts new file mode 100644 index 0000000..cdfa555 --- /dev/null +++ b/src/infrastructure/stores/EnvioClient.test.ts @@ -0,0 +1,86 @@ +import { GraphQLClient } from 'graphql-request'; +import { EnvioClient } from './EnvioClient'; + +describe('EnvioClient', () => { + const ENDPOINT = 'https://indexer.example.invalid/v1/graphql'; + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('constructs without auth headers when no token is supplied', () => { + const ctorSpy = vi.spyOn(GraphQLClient.prototype, 'setHeader'); + + // Just constructing should not call setHeader; we look at the + // GraphQLClient internals instead. + const client = new EnvioClient(ENDPOINT); + // Cast through unknown to inspect the private GraphQLClient. + const inner = ( + client as unknown as { client: { requestConfig: { headers?: unknown } } } + ).client; + + expect(inner.requestConfig.headers).toEqual({}); + // No setHeader is invoked. + expect(ctorSpy).not.toHaveBeenCalled(); + }); + + it('sets a Bearer authorization header when an apiToken is supplied', () => { + const client = new EnvioClient(ENDPOINT, 'secret-token'); + const inner = ( + client as unknown as { + client: { requestConfig: { headers?: Record } }; + } + ).client; + + expect(inner.requestConfig.headers).toEqual({ + Authorization: 'Bearer secret-token', + }); + }); + + it('delegates query() to the underlying GraphQLClient and returns the result', async () => { + const payload = { Domain: [] }; + const requestSpy = vi + .spyOn(GraphQLClient.prototype, 'request') + .mockResolvedValue(payload); + + const client = new EnvioClient(ENDPOINT); + const result = await client.query('query { Domain { id } }', { + name: 'x.aragon.eth', + }); + + expect(result).toBe(payload); + expect(requestSpy).toHaveBeenCalledTimes(1); + expect(requestSpy).toHaveBeenCalledWith('query { Domain { id } }', { + name: 'x.aragon.eth', + }); + }); + + it('forwards undefined variables when none are supplied', async () => { + const requestSpy = vi + .spyOn(GraphQLClient.prototype, 'request') + .mockResolvedValue({}); + + const client = new EnvioClient(ENDPOINT); + await client.query('query { __typename }'); + + expect(requestSpy).toHaveBeenCalledWith('query { __typename }', undefined); + }); + + it('wraps network/GraphQL failures in a descriptive Error', async () => { + const cause = new Error('network down'); + vi.spyOn(GraphQLClient.prototype, 'request').mockRejectedValue(cause); + + const client = new EnvioClient(ENDPOINT); + + await expect(client.query('query { x }')).rejects.toThrow( + 'Envio GraphQL query failed', + ); + + // The original error is preserved on `.cause`. + try { + await client.query('query { x }'); + } catch (err) { + expect((err as Error).cause).toBe(cause); + } + }); +}); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts index 2d6e00a..ef899ae 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -4,7 +4,8 @@ import { EnvioMemberProfileStore } from './EnvioMemberProfileStore'; const CHAIN_ID = '1'; const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; -const NODE = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +const NODE = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; const textId = (version: string, key: string) => `${CHAIN_ID}-${RESOLVER}-${NODE}-${version}-${key}`; @@ -23,7 +24,7 @@ function buildMockEnvioClient(responses: unknown[]): MockClient { const calls: QueryCall[] = []; const queue = [...responses]; const envio = { - query: jest.fn( + query: vi.fn( async (document: string, variables: Record) => { calls.push({ document, variables }); return queue.shift(); @@ -47,7 +48,11 @@ describe('EnvioMemberProfileStore', () => { version: '0', texts: [ { id: textId('0', 'avatar'), key: 'avatar', value: 'ipfs://x' }, - { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + { + id: textId('0', 'url'), + key: 'url', + value: 'https://aragon.org', + }, ], }, }, @@ -117,7 +122,11 @@ describe('EnvioMemberProfileStore', () => { version: '0', texts: [ { id: textId('0', 'avatar'), key: 'avatar', value: null }, - { id: textId('0', 'url'), key: 'url', value: 'https://aragon.org' }, + { + id: textId('0', 'url'), + key: 'url', + value: 'https://aragon.org', + }, ], }, }, @@ -194,7 +203,7 @@ describe('EnvioMemberProfileStore', () => { it('wraps GraphQL failures with a profile-level error', async () => { const envio = { - query: jest.fn().mockRejectedValue(new Error('graphql exploded')), + query: vi.fn().mockRejectedValue(new Error('graphql exploded')), } as unknown as EnvioClient; const store = new EnvioMemberProfileStore(envio); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts new file mode 100644 index 0000000..5423dd8 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts @@ -0,0 +1,22 @@ +import { extractVersion, mapDTOToDomain } from './MemberProfileTextRecordMap'; + +describe('extractVersion', () => { + it('returns the 4th hyphen-separated segment of a well-formed id', () => { + expect(extractVersion('1-0xresolver-0xnode-7-com.github-handle')).toBe('7'); + }); + + it('returns an empty string when the id has fewer than 4 segments', () => { + // This is the safety fallback for malformed indexer ids — the + // production code path never produces these, but we still treat + // them as "non-live" rather than crashing. + expect(extractVersion('')).toBe(''); + expect(extractVersion('a-b-c')).toBe(''); + }); +}); + +describe('mapDTOToDomain', () => { + it('rejects responses whose shape does not match the schema', () => { + expect(() => mapDTOToDomain({ Domain: 'nope' })).toThrow(); + expect(() => mapDTOToDomain(null)).toThrow(); + }); +}); diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts index 11a20e7..78e3489 100644 --- a/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.test.ts @@ -12,7 +12,7 @@ describe('GetMemberProfileTextRecordsUseCase', () => { const buildMockStore = ( records: MemberProfileTextRecord[] = [], ): MemberProfileStore => ({ - findTextRecordsBySubdomain: jest.fn().mockResolvedValue(records), + findTextRecordsBySubdomain: vi.fn().mockResolvedValue(records), }); it('returns the text records from the store', async () => { @@ -49,7 +49,7 @@ describe('GetMemberProfileTextRecordsUseCase', () => { it('wraps store errors with a use-case message', async () => { const failingStore: MemberProfileStore = { - findTextRecordsBySubdomain: jest + findTextRecordsBySubdomain: vi .fn() .mockRejectedValue(new Error('graphql exploded')), }; diff --git a/src/use-cases/GetMemberProfileTextRecordsUseCase.ts b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts index 7aabdbf..62f0915 100644 --- a/src/use-cases/GetMemberProfileTextRecordsUseCase.ts +++ b/src/use-cases/GetMemberProfileTextRecordsUseCase.ts @@ -14,10 +14,7 @@ export interface GetMemberProfileTextRecordsUseCaseProps { */ export class GetMemberProfileTextRecordsUseCase implements - UseCase< - GetMemberProfileTextRecordsUseCaseProps, - MemberProfileTextRecord[] - > + UseCase { public readonly code = 'GetMemberProfileTextRecordsUseCase'; @@ -27,7 +24,9 @@ export class GetMemberProfileTextRecordsUseCase props: GetMemberProfileTextRecordsUseCaseProps, ): Promise { try { - return await this.profileStore.findTextRecordsBySubdomain(props.subdomain); + return await this.profileStore.findTextRecordsBySubdomain( + props.subdomain, + ); } catch (cause) { throw new Error('Error while getting member profile text records', { cause, diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 579f822..7d593b0 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -14,7 +14,7 @@ const NODE = function buildController(responses: unknown[]): AragonSubdomain { const envio = new EnvioClient('https://unused.example.invalid'); const queue = [...responses]; - jest.spyOn(envio, 'query').mockImplementation(async () => { + vi.spyOn(envio, 'query').mockImplementation(async () => { if (queue.length === 0) { throw new Error('EnvioClient.query called more times than expected'); } @@ -83,9 +83,7 @@ describe('AragonSubdomain', () => { it('returns [] when the subdomain has no resolver yet', async () => { const controller = buildController([ { - Domain: [ - { id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }, - ], + Domain: [{ id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }], }, ]); diff --git a/tsconfig.json b/tsconfig.json index ddbfbe7..f1be642 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "resolveJsonModule": true, "sourceMap": true, "noEmit": true, - "types": ["jest", "node"], + "types": ["node", "vitest/globals"], "baseUrl": ".", "paths": { "@/*": ["src/*"] diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..a78ef13 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,35 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@': resolve(__dirname, 'src'), + }, + }, + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts', 'test/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'text-summary', 'html', 'lcov'], + include: ['src/**/*.ts'], + exclude: [ + 'src/**/*.test.ts', + // Pure type-only modules — no executable JS is emitted, so + // including them in coverage just produces phantom uncovered + // lines. + 'src/**/index.ts', + 'src/domain/member-profile/MemberProfileStore.ts', + 'src/domain/primitives/units/EVMUnit.ts', + ], + thresholds: { + statements: 100, + branches: 100, + functions: 100, + lines: 100, + }, + }, + }, +}); From 0bf3dc8e412ff5e3bd47bce6cb3de747f4ef8f1d Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 21 May 2026 22:43:45 +0000 Subject: [PATCH 08/33] Upgrade to vitest --- .../TokenVotingMember.test.ts | 5 +- src/domain/voting-power/VotingPower.test.ts | 5 + .../maps/domain/MemberMap.test.ts | 45 +++++++ .../use-cases/GetERC20MembershipMap.test.ts | 32 +++++ .../EnvioTokenVotingMemberStore.test.ts | 29 ++-- .../maps/TokenVotingMemberMap.test.ts | 126 ++++++++++++++++++ .../GetERC20MembershipUseCase.test.ts | 4 +- vitest.config.ts | 1 + 8 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts create mode 100644 src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts diff --git a/src/domain/token-voting-member/TokenVotingMember.test.ts b/src/domain/token-voting-member/TokenVotingMember.test.ts index 4a1c44f..a091aa1 100644 --- a/src/domain/token-voting-member/TokenVotingMember.test.ts +++ b/src/domain/token-voting-member/TokenVotingMember.test.ts @@ -40,7 +40,10 @@ describe('TokenVotingMember', () => { }); it('carries an ENS name when present', () => { - const member = TokenVotingMember.create({ ...validProps, ens: 'alice.eth' }); + const member = TokenVotingMember.create({ + ...validProps, + ens: 'alice.eth', + }); expect(member.ens).toBe('alice.eth'); }); diff --git a/src/domain/voting-power/VotingPower.test.ts b/src/domain/voting-power/VotingPower.test.ts index 91a5af3..7a86b2a 100644 --- a/src/domain/voting-power/VotingPower.test.ts +++ b/src/domain/voting-power/VotingPower.test.ts @@ -38,6 +38,11 @@ describe('VotingPower', () => { expect(a.equals(b)).toBe(false); }); + it('equals(undefined) returns false', () => { + const vp = VotingPower.fromBigInt(100n); + expect(vp.equals(undefined)).toBe(false); + }); + it('converts to ether', () => { const vp = VotingPower.fromBigInt(1230000000000000000n); expect(vp.value.toEther().toBigNumber().toNumber()).toBeCloseTo(1.23); diff --git a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts b/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts new file mode 100644 index 0000000..182044a --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts @@ -0,0 +1,45 @@ +import { Member } from '@/domain/member/Member'; +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { mapDomainToDTO } from './MemberMap'; + +describe('MemberMap.mapDomainToDTO', () => { + const address = Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ); + + const buildMember = ( + overrides: Partial[0]> = {}, + ) => + Member.create({ + address, + ens: 'alice.eth', + votingPower: VotingPower.fromBigInt(5000000000000000000n), + firstActivityTimestamp: 1705320000, + lastActivityTimestamp: 1718872200, + delegationCount: 3, + ...overrides, + }); + + it('maps a Member with full activity to a DTO', () => { + const dto = mapDomainToDTO(buildMember()); + + expect(dto.ens).toBe('alice.eth'); + expect(dto.address).toMatch(/^0x[0-9a-fA-F]{40}$/); + expect(dto.votingPower).toBe('5000000000000000000'); + expect(dto.metrics).toEqual({ + firstActivityTimestamp: 1705320000, + lastActivityTimestamp: 1718872200, + delegationCount: 3, + }); + }); + + it('serializes zero activity timestamps as null (no recorded activity)', () => { + const dto = mapDomainToDTO( + buildMember({ firstActivityTimestamp: 0, lastActivityTimestamp: 0 }), + ); + + expect(dto.metrics.firstActivityTimestamp).toBeNull(); + expect(dto.metrics.lastActivityTimestamp).toBeNull(); + }); +}); diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts new file mode 100644 index 0000000..8d9d461 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts @@ -0,0 +1,32 @@ +import { mapDTOToDomain } from './GetERC20MembershipMap'; + +describe('GetERC20MembershipMap.mapDTOToDomain', () => { + const pluginAddress = '0x1111111111111111111111111111111111111111'; + const tokenContractAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + + it('passes explicit page and pageSize through to the PageRequest', () => { + const result = mapDTOToDomain({ + pluginAddress, + tokenContractAddress, + page: 3, + pageSize: 50, + }); + + expect(result.page.page).toBe(3); + expect(result.page.pageSize).toBe(50); + }); + + it('defaults page to 1 and pageSize to 20 when omitted', () => { + const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); + + expect(result.page.page).toBe(1); + expect(result.page.pageSize).toBe(20); + }); + + it('forwards plugin and token addresses unchanged', () => { + const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); + + expect(result.pluginAddress).toBe(pluginAddress); + expect(result.tokenContractAddress).toBe(tokenContractAddress); + }); +}); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts index c5ce609..d6db8f7 100644 --- a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts @@ -30,15 +30,20 @@ function buildMockEnvioClient(responses: unknown[]): MockClient { const calls: QueryCall[] = []; const queue = [...responses]; const envio = { - query: jest.fn(async (document: string, variables: Record) => { - calls.push({ document, variables }); - return queue.shift(); - }), + query: vi.fn( + async (document: string, variables: Record) => { + calls.push({ document, variables }); + return queue.shift(); + }, + ), } as unknown as EnvioClient; return { envio, calls }; } -const buildDelegate = (address: string, votingPower = '5000000000000000000') => ({ +const buildDelegate = ( + address: string, + votingPower = '5000000000000000000', +) => ({ id: `1-${TOKEN}-${address}`, chainId: 1, tokenContractAddress: TOKEN, @@ -61,7 +66,11 @@ describe('EnvioTokenVotingMemberStore', () => { }, { ReverseName: [ - { address: ALICE, coinType: DEFAULT_EVM_COIN_TYPE, name: 'alice.eth' }, + { + address: ALICE, + coinType: DEFAULT_EVM_COIN_TYPE, + name: 'alice.eth', + }, ], }, ]); @@ -87,7 +96,11 @@ describe('EnvioTokenVotingMemberStore', () => { }, { ReverseName: [ - { address: ALICE, coinType: DEFAULT_EVM_COIN_TYPE, name: 'alice-default.eth' }, + { + address: ALICE, + coinType: DEFAULT_EVM_COIN_TYPE, + name: 'alice-default.eth', + }, { address: ALICE, coinType: ETH_COIN_TYPE, name: 'alice-legacy.eth' }, ], }, @@ -154,7 +167,7 @@ describe('EnvioTokenVotingMemberStore', () => { it('wraps query failures with a member-level error', async () => { const envio = { - query: jest.fn().mockRejectedValue(new Error('graphql exploded')), + query: vi.fn().mockRejectedValue(new Error('graphql exploded')), } as unknown as EnvioClient; const store = new EnvioTokenVotingMemberStore(envio); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts new file mode 100644 index 0000000..a013286 --- /dev/null +++ b/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts @@ -0,0 +1,126 @@ +import { + type ERC20VotesDelegateDTO, + type MemberMetricsDTO, + mapDTOToDomain, + parseFindMembersResponse, +} from './TokenVotingMemberMap'; + +const PLUGIN = '0x1111111111111111111111111111111111111111'; +const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; +const MEMBER = '0x0123456789abcdef0123456789abcdef01234567'; + +const buildDelegate = ( + overrides: Partial = {}, +): ERC20VotesDelegateDTO => ({ + id: `1-${TOKEN}-${MEMBER}`, + chainId: 1, + tokenContractAddress: TOKEN, + delegateAddress: MEMBER, + votingPower: '5000000000000000000', + delegationCount: 1, + firstVotingPowerChangeTimestamp: '1700000000', + lastVotingPowerChangeTimestamp: '1700000100', + ...overrides, +}); + +const buildMetrics = ( + overrides: Partial = {}, +): MemberMetricsDTO => ({ + id: `1-${PLUGIN}-${MEMBER}`, + chainId: 1, + pluginAddress: PLUGIN, + memberAddress: MEMBER, + firstActivityTimestamp: '1650000000', + lastActivityTimestamp: '1750000000', + ...overrides, +}); + +describe('parseFindMembersResponse', () => { + it('parses a well-formed response', () => { + const parsed = parseFindMembersResponse({ + ERC20VotesDelegate: [buildDelegate()], + AllERC20VotesDelegate: [{ id: '1' }], + MemberMetrics: [buildMetrics()], + }); + + expect(parsed.ERC20VotesDelegate).toHaveLength(1); + expect(parsed.MemberMetrics).toHaveLength(1); + }); + + it('throws when the response shape does not match', () => { + expect(() => + parseFindMembersResponse({ ERC20VotesDelegate: 'oops' }), + ).toThrow(); + expect(() => parseFindMembersResponse(null)).toThrow(); + }); +}); + +describe('mapDTOToDomain', () => { + it('takes the min of metrics first-activity and delegate first-VP-change when both are defined and the metrics value is smaller', () => { + // metrics.firstActivityTimestamp (1650000000) < delegate.firstVotingPowerChangeTimestamp (1700000000) + const member = mapDTOToDomain( + buildDelegate(), + buildMetrics(), + null, + PLUGIN, + ); + expect(member.firstActivityTimestamp).toBe(1650000000); + }); + + it('takes the min when the delegate value is smaller (exercises the n < min comparison branch)', () => { + const member = mapDTOToDomain( + buildDelegate({ firstVotingPowerChangeTimestamp: '1600000000' }), + buildMetrics({ firstActivityTimestamp: '1650000000' }), + null, + PLUGIN, + ); + expect(member.firstActivityTimestamp).toBe(1600000000); + }); + + it('takes the max of metrics last-activity and delegate last-VP-change when both are defined and the metrics value is larger', () => { + // metrics.lastActivityTimestamp (1750000000) > delegate.lastVotingPowerChangeTimestamp (1700000100) + const member = mapDTOToDomain( + buildDelegate(), + buildMetrics(), + null, + PLUGIN, + ); + expect(member.lastActivityTimestamp).toBe(1750000000); + }); + + it('takes the max when the delegate value is larger (exercises the n > max comparison branch)', () => { + const member = mapDTOToDomain( + buildDelegate({ lastVotingPowerChangeTimestamp: '1800000000' }), + buildMetrics({ lastActivityTimestamp: '1750000000' }), + null, + PLUGIN, + ); + expect(member.lastActivityTimestamp).toBe(1800000000); + }); + + it('returns 0 timestamps when both signals are null/undefined', () => { + // Delegate without VP-change timestamps, no metrics — the merger + // should fall through to the `?? 0` sentinel on both sides. + const member = mapDTOToDomain( + buildDelegate({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), + undefined, + null, + PLUGIN, + ); + expect(member.firstActivityTimestamp).toBe(0); + expect(member.lastActivityTimestamp).toBe(0); + }); + + it('forwards the supplied ENS name to the domain object', () => { + const member = mapDTOToDomain( + buildDelegate(), + undefined, + 'alice.eth', + PLUGIN, + ); + expect(member.ens).toBe('alice.eth'); + }); +}); diff --git a/src/use-cases/GetERC20MembershipUseCase.test.ts b/src/use-cases/GetERC20MembershipUseCase.test.ts index 51a4297..76fe269 100644 --- a/src/use-cases/GetERC20MembershipUseCase.test.ts +++ b/src/use-cases/GetERC20MembershipUseCase.test.ts @@ -34,7 +34,7 @@ describe('GetERC20MembershipUseCase', () => { const buildMockStore = ( member: TokenVotingMember = buildMember(), ): TokenVotingMemberStore => ({ - findMembersByPluginAndToken: jest + findMembersByPluginAndToken: vi .fn() .mockResolvedValue(createPage([member], 1, 20, 1)), }); @@ -87,7 +87,7 @@ describe('GetERC20MembershipUseCase', () => { it('wraps store errors', async () => { const failingStore: TokenVotingMemberStore = { - findMembersByPluginAndToken: jest + findMembersByPluginAndToken: vi .fn() .mockRejectedValue(new Error('db down')), }; diff --git a/vitest.config.ts b/vitest.config.ts index a78ef13..1bd9f22 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ 'src/**/index.ts', 'src/domain/member-profile/MemberProfileStore.ts', 'src/domain/primitives/units/EVMUnit.ts', + 'src/domain/token-voting-member/TokenVotingMemberStore.ts', ], thresholds: { statements: 100, From 16aeb929e767a1774582530958d5387f755240a0 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 22 May 2026 00:10:23 +0100 Subject: [PATCH 09/33] Remove unneeded fields from query --- .../EnvioMemberProfileStore.test.ts | 10 ---------- .../EnvioMemberProfileStore/EnvioMemberProfileStore.ts | 2 -- .../maps/MemberProfileTextRecordMap.ts | 2 -- 3 files changed, 14 deletions(-) diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts index ef899ae..ca26776 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -42,8 +42,6 @@ describe('EnvioMemberProfileStore', () => { { Domain: [ { - id: 'domain-id', - name: 'ea1.aragon.eth', resolver: { version: '0', texts: [ @@ -90,8 +88,6 @@ describe('EnvioMemberProfileStore', () => { { Domain: [ { - id: 'domain-id', - name: 'ea1.aragon.eth', resolver: { version: '1', texts: [ @@ -116,8 +112,6 @@ describe('EnvioMemberProfileStore', () => { { Domain: [ { - id: 'domain-id', - name: 'ea1.aragon.eth', resolver: { version: '0', texts: [ @@ -148,8 +142,6 @@ describe('EnvioMemberProfileStore', () => { { Domain: [ { - id: 'domain-id', - name: 'ea1.aragon.eth', resolver: { version: '7', texts: [ @@ -187,8 +179,6 @@ describe('EnvioMemberProfileStore', () => { { Domain: [ { - id: 'domain-id', - name: 'ea1.aragon.eth', resolver: null, }, ], diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts index abf9024..c0b8e29 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts @@ -11,8 +11,6 @@ import * as MemberProfileTextRecordMap from './maps/MemberProfileTextRecordMap'; const FIND_TEXT_RECORDS_QUERY = ` query FindMemberProfileTextRecords($name: String!) { Domain(where: { name: { _eq: $name } }, limit: 1) { - id - name resolver { version texts { diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts index 5ac4913..770751d 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts @@ -30,8 +30,6 @@ const FindMemberProfileTextRecordsResponseSchema = z.object({ Domain: z .array( z.object({ - id: z.string(), - name: z.string(), resolver: ResolverSchema.nullable(), }), ) From 367efecbfd50e845d39690b70d6221c4e7f9e7bc Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 22 May 2026 20:02:59 +0100 Subject: [PATCH 10/33] JSdoc --- .../controllers/AragonController/AragonController.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index 5692f23..02fe7e9 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -19,9 +19,15 @@ interface HandlersRecord { >; } +/** + * General domain for all Aragon-related business logic. + */ export class AragonController { private constructor(private readonly handlers: HandlersRecord) {} + /** + * Initializes the `AragonSubdomain`. + */ static load(envioClient: EnvioClient): AragonController { const memberProfileStore = new EnvioMemberProfileStore(envioClient); const getMemberProfileTextRecordsUseCase = From 3e6f1542e1d82a54cb8b241fd836ad3459e3699c Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 22 May 2026 22:24:46 +0100 Subject: [PATCH 11/33] Moves business logic from mapper to domain object --- .../MemberProfileResolver.test.ts | 96 +++++++++++++++++ .../member-profile/MemberProfileResolver.ts | 67 ++++++++++++ .../EnvioMemberProfileStore.test.ts | 100 +----------------- .../EnvioMemberProfileStore.ts | 5 +- .../maps/MemberProfileTextRecordMap.test.ts | 45 +++++--- .../maps/MemberProfileTextRecordMap.ts | 54 +++------- test/AragonSubdomain.test.ts | 15 +-- 7 files changed, 217 insertions(+), 165 deletions(-) create mode 100644 src/domain/member-profile/MemberProfileResolver.test.ts create mode 100644 src/domain/member-profile/MemberProfileResolver.ts diff --git a/src/domain/member-profile/MemberProfileResolver.test.ts b/src/domain/member-profile/MemberProfileResolver.test.ts new file mode 100644 index 0000000..285186c --- /dev/null +++ b/src/domain/member-profile/MemberProfileResolver.test.ts @@ -0,0 +1,96 @@ +import { MemberProfileResolver } from './MemberProfileResolver'; + +describe('MemberProfileResolver', () => { + it('returns entries whose version matches the resolver version', () => { + const resolver = MemberProfileResolver.create({ + version: '0', + entries: [ + { key: 'avatar', value: 'ipfs://x', version: '0' }, + { key: 'url', value: 'https://aragon.org', version: '0' }, + ], + }); + + const live = resolver.liveTextRecords(); + expect(live).toHaveLength(2); + expect(live[0].key).toBe('avatar'); + expect(live[0].value).toBe('ipfs://x'); + expect(live[1].key).toBe('url'); + expect(live[1].value).toBe('https://aragon.org'); + }); + + it('drops entries superseded by a VersionChanged bump', () => { + // Resolver bumped to v1; the v0 row is still in storage but is no + // longer live. + const resolver = MemberProfileResolver.create({ + version: '1', + entries: [ + { key: 'avatar', value: 'stale', version: '0' }, + { key: 'avatar', value: 'fresh', version: '1' }, + ], + }); + + const live = resolver.liveTextRecords(); + expect(live).toHaveLength(1); + expect(live[0].value).toBe('fresh'); + }); + + it('drops cleared entries (value === null)', () => { + const resolver = MemberProfileResolver.create({ + version: '0', + entries: [ + { key: 'avatar', value: null, version: '0' }, + { key: 'url', value: 'https://aragon.org', version: '0' }, + ], + }); + + const live = resolver.liveTextRecords(); + expect(live).toHaveLength(1); + expect(live[0].key).toBe('url'); + }); + + it('preserves empty-string values (cleared is null, not empty string)', () => { + const resolver = MemberProfileResolver.create({ + version: '0', + entries: [{ key: 'bio', value: '', version: '0' }], + }); + + const live = resolver.liveTextRecords(); + expect(live).toHaveLength(1); + expect(live[0].value).toBe(''); + }); + + it('returns [] when there are no entries', () => { + const resolver = MemberProfileResolver.create({ + version: '0', + entries: [], + }); + + expect(resolver.liveTextRecords()).toEqual([]); + }); + + it('exposes the current version', () => { + const resolver = MemberProfileResolver.create({ + version: '7', + entries: [], + }); + expect(resolver.version).toBe('7'); + }); + + it('rejects entries with an empty key', () => { + expect(() => + MemberProfileResolver.create({ + version: '0', + entries: [{ key: '', value: 'x', version: '0' }], + }), + ).toThrow(); + }); + + it('rejects an empty resolver version', () => { + expect(() => + MemberProfileResolver.create({ + version: '', + entries: [], + }), + ).toThrow(); + }); +}); diff --git a/src/domain/member-profile/MemberProfileResolver.ts b/src/domain/member-profile/MemberProfileResolver.ts new file mode 100644 index 0000000..12a8681 --- /dev/null +++ b/src/domain/member-profile/MemberProfileResolver.ts @@ -0,0 +1,67 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { MemberProfileTextRecord } from './MemberProfileTextRecord'; + +const MemberProfileResolverEntrySchema = z.object({ + key: z.string().min(1, 'entry key must not be empty'), + value: z.string().nullable(), + version: z.string().min(1, 'entry version must not be empty'), +}); + +const MemberProfileResolverPropsSchema = z.object({ + version: z.string().min(1, 'resolver version must not be empty'), + entries: z.array(MemberProfileResolverEntrySchema).readonly(), +}); + +type MemberProfileResolverProps = z.output< + typeof MemberProfileResolverPropsSchema +>; + +/** + * The ENS Public Resolver attached to a member's + * subdomain. Owns the resolver's current version plus the candidate + * text-record entries the indexer has seen for it. + * + * Liveness rule (encoded in `liveTextRecords`): + * 1. an entry with `value === null` has been cleared on-chain and is + * not a live record; + * 2. an entry whose `version` differs from the resolver's current + * `version` was invalidated by a later `VersionChanged` event and + * is not a live record. + * + * This rule mirrors the on-chain `VersionChanged` semantic of the + * canonical ENS Public Resolver: bumping the version logically clears + * every prior record without touching individual rows. + */ +export class MemberProfileResolver extends ValueObject { + get version(): string { + return this.props.version; + } + + /** + * Returns the text records that are currently live on this resolver. + * Drops cleared rows (`value === null`) and rows superseded by a + * later `VersionChanged` bump. + */ + liveTextRecords(): MemberProfileTextRecord[] { + const live: MemberProfileTextRecord[] = []; + for (const entry of this.props.entries) { + if (entry.value === null) continue; + if (entry.version !== this.props.version) continue; + live.push( + MemberProfileTextRecord.create({ + key: entry.key, + value: entry.value, + }), + ); + } + return live; + } + + static create( + props: z.input, + ): MemberProfileResolver { + const validated = MemberProfileResolverPropsSchema.parse(props); + return new MemberProfileResolver(validated); + } +} diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts index ca26776..53295db 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -2,14 +2,6 @@ import { MemberProfileAragonName } from '@/domain/member-profile/MemberProfileAr import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberProfileStore } from './EnvioMemberProfileStore'; -const CHAIN_ID = '1'; -const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; -const NODE = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - -const textId = (version: string, key: string) => - `${CHAIN_ID}-${RESOLVER}-${NODE}-${version}-${key}`; - interface QueryCall { document: string; variables: Record; @@ -45,12 +37,8 @@ describe('EnvioMemberProfileStore', () => { resolver: { version: '0', texts: [ - { id: textId('0', 'avatar'), key: 'avatar', value: 'ipfs://x' }, - { - id: textId('0', 'url'), - key: 'url', - value: 'https://aragon.org', - }, + { key: 'avatar', value: 'ipfs://x', version: '0' }, + { key: 'url', value: 'https://aragon.org', version: '0' }, ], }, }, @@ -81,90 +69,6 @@ describe('EnvioMemberProfileStore', () => { expect(calls[0].variables).toEqual({ name: 'ea1.aragon.eth' }); }); - it('drops text rows superseded by a VersionChanged bump', async () => { - // Resolver is on version 1; the indexer still has the v0 row sitting - // there. The store must filter it out. - const { envio } = buildMockEnvioClient([ - { - Domain: [ - { - resolver: { - version: '1', - texts: [ - { id: textId('0', 'avatar'), key: 'avatar', value: 'stale' }, - { id: textId('1', 'avatar'), key: 'avatar', value: 'fresh' }, - ], - }, - }, - ], - }, - ]); - const store = new EnvioMemberProfileStore(envio); - - const result = await store.findTextRecordsBySubdomain(subdomain); - - expect(result).toHaveLength(1); - expect(result[0].value).toBe('fresh'); - }); - - it('drops cleared rows (value === null)', async () => { - const { envio } = buildMockEnvioClient([ - { - Domain: [ - { - resolver: { - version: '0', - texts: [ - { id: textId('0', 'avatar'), key: 'avatar', value: null }, - { - id: textId('0', 'url'), - key: 'url', - value: 'https://aragon.org', - }, - ], - }, - }, - ], - }, - ]); - const store = new EnvioMemberProfileStore(envio); - - const result = await store.findTextRecordsBySubdomain(subdomain); - - expect(result).toHaveLength(1); - expect(result[0].key).toBe('url'); - }); - - it('preserves keys that contain hyphens', async () => { - // The key segment may contain `-`. Version-extraction must not be - // tripped up by it (we split on `-` and take parts[3]). - const { envio } = buildMockEnvioClient([ - { - Domain: [ - { - resolver: { - version: '7', - texts: [ - { - id: textId('7', 'com.github-handle'), - key: 'com.github-handle', - value: 'aragon', - }, - ], - }, - }, - ], - }, - ]); - const store = new EnvioMemberProfileStore(envio); - - const result = await store.findTextRecordsBySubdomain(subdomain); - - expect(result).toHaveLength(1); - expect(result[0].key).toBe('com.github-handle'); - expect(result[0].value).toBe('aragon'); - }); - it('returns [] when the subdomain is not on the indexer', async () => { const { envio } = buildMockEnvioClient([{ Domain: [] }]); const store = new EnvioMemberProfileStore(envio); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts index c0b8e29..5b8366d 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts @@ -14,9 +14,9 @@ const FIND_TEXT_RECORDS_QUERY = ` resolver { version texts { - id key value + version } } } @@ -33,7 +33,8 @@ export class EnvioMemberProfileStore implements MemberProfileStore { const raw = await this.envio.query(FIND_TEXT_RECORDS_QUERY, { name: subdomain.toString(), }); - return MemberProfileTextRecordMap.mapDTOToDomain(raw); + const resolver = MemberProfileTextRecordMap.mapDTOToDomain(raw); + return resolver?.liveTextRecords() ?? []; } catch (cause) { throw new Error('Error querying member profile from Envio', { cause }); } diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts index 5423dd8..8cfe26a 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts @@ -1,22 +1,37 @@ -import { extractVersion, mapDTOToDomain } from './MemberProfileTextRecordMap'; - -describe('extractVersion', () => { - it('returns the 4th hyphen-separated segment of a well-formed id', () => { - expect(extractVersion('1-0xresolver-0xnode-7-com.github-handle')).toBe('7'); - }); - - it('returns an empty string when the id has fewer than 4 segments', () => { - // This is the safety fallback for malformed indexer ids — the - // production code path never produces these, but we still treat - // them as "non-live" rather than crashing. - expect(extractVersion('')).toBe(''); - expect(extractVersion('a-b-c')).toBe(''); - }); -}); +import { mapDTOToDomain } from './MemberProfileTextRecordMap'; describe('mapDTOToDomain', () => { it('rejects responses whose shape does not match the schema', () => { expect(() => mapDTOToDomain({ Domain: 'nope' })).toThrow(); expect(() => mapDTOToDomain(null)).toThrow(); }); + + it('returns null when the indexer has no Domain row for this name', () => { + expect(mapDTOToDomain({ Domain: [] })).toBeNull(); + }); + + it('returns null when the Domain exists but has no resolver yet', () => { + expect(mapDTOToDomain({ Domain: [{ resolver: null }] })).toBeNull(); + }); + + it('builds a MemberProfileResolver carrying the resolver version + entries', () => { + const resolver = mapDTOToDomain({ + Domain: [ + { + resolver: { + version: '0', + texts: [ + { key: 'avatar', value: 'ipfs://x', version: '0' }, + { key: 'url', value: 'https://aragon.org', version: '0' }, + ], + }, + }, + ], + }); + + expect(resolver).not.toBeNull(); + expect(resolver?.version).toBe('0'); + const live = resolver?.liveTextRecords() ?? []; + expect(live.map((r) => r.key)).toEqual(['avatar', 'url']); + }); }); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts index 770751d..b4d24f5 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts @@ -1,18 +1,10 @@ import { z } from 'zod'; -import { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; +import { MemberProfileResolver } from '@/domain/member-profile/MemberProfileResolver'; -/** - * Shape of a single text-record row on the indexer. - * - * The `id` is structured as - * `${chainId}-${resolverAddress}-${domainNode}-${version}-${key}`. - * The first four segments are hyphen-free; the `key` segment (last) - * may itself contain hyphens. - */ const TextRecordRowSchema = z.object({ - id: z.string(), key: z.string(), value: z.string().nullable(), + version: z.string(), }); const ResolverSchema = z.object({ @@ -41,40 +33,28 @@ export type FindMemberProfileTextRecordsResponse = z.infer< typeof FindMemberProfileTextRecordsResponseSchema >; -/** - * Extracts the `${version}` segment from a TextRecord id. The first - * four segments (chainId, resolverAddress, domainNode, version) never - * contain a hyphen, so `split('-')[3]` is safe. - */ -export function extractVersion(id: string): string { - return id.split('-')[3] ?? ''; -} - /** * Trust boundary for the indexer's `FindMemberProfileTextRecords` - * response. Asserts the shape via Zod, filters out cleared rows and - * rows superseded by `VersionChanged` (kept around on the indexer but - * logically invalid), and constructs the domain value objects. + * response. * - * Throws a `ZodError` if the shape doesn't match what we expect. + * Returns `null` when the subdomain is unknown to the indexer or + * exists but has no resolver assigned yet. */ -export function mapDTOToDomain(raw: unknown): MemberProfileTextRecord[] { +export function mapDTOToDomain(raw: unknown): MemberProfileResolver | null { const parsed = FindMemberProfileTextRecordsResponseSchema.parse(raw); const domain = parsed.Domain[0]; // No row for this name on the indexer. - if (!domain) return []; + if (!domain) return null; // Domain exists but `setResolver` has not been called for it yet. - if (!domain.resolver) return []; - - const liveVersion = domain.resolver.version; - const live: MemberProfileTextRecord[] = []; - for (const text of domain.resolver.texts) { - if (text.value === null) continue; - if (extractVersion(text.id) !== liveVersion) continue; - live.push( - MemberProfileTextRecord.create({ key: text.key, value: text.value }), - ); - } - return live; + if (!domain.resolver) return null; + + return MemberProfileResolver.create({ + version: domain.resolver.version, + entries: domain.resolver.texts.map((text) => ({ + key: text.key, + value: text.value, + version: text.version, + })), + }); } diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 7d593b0..299e6f8 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -1,10 +1,6 @@ import assert from 'node:assert'; import { AragonSubdomain, EnvioClient } from '../src'; -const RESOLVER = '0x231b0ee14048e9dccd1d247744d114a4eb5e8e63'; -const NODE = - '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; - /** * Builds an AragonSubdomain backed by an EnvioClient whose `query` * method is replaced with a FIFO queue of canned responses. The @@ -25,9 +21,6 @@ function buildController(responses: unknown[]): AragonSubdomain { describe('AragonSubdomain', () => { describe('getMemberProfileTextRecords', () => { - const textId = (version: string, key: string) => - `1-${RESOLVER}-${NODE}-${version}-${key}`; - it('returns the live text records as a DTO list', async () => { const controller = buildController([ { @@ -38,15 +31,11 @@ describe('AragonSubdomain', () => { resolver: { version: '0', texts: [ + { key: 'avatar', value: 'ipfs://x', version: '0' }, { - id: textId('0', 'avatar'), - key: 'avatar', - value: 'ipfs://x', - }, - { - id: textId('0', 'url'), key: 'url', value: 'https://aragon.org', + version: '0', }, ], }, From 2e04f8c09de5f56067343c70bccbd2efaff39c9c Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 26 May 2026 17:12:44 +0100 Subject: [PATCH 12/33] Docs --- AGENTS.md | 175 +++++++++++++++++++----------------------------------- CLAUDE.md | 1 + 2 files changed, 63 insertions(+), 113 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 2a21c34..c416556 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,145 +1,94 @@ # AGENTS.md — Aragon Subdomain -## What This Repo Is +Business logic package between the Envio indexer (`aragon-indexer/`) and the Next.js BFF. Built on [`ddd-core-ts`](https://github.com/asciiman/ddd-core-ts). -This package encapsulates all governance business logic for the Aragon platform. It replaces business logic currently fragmented across `app-backend` (Node.js) and `app` (Next.js frontend), consolidating it into a single importable TypeScript package. +## Read `ddd-core-ts` first -The package sits between the Envio on-chain indexer (`aragon-indexer/`) and the Next.js BFF layer. It queries indexed data from Envio, applies domain logic (membership aggregation, voting power calculation, permission checks), enriches with external data (ENS, token prices), and exposes use cases via a controller. +**Before writing any code, read the `ddd-core-ts` package README.** It defines the base classes (`ValueObject`, `Entity`, `UseCase`, `DomainError`, `DomainEvent`, `ProcessManager`), the store/repository pattern, the mapper and DTO conventions, the `handleRequest` controller wiring, Zod-at-creation validation, and `ResultOrError` / `defineError` error handling. Every domain object, use case, store, and controller in this repo must conform. Do not invent alternative patterns. -## How to Approach This Repo +Domain terminology comes from the [Aragon Governance Membership Domain Model](https://www.figma.com/board/WTlB4By8MoKhh5BJ58dpVE/Aragon-Governance---Membership-Domain-Model) — use those names, not legacy names from `app-backend` or `ve-governance-indexer`. -### Read `ddd-core-ts` first - -This repo is built on [`ddd-core-ts`](https://github.com/asciiman/ddd-core-ts). **Before writing any code, read the `ddd-core-ts` package README thoroughly.** It defines the specific patterns, base classes, naming conventions, and architectural rules that this repo must follow. Key things it covers: - -- `ValueObject`, `Entity`, `UseCase`, `DomainError`, `DomainEvent`, `ProcessManager` base classes -- Store (repository) pattern: interface in domain, implementation in infrastructure -- Mapper and DTO conventions -- `handleRequest` controller wiring -- Zod validation at domain object creation -- Error handling with `ResultOrError` and `defineError` - -Every domain object, use case, store, and controller in this repo must conform to the `ddd-core-ts` patterns. Do not invent alternative patterns. - -### Follow the domain model diagram - -The [Aragon Governance Membership Domain Model](https://www.figma.com/board/WTlB4By8MoKhh5BJ58dpVE/Aragon-Governance---Membership-Domain-Model) is the source of truth for naming and relationships. Use the terms from the diagram, not from legacy codebases (`app-backend`, `ve-governance-indexer`). Those are being deprecated. - -Key domain concepts from the diagram: - -- **Governance Plugin** — decides via votes, has Membership -- **Membership** — contains multiple Members -- **Member** — has Identity (ENS, avatar), Metrics, and Voting Power -- **Voting Power** — provided by different mechanisms depending on governance type -- **TokenVoting Delegation** — ERC20Votes: DelegateChanged, DelegateVotesChanged (all-or-nothing balance delegation) -- **VotingEscrow Delegation** — per-lock NFT delegation via EscrowIVotesAdapter -- Both TokenVoting Delegation and VotingEscrow Delegation provide VP to the TokenVoting Plugin - -### Architecture layers +## Repo map ``` src/ -├── domain/ # Pure business logic — no I/O, no infrastructure imports -├── use-cases/ # Application logic — orchestrates domain objects -└── infrastructure/ # Adapters — Envio client, ENS resolver, RPC readers, controllers +├── domain/ # Pure business logic — no I/O. Only depends on ddd-core-ts and zod. +├── use-cases/ # Application logic — orchestrates domain objects. +└── infrastructure/ # Adapters — Envio client, controllers, stores, mappers. ``` -Dependencies flow inward: Infrastructure → Use Cases → Domain. The domain layer has zero external dependencies beyond `ddd-core-ts` and `zod`. - -### Project structure within each layer +Dependencies flow inward: Infrastructure → Use Cases → Domain. -Follow the `ddd-core-ts` project structure conventions. +- **Path alias:** `@/*` resolves to `src/*` (see `tsconfig.json`). Always import as `@/domain/...`, never relative. +- **Public surface:** `src/index.ts` exports `AragonSubdomain` (the controller) and `EnvioClient`. That's the entire consumer-facing API. +- **Scripts:** `pnpm run build | test | lint | type-check`. Lint auto-formats via biome. -## Formatting Rules +Today the domain implements only `MemberProfile` (ENS text records on `.aragon.eth` subdomains). Other concepts (`Member`, `Membership`, `VotingPower`, `Delegation`, `Lock`, etc.) are planned — see `ENVIO_MIGRATION_PLAN.md`. Do not assume they exist. -1. **Indent with 2 spaces.** Configured via biome (`indentWidth: 2`). Run `pnpm run lint` to auto-format. -2. **JSDoc comments use multi-line format:** - ```typescript - /** - * My comment - */ - ``` - Never use single-line JSDoc (`/** My comment */`). -3. **Test files are colocated** with the source files they test: `Member.ts` → `Member.test.ts` in the same directory. Not in a separate `test/` folder. +## Canonical examples -## Primitives - -The `src/domain/primitives/` directory contains reusable domain primitives. Always use these instead of raw types: - -| Primitive | Use instead of | Import from | -|-----------|---------------|-------------| -| `Address` | `string` for Ethereum addresses | `@/domain/primitives` | -| `HexString` | `string` for 0x-prefixed hex values | `@/domain/primitives` | -| `HexNumber` | `string` or `bigint` for hex-encoded numbers | `@/domain/primitives` | -| `Wei` / `Gwei` / `Ether` | `BigNumber` for EVM unit values | `@/domain/primitives` | -| `UUID` | `string` for UUIDs | `@/domain/primitives` | -| `Page` / `PageRequest` | ad-hoc pagination shapes | `@/domain/primitives` | - -**Rules:** -- **Use `Address` for all Ethereum addresses** in domain objects and use cases. Never store addresses as raw strings in domain types. -- **Use `HexString` as the parameter type** for functions that accept 0x-prefixed hex strings (e.g., `Address.fromHexString(hex: HexString)`). This catches invalid inputs at compile time. -- **Cast to `HexString` at infrastructure boundaries** where external data enters as `string` (e.g., DTOs from Envio). Use `as HexString` in mappers — the runtime validation happens inside the primitive's factory method. -- **Serialize via `.toHexString()`** when converting domain objects to DTOs for external consumers. -- Import primitives from the barrel `@/domain/primitives`, not from individual files. - -## Key Rules +When in doubt, copy the shape of these files: -1. **Domain objects use Zod validation** in their `create()` factory methods. No invalid objects can exist. -2. **Value objects are immutable.** Use copy-and-modify patterns for state changes. -3. **Use cases have a single `execute()` method** and a `code` string matching the class name. -4. **Store interfaces live in the domain layer.** Implementations live in infrastructure. -5. **All dependencies are constructor-injected.** No singletons, no service locator. -6. **Mappers are co-located** with the infrastructure code that uses them, not in the domain. -7. **One public class per file.** Filename matches the class name. -8. **No `I` prefix on interfaces.** Use `MemberStore`, not `IMemberStore`. -9. **`as const` over TypeScript enums.** -10. **Named exports only.** No default exports. +| Concern | File | +|---------|------| +| Controller + `handleRequest` wiring | `src/infrastructure/controllers/AragonController/AragonController.ts` | +| Store-side mapper (Zod trust boundary) | `src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts` | +| Use case (single `execute()`, `code` field) | `src/use-cases/GetMemberProfileTextRecordsUseCase.ts` | +| Value object with Zod-at-`create()` | `src/domain/member-profile/MemberProfileTextRecord.ts` | +| Store interface in domain | `src/domain/member-profile/MemberProfileStore.ts` | -## Data Sources - -The primary data source is the Envio indexer (`aragon-indexer/`), which is partitioned internally into a generic / Aragon split (see `aragon-indexer/CLAUDE.md` → "Split path"): +## Primitives -Generic (chain-wide ERC20Votes): -- `ERC20VotesDelegate` — per-(token, delegate) state: voting power, delegationCount, first/lastVotingPowerChangeTimestamp -- `Delegation` — per-(token, delegator) → currentDelegate +Reusable domain primitives live in `src/domain/primitives/` and are re-exported from the barrel `@/domain/primitives`. Use these instead of raw types. -Aragon-specific: -- `Plugin` — Installed governance plugins (type, associated contracts) -- `VELock` — Voting escrow lock positions (amount, status, delegation) -- `MemberMetrics` — In-plugin engagement (VoteCast / ProposalCreated activity only) +| Primitive | Use instead of | Notes | +|-----------|---------------|-------| +| `Address` | `string` for Ethereum addresses | Build via `Address.fromHexString(hex)` | +| `HexString` | `string` for 0x-prefixed hex | Use as parameter type to catch invalid input at compile time | +| `HexNumber` | `string` / `bigint` for hex-encoded numbers | | +| `Wei` / `Gwei` / `Ether` | `BigNumber` for EVM unit values | | +| `UUID` | `string` for UUIDs | | +| `Page` / `PageRequest` | ad-hoc pagination shapes | | -`EnvioTokenVotingMemberStore.findMembersByPluginAndToken` is the join point between the two halves: it pulls `ERC20VotesDelegate` rows and `MemberMetrics` rows in one query and merges activity timestamps client-side via `min`/`max`. If the indexer is ever split into two separate Envio projects, this is the file that gets a second `EnvioClient` argument and runs two queries instead of one — no other code change required. +At infrastructure boundaries where external data enters as `string` (DTOs from Envio), cast with `as HexString` inside mappers — the runtime check happens in the primitive's factory. Serialize back out with `.toHexString()`. -Infrastructure adapters query this data via Envio's GraphQL/SQL API. Additional data comes from: -- ENS resolution (name, avatar) -- On-chain RPC reads (real-time token balance, current voting power) -- Token price feeds (USD conversion) +## Conventions -## Mapper function naming +| Rule | Why it matters | +|------|----------------| +| One public class per file; filename matches class name | Grep-ability; ddd-core-ts module-discovery assumptions | +| Named exports only | No default exports anywhere | +| `as const` over TypeScript `enum` | | +| No `I` prefix on interfaces (`MemberStore`, not `IMemberStore`) | | +| All dependencies constructor-injected; no singletons | | +| Domain objects validate via Zod inside `create()` | Invalid objects cannot exist | +| Value objects are immutable; copy-and-modify for state changes | | +| Use cases expose a single `execute()` and a `code` string matching the class name | `handleRequest` dispatch | +| Store interfaces live in domain; implementations in infrastructure | | +| Mappers co-located with the infrastructure code that uses them | Not in `domain/` | +| Tests colocated next to source (`Member.ts` → `Member.test.ts`) | Not in a separate `test/` folder | +| 2-space indent; multi-line JSDoc only (`/**\n * ...\n */`) | Enforced by biome | -Every mapper module — controller-side and store-side — exposes its mapping conversion under one of two canonical names: +## Mappers (the trust boundary) -- `mapDTOToDomain(...)` for the DTO → domain direction. -- `mapDomainToDTO(...)` for the domain → DTO direction. +`EnvioClient.query` returns `unknown`. Stores never declare DTO interfaces inline and never pass a type argument to `query`. The store's job is orchestration (form variables, call client, combine results, wrap errors). Shape assertions and DTO → domain conversion happen exclusively in mappers. -These names match what `ddd-core-ts`'s `handleRequest` looks up on controller-side mapper modules, so using them everywhere also keeps the controller and store layers consistent. Auxiliary helpers within a mapper (private parsers, ID-extraction utilities, etc.) keep descriptive names — the rule applies to the conversion function the rest of the codebase calls. +Each mapper module owns: -## GraphQL response handling +1. Zod schemas describing the response, with TypeScript types derived via `z.infer<...>` — the schema is the single source of truth. +2. A conversion function named exactly **`mapDTOToDomain(raw: unknown)`** (DTO → domain) or **`mapDomainToDTO(...)`** (domain → DTO). These names are load-bearing — `handleRequest` looks them up on controller-side mapper modules by name, so the convention is non-negotiable everywhere. +3. A `.parse()` call as the first line. Parse failures become descriptive runtime errors when the indexer's response shape drifts. -Anything returned by `EnvioClient.query` is `unknown`. The store's job is orchestration — form variables, call the client, hand the raw response off, combine results across queries, wrap errors. Shape assertions and DTO → domain conversion happen in mappers, which are the single trust boundary for indexer data. +Auxiliary helpers within a mapper (private parsers, ID extractors) keep descriptive names — the rule applies to the conversion function the rest of the codebase calls. -Each query has a corresponding mapper module under the store's `maps/` directory. The mapper owns: +Reference: `src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts`. -1. Zod schemas describing the expected response shape, with TypeScript types derived from those schemas via `z.infer<...>` so the schema is the single source of truth. -2. A `mapDTOToDomain(raw: unknown)` function that calls `.parse()` on the raw input and returns the domain objects. The Zod parse failure becomes a descriptive runtime error if the indexer's response shape ever drifts. -3. DTO → domain mapping. When the mapper passes raw inputs into a domain factory (`Address.fromHexString`, `TokenVotingMember.create`, etc.), the factory's own validation is the final shape check on the values inside the response. +## Testing -Stores never declare DTO interfaces inline and never pass a type argument to `query`. Calling `await this.envio.query(QUERY, vars)` produces `unknown`; that goes straight into the mapper's `mapDTOToDomain` function. +- Unit-test domain logic and use cases (with mocked store interfaces); integration-test infrastructure adapters. +- Build test data with the domain factory methods (`MemberProfileTextRecord.create(...)`), not by hand-constructing objects — invalid shapes won't even compile. -## Testing +## Data sources -- Unit tests for domain logic (value objects, aggregates, pure functions) -- Unit tests for use cases with mocked store interfaces -- Integration tests for infrastructure adapters -- Test files colocated: `Member.ts` → `Member.test.ts` -- Use domain factory methods (`Member.create(...)`) to build test data +Primary: the Envio indexer (`aragon-indexer/`). See `aragon-indexer/AGNETS.md` for entity schemas — do not duplicate them here. +Secondary: ENS resolution (name, avatar), on-chain RPC reads, token price feeds. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From d5522bf6e60a77ebf4221a43408c94bfeea9db85 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 26 May 2026 19:17:55 +0100 Subject: [PATCH 13/33] Fix test --- test/AragonSubdomain.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 299e6f8..974aba7 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -93,15 +93,5 @@ describe('AragonSubdomain', () => { expect(response.success).toBe(false); }); - - it('returns a failed response when the subdomain targets .aragonx.eth', async () => { - const controller = buildController([]); - - const response = await controller.getMemberProfileTextRecords({ - subdomain: 'ea1.aragonx.eth', - }); - - expect(response.success).toBe(false); - }); }); }); From 292e834f64bc7d399e5eb968fcd46359f88cc514 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 26 May 2026 19:41:01 +0100 Subject: [PATCH 14/33] Improve readme --- README.md | 134 +++++++++++++++++++++++++++++------------------------- 1 file changed, 71 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index e298062..ca645c2 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,96 @@ # Aragon Subdomain -Business logic package for the Aragon governance platform. Encapsulates all domain logic, application use cases, and infrastructure adapters for governance membership, voting power, delegation, and permissions. +Shared business-logic package for the Aragon governance platform. It is the single home for everything a frontend consumer needs that *isn't* raw on-chain data — membership rules, voting-power math, delegation logic, permission checks, ENS profile enrichment. Paired with [`aragon-indexer`](../aragon-indexer) (which provides only deterministic, indexed on-chain state), it lets any frontend consumer stay thin. -Part of the broader [Envio migration plan](https://www.notion.so/aragonorg/Plan-POC-to-use-Envio-as-backend-32e6b18349dc8084a1d9f8886539c8ad) to replace the existing `app-backend` with Envio-based indexing and a shared business logic layer. +Today the package ships one capability — looking up the ENS text records attached to a member's `.aragon.eth` subdomain — with the rest of the surface area arriving as the Envio migration progresses. -## Purpose +> **About the name.** "Subdomain" here is the DDD sense — a bounded subdomain of the Aragon business domain — and it happens to overlap with the ENS `.aragon.eth` subdomains that the first use case reads. The package is not limited to ENS work; that's just what ships first. -Currently, business logic is fragmented between `app-backend` (Node.js backend) and `app` (Next.js frontend). This package consolidates all governance business logic into a single importable package that can be consumed by: +## How it fits in -- **Next.js BFF** (`app/src/app/api/`) — thin API routes that delegate to this package -- **Utility services** — cron jobs, storage services that execute application logic from here -- **Frontend** — domain rules (e.g., delegation validation) imported directly +``` +On-chain events → Envio (aragon-indexer) → aragon-subdomain → Frontend consumers +``` + +`aragon-indexer` indexes raw on-chain state. `aragon-subdomain` queries that indexed data, pulls non-deterministic data from other sources, applies domain rules, and returns clean DTOs. Consumers (the App Next.js BFF, an MCP server, future mobile app, etc) call into a single `AragonSubdomain` controller. ## Architecture -Built with Domain-Driven Design using [`ddd-core-ts`](https://github.com/asciiman/ddd-core-ts). Three layers with inward-pointing dependencies: +Built with Domain-Driven Design on top of [`ddd-core-ts`](https://github.com/asciiman/ddd-core-ts). Three layers, dependencies point inward: -| Layer | Purpose | Dependencies | -|-------|---------|--------------| -| **Domain** | Pure business logic, value objects, aggregates, store interfaces | None | -| **Use Cases** | Application logic, orchestrates domain objects and I/O | Domain only | -| **Infrastructure** | Adapters for Envio, ENS, RPC, token pricing | Domain + Use Cases | +| Layer | Purpose | Depends on | +|-------|---------|------------| +| **Domain** | Pure business logic — value objects, aggregates, store interfaces | Nothing | +| **Use Cases** | Application logic that orchestrates domain objects and I/O | Domain | +| **Infrastructure** | Adapters for Envio and other external systems; the public controller | Domain + Use Cases | -## Domain Model +Validation is done with [`zod`](https://zod.dev) inside domain value objects, and the Envio adapter talks to the indexer through [`graphql-request`](https://github.com/jasonkuhrt/graphql-request). -Based on the [Aragon Governance Membership Domain Model](https://www.figma.com/board/WTlB4By8MoKhh5BJ58dpVE/Aragon-Governance---Membership-Domain-Model). +## Quick start -### Domain Objects (planned) +Requires Node 24+ and pnpm 10+. -| Domain Area | Objects | Description | -|-------------|---------|-------------| -| **Member** | `Member`, `MemberMetrics`, `MemberFilter` | Core member identity and activity | -| **Membership** | `Membership`, `GovernanceType`, `GovernanceTypeResolver` | Plugin membership aggregation | -| **Voting Power** | `VotingPower`, `ERC20VotingPower`, `VEVotingPower` | VP calculation per governance type | -| **Delegation** | `Delegation`, `DelegationRules` | Delegation relationships and validation | -| **Lock** | `VELock`, `LockLifecycle`, `LockFilter` | VE lock positions and state machine | -| **Permission** | `ProposalCreationPermission`, `MembershipPermission` | Governance permission rules | +```bash +pnpm install +pnpm run build +pnpm run test +``` -### Use Cases (planned) +## Usage -| Use Case | Description | -|----------|-------------| -| `GetMembership` | List members for a plugin (routes by governance type) | -| `GetERC20Membership` | ERC20-specific: query delegates, enrich with metrics | -| `GetVEMembership` | VE-specific: calculate VP from locks, group by delegate | -| `GetMemberDetail` | Single member: governance data + metrics + balance | -| `GetMemberLocks` | VE lock history for a member | -| `CheckMemberExists` | Boolean membership check | -| `CheckProposalCreationPermission` | Can this member create a proposal? | -| `GetDaosByMember` | Cross-DAO membership lookup | +```ts +import { AragonSubdomain, EnvioClient } from '@aragon/aragon-subdomain'; -### Infrastructure Adapters (planned) +const envioClient = new EnvioClient({ /* ... */ }); +const aragon = AragonSubdomain.load(envioClient); -| Adapter | Purpose | -|---------|---------| -| Envio GraphQL/SQL client | Query indexed on-chain state | -| ENS resolver | Name resolution | -| On-chain balance/VP reader | Real-time token balance and voting power via RPC | -| Token price provider | USD pricing for token balances | +const records = await aragon.getMemberProfileTextRecords({ + subdomain: 'alice.aragon.eth', +}); +// → [{ key: 'avatar', value: 'ipfs://…' }, …] +// Returns [] if the subdomain is unknown, has no resolver, or has no records. +``` -## Data Flow +## Roadmap -``` -On-chain events → Envio (aragon-indexer/) → aragon-subdomain → Next.js BFF → Frontend -``` +This package is the target for the [Envio migration](https://www.notion.so/aragonorg/Plan-POC-to-use-Envio-as-backend-32e6b18349dc8084a1d9f8886539c8ad), which moves governance business logic out of `app-backend` and `app` and into a shared library. Upcoming areas, in rough order: -- **Envio** indexes raw on-chain state (delegations, locks, plugin installations) -- **aragon-subdomain** queries Envio data, applies business logic (VP calculation, membership aggregation, permission checks), enriches with external data (ENS, token prices) -- **Next.js BFF** provides thin API routes that call aragon-subdomain use cases -- **Frontend** renders the results +- **Membership** — list members for a plugin, routed by governance type (ERC20 vs. VE) +- **Voting power** — VP calculation per governance type, including VE locks +- **Delegation** — delegation relationships and validation rules +- **Permissions** — proposal-creation and membership permission checks +- **Member detail** — single-member views enriched with balances and ENS metadata -## Quick Start +Scope is tracked against the [Aragon Governance Membership Domain Model](https://www.figma.com/board/WTlB4By8MoKhh5BJ58dpVE/Aragon-Governance---Membership-Domain-Model). -```bash -pnpm install -pnpm run build -pnpm run test -``` +## Development + +### Publishing a snapshot release to npm + +Snapshot releases let you test unreleased changes from a branch on npm without cutting a real version. The flow uses [Changesets](https://github.com/changesets/changesets) and a manually-dispatched GitHub Actions workflow. + +1. **Add a changeset locally** describing the change: + + ```bash + pnpm changeset + ``` + + Pick the bump type and write a short summary. Commit the generated file under `.changeset/` and push your branch. + +2. **Run the snapshot workflow.** On GitHub, go to *Actions → Library Snapshot → Run workflow* and select your branch. The workflow builds the package, runs `pnpm changeset version --snapshot`, and publishes to npm under a per-run dist-tag (`snapshot-`). + +3. **Install the snapshot** in a consumer — the exact command also appears in the workflow run's summary: + + ```bash + pnpm add @aragon/aragon-subdomain@snapshot- + ``` + +Each run gets its own dist-tag, so multiple in-flight branches can publish snapshots in parallel without colliding. The workflow won't publish anything if no changesets are pending. -## Related Projects +## Related projects -| Package | Path | Role | -|---------|------|------| -| `aragon-indexer` | `/aragon-indexer` | Envio indexer for on-chain event data | -| `app-backend` | `/app-backend` | Legacy backend (being replaced) | -| `app` | `/app` | Next.js frontend | -| `ve-governance-indexer` | `/ve-governance-indexer` | Production VE governance indexer (reference) | +| Package | Path | Relationship | +|---------|------|--------------| +| `aragon-indexer` | `/aragon-indexer` | Upstream — Envio indexer this package queries | +| `app` | `/app` | Consumer — Next.js frontend and BFF | +| `app-backend` | `/app-backend` | Consumer being replaced as logic moves here | From 6638426d8287d9e9a40a8c2ee26892f849b91ff8 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 17:10:49 +0100 Subject: [PATCH 15/33] Filtered out old text versions and empty records on indexer --- .../MemberProfileResolver.test.ts | 96 ------------------- .../member-profile/MemberProfileResolver.ts | 67 ------------- .../member-profile/MemberProfileStore.ts | 3 +- .../EnvioMemberProfileStore.test.ts | 5 +- .../EnvioMemberProfileStore.ts | 5 +- .../maps/MemberProfileTextRecordMap.test.ts | 40 +++++--- .../maps/MemberProfileTextRecordMap.ts | 36 ++----- 7 files changed, 40 insertions(+), 212 deletions(-) delete mode 100644 src/domain/member-profile/MemberProfileResolver.test.ts delete mode 100644 src/domain/member-profile/MemberProfileResolver.ts diff --git a/src/domain/member-profile/MemberProfileResolver.test.ts b/src/domain/member-profile/MemberProfileResolver.test.ts deleted file mode 100644 index 285186c..0000000 --- a/src/domain/member-profile/MemberProfileResolver.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { MemberProfileResolver } from './MemberProfileResolver'; - -describe('MemberProfileResolver', () => { - it('returns entries whose version matches the resolver version', () => { - const resolver = MemberProfileResolver.create({ - version: '0', - entries: [ - { key: 'avatar', value: 'ipfs://x', version: '0' }, - { key: 'url', value: 'https://aragon.org', version: '0' }, - ], - }); - - const live = resolver.liveTextRecords(); - expect(live).toHaveLength(2); - expect(live[0].key).toBe('avatar'); - expect(live[0].value).toBe('ipfs://x'); - expect(live[1].key).toBe('url'); - expect(live[1].value).toBe('https://aragon.org'); - }); - - it('drops entries superseded by a VersionChanged bump', () => { - // Resolver bumped to v1; the v0 row is still in storage but is no - // longer live. - const resolver = MemberProfileResolver.create({ - version: '1', - entries: [ - { key: 'avatar', value: 'stale', version: '0' }, - { key: 'avatar', value: 'fresh', version: '1' }, - ], - }); - - const live = resolver.liveTextRecords(); - expect(live).toHaveLength(1); - expect(live[0].value).toBe('fresh'); - }); - - it('drops cleared entries (value === null)', () => { - const resolver = MemberProfileResolver.create({ - version: '0', - entries: [ - { key: 'avatar', value: null, version: '0' }, - { key: 'url', value: 'https://aragon.org', version: '0' }, - ], - }); - - const live = resolver.liveTextRecords(); - expect(live).toHaveLength(1); - expect(live[0].key).toBe('url'); - }); - - it('preserves empty-string values (cleared is null, not empty string)', () => { - const resolver = MemberProfileResolver.create({ - version: '0', - entries: [{ key: 'bio', value: '', version: '0' }], - }); - - const live = resolver.liveTextRecords(); - expect(live).toHaveLength(1); - expect(live[0].value).toBe(''); - }); - - it('returns [] when there are no entries', () => { - const resolver = MemberProfileResolver.create({ - version: '0', - entries: [], - }); - - expect(resolver.liveTextRecords()).toEqual([]); - }); - - it('exposes the current version', () => { - const resolver = MemberProfileResolver.create({ - version: '7', - entries: [], - }); - expect(resolver.version).toBe('7'); - }); - - it('rejects entries with an empty key', () => { - expect(() => - MemberProfileResolver.create({ - version: '0', - entries: [{ key: '', value: 'x', version: '0' }], - }), - ).toThrow(); - }); - - it('rejects an empty resolver version', () => { - expect(() => - MemberProfileResolver.create({ - version: '', - entries: [], - }), - ).toThrow(); - }); -}); diff --git a/src/domain/member-profile/MemberProfileResolver.ts b/src/domain/member-profile/MemberProfileResolver.ts deleted file mode 100644 index 12a8681..0000000 --- a/src/domain/member-profile/MemberProfileResolver.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ValueObject } from 'ddd-core-ts'; -import { z } from 'zod'; -import { MemberProfileTextRecord } from './MemberProfileTextRecord'; - -const MemberProfileResolverEntrySchema = z.object({ - key: z.string().min(1, 'entry key must not be empty'), - value: z.string().nullable(), - version: z.string().min(1, 'entry version must not be empty'), -}); - -const MemberProfileResolverPropsSchema = z.object({ - version: z.string().min(1, 'resolver version must not be empty'), - entries: z.array(MemberProfileResolverEntrySchema).readonly(), -}); - -type MemberProfileResolverProps = z.output< - typeof MemberProfileResolverPropsSchema ->; - -/** - * The ENS Public Resolver attached to a member's - * subdomain. Owns the resolver's current version plus the candidate - * text-record entries the indexer has seen for it. - * - * Liveness rule (encoded in `liveTextRecords`): - * 1. an entry with `value === null` has been cleared on-chain and is - * not a live record; - * 2. an entry whose `version` differs from the resolver's current - * `version` was invalidated by a later `VersionChanged` event and - * is not a live record. - * - * This rule mirrors the on-chain `VersionChanged` semantic of the - * canonical ENS Public Resolver: bumping the version logically clears - * every prior record without touching individual rows. - */ -export class MemberProfileResolver extends ValueObject { - get version(): string { - return this.props.version; - } - - /** - * Returns the text records that are currently live on this resolver. - * Drops cleared rows (`value === null`) and rows superseded by a - * later `VersionChanged` bump. - */ - liveTextRecords(): MemberProfileTextRecord[] { - const live: MemberProfileTextRecord[] = []; - for (const entry of this.props.entries) { - if (entry.value === null) continue; - if (entry.version !== this.props.version) continue; - live.push( - MemberProfileTextRecord.create({ - key: entry.key, - value: entry.value, - }), - ); - } - return live; - } - - static create( - props: z.input, - ): MemberProfileResolver { - const validated = MemberProfileResolverPropsSchema.parse(props); - return new MemberProfileResolver(validated); - } -} diff --git a/src/domain/member-profile/MemberProfileStore.ts b/src/domain/member-profile/MemberProfileStore.ts index de48a86..ef2d555 100644 --- a/src/domain/member-profile/MemberProfileStore.ts +++ b/src/domain/member-profile/MemberProfileStore.ts @@ -8,8 +8,7 @@ export interface MemberProfileStore { * Returns an empty array when: * - the subdomain is unknown to the indexer * - the subdomain exists but has no resolver attached yet - * - the resolver has no current text records (all cleared or - * superseded by a `VersionChanged` bump) + * - the resolver has no text records */ findTextRecordsBySubdomain( subdomain: MemberProfileAragonName, diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts index 53295db..3d14d3f 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.test.ts @@ -35,10 +35,9 @@ describe('EnvioMemberProfileStore', () => { Domain: [ { resolver: { - version: '0', texts: [ - { key: 'avatar', value: 'ipfs://x', version: '0' }, - { key: 'url', value: 'https://aragon.org', version: '0' }, + { key: 'avatar', value: 'ipfs://x' }, + { key: 'url', value: 'https://aragon.org' }, ], }, }, diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts index 5b8366d..739f082 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore.ts @@ -12,11 +12,9 @@ const FIND_TEXT_RECORDS_QUERY = ` query FindMemberProfileTextRecords($name: String!) { Domain(where: { name: { _eq: $name } }, limit: 1) { resolver { - version texts { key value - version } } } @@ -33,8 +31,7 @@ export class EnvioMemberProfileStore implements MemberProfileStore { const raw = await this.envio.query(FIND_TEXT_RECORDS_QUERY, { name: subdomain.toString(), }); - const resolver = MemberProfileTextRecordMap.mapDTOToDomain(raw); - return resolver?.liveTextRecords() ?? []; + return MemberProfileTextRecordMap.mapDTOToDomain(raw); } catch (cause) { throw new Error('Error querying member profile from Envio', { cause }); } diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts index 8cfe26a..d696c75 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.test.ts @@ -6,32 +6,46 @@ describe('mapDTOToDomain', () => { expect(() => mapDTOToDomain(null)).toThrow(); }); - it('returns null when the indexer has no Domain row for this name', () => { - expect(mapDTOToDomain({ Domain: [] })).toBeNull(); + it('returns [] when the indexer has no Domain row for this name', () => { + expect(mapDTOToDomain({ Domain: [] })).toEqual([]); }); - it('returns null when the Domain exists but has no resolver yet', () => { - expect(mapDTOToDomain({ Domain: [{ resolver: null }] })).toBeNull(); + it('returns [] when the Domain exists but has no resolver yet', () => { + expect(mapDTOToDomain({ Domain: [{ resolver: null }] })).toEqual([]); }); - it('builds a MemberProfileResolver carrying the resolver version + entries', () => { - const resolver = mapDTOToDomain({ + it('maps each row in the response to a MemberProfileTextRecord', () => { + const records = mapDTOToDomain({ Domain: [ { resolver: { - version: '0', texts: [ - { key: 'avatar', value: 'ipfs://x', version: '0' }, - { key: 'url', value: 'https://aragon.org', version: '0' }, + { key: 'avatar', value: 'ipfs://x' }, + { key: 'url', value: 'https://aragon.org' }, ], }, }, ], }); - expect(resolver).not.toBeNull(); - expect(resolver?.version).toBe('0'); - const live = resolver?.liveTextRecords() ?? []; - expect(live.map((r) => r.key)).toEqual(['avatar', 'url']); + expect(records).toHaveLength(2); + expect(records[0].key).toBe('avatar'); + expect(records[0].value).toBe('ipfs://x'); + expect(records[1].key).toBe('url'); + expect(records[1].value).toBe('https://aragon.org'); + }); + + it('propagates MemberProfileTextRecord validation errors (e.g. empty key)', () => { + expect(() => + mapDTOToDomain({ + Domain: [ + { + resolver: { + texts: [{ key: '', value: 'x' }], + }, + }, + ], + }), + ).toThrow(); }); }); diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts index b4d24f5..8654e98 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts @@ -1,22 +1,17 @@ import { z } from 'zod'; -import { MemberProfileResolver } from '@/domain/member-profile/MemberProfileResolver'; +import { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; const TextRecordRowSchema = z.object({ key: z.string(), - value: z.string().nullable(), - version: z.string(), + value: z.string(), }); const ResolverSchema = z.object({ - version: z.string(), texts: z.array(TextRecordRowSchema), }); /** - * Shape of the `FindMemberProfileTextRecords` GraphQL response. The - * query selects up to one Domain by name; `Domain` is `[]` when no - * such row exists and `resolver` is null when the subdomain has no - * resolver assigned yet. + * Shape of the `FindMemberProfileTextRecords` GraphQL response. */ const FindMemberProfileTextRecordsResponseSchema = z.object({ Domain: z @@ -33,28 +28,15 @@ export type FindMemberProfileTextRecordsResponse = z.infer< typeof FindMemberProfileTextRecordsResponseSchema >; -/** - * Trust boundary for the indexer's `FindMemberProfileTextRecords` - * response. - * - * Returns `null` when the subdomain is unknown to the indexer or - * exists but has no resolver assigned yet. - */ -export function mapDTOToDomain(raw: unknown): MemberProfileResolver | null { +export function mapDTOToDomain(raw: unknown): MemberProfileTextRecord[] { const parsed = FindMemberProfileTextRecordsResponseSchema.parse(raw); const domain = parsed.Domain[0]; + if (!domain?.resolver) return []; - // No row for this name on the indexer. - if (!domain) return null; - // Domain exists but `setResolver` has not been called for it yet. - if (!domain.resolver) return null; - - return MemberProfileResolver.create({ - version: domain.resolver.version, - entries: domain.resolver.texts.map((text) => ({ + return domain.resolver.texts.map((text) => + MemberProfileTextRecord.create({ key: text.key, value: text.value, - version: text.version, - })), - }); + }), + ); } From 8ded8a6e463a2673168b65043d871e2dca188436 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 17:47:20 +0100 Subject: [PATCH 16/33] Fix code review comments --- package.json | 1 + pnpm-lock.yaml | 142 ++++++++++++++++++ .../MemberProfileAragonName.test.ts | Bin 2520 -> 3478 bytes .../member-profile/MemberProfileAragonName.ts | 11 +- src/domain/primitives/blockchain/Address.ts | 9 +- src/domain/primitives/math/HexNumber.ts | 14 +- src/domain/primitives/units/Ether.test.ts | 15 ++ src/domain/primitives/units/Ether.ts | 21 +++ src/domain/primitives/units/Gwei.test.ts | 5 - src/domain/primitives/units/Gwei.ts | 5 - src/domain/primitives/units/Wei.test.ts | 17 +++ src/domain/primitives/units/Wei.ts | 21 +++ 12 files changed, 230 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index f57a9ad..3781896 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "graphql": "16.13.2", "graphql-request": "7.4.0", "js-sha3": "0.9.3", + "viem": "2.51.3", "zod": "3.23.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6e20ab..e29827b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: js-sha3: specifier: 0.9.3 version: 0.9.3 + viem: + specifier: ^2.51.3 + version: 2.51.3(typescript@5.4.5)(zod@3.23.0) zod: specifier: 3.23.0 version: 3.23.0 @@ -54,6 +57,9 @@ importers: packages: + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -535,6 +541,18 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -689,6 +707,15 @@ packages: cpu: [x64] os: [win32] + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} + + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -736,6 +763,17 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -915,6 +953,9 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -1037,6 +1078,11 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -1162,6 +1208,14 @@ packages: outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + ox@0.14.25: + resolution: {integrity: sha512-8DoibKtxE8yw63Y2jjMhlbjaURev6WCx4QR4MWLusl2/qIaeTzMJMBIYIDl1KOF45+8H1Ur6eLTdPlUoO8PlRw==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -1457,6 +1511,14 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + viem@2.51.3: + resolution: {integrity: sha512-DA4EbrsvatzzLo6MwcWWiv6kI6dIr3I9HH9B6qsJaClN/s0AjIDUz5RIxl+VmGrovIUCcIvG8744yuGH7d37zw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1542,11 +1604,25 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + zod@3.23.0: resolution: {integrity: sha512-OFLT+LTocvabn6q76BTwVB0hExEBS0IduTr3cqZyMqEDbOnYmcU+y0tUAYbND4uwclpBGi4I4UUBGzylWpjLGA==} snapshots: + '@adraffy/ens-normalize@1.11.1': {} + '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -1961,6 +2037,14 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2051,6 +2135,19 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + '@types/estree@1.0.8': {} '@types/node@12.20.55': {} @@ -2117,6 +2214,11 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + abitype@1.2.3(typescript@5.4.5)(zod@3.23.0): + optionalDependencies: + typescript: 5.4.5 + zod: 3.23.0 + acorn@8.16.0: {} ansi-colors@4.1.3: {} @@ -2300,6 +2402,8 @@ snapshots: dependencies: '@types/estree': 1.0.8 + eventemitter3@5.0.1: {} + expect-type@1.3.0: {} extendable-error@0.1.7: {} @@ -2416,6 +2520,10 @@ snapshots: isexe@2.0.0: {} + isows@1.0.7(ws@8.20.1): + dependencies: + ws: 8.20.1 + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -2534,6 +2642,21 @@ snapshots: outdent@0.5.0: {} + ox@0.14.25(typescript@5.4.5)(zod@3.23.0): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.4.5)(zod@3.23.0) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - zod + p-filter@2.1.0: dependencies: p-map: 2.1.0 @@ -2801,6 +2924,23 @@ snapshots: universalify@0.1.2: {} + viem@2.51.3(typescript@5.4.5)(zod@3.23.0): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.4.5)(zod@3.23.0) + isows: 1.0.7(ws@8.20.1) + ox: 0.14.25(typescript@5.4.5)(zod@3.23.0) + ws: 8.20.1 + optionalDependencies: + typescript: 5.4.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + vite-node@2.1.9(@types/node@20.8.8): dependencies: cac: 6.7.14 @@ -2891,4 +3031,6 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + ws@8.20.1: {} + zod@3.23.0: {} diff --git a/src/domain/member-profile/MemberProfileAragonName.test.ts b/src/domain/member-profile/MemberProfileAragonName.test.ts index 9b489e4b415a555aee42df60cc6fb9f32ae8d70d..131bf3b3e112273737da3a168f6aed3665ed254f 100644 GIT binary patch delta 756 zcmZvay>8S%6oo0m5@=Bx!q1h3Hy*R4uWaYh z_2x*`zp-BZSXo_6=@^rNla_K1teF;0@H7s(uY@PfVRmMglp-1Zi>Lhs!Pzo^?tX9l z^vO8lP#hH@S3)9~6fTqkaY2lE6^v#EOhPX_NugRmH9^7X(iAP~I!}T*SL6E)_1|z) zV`EBOrI(33C-xIkA`i(R>k4ygl0xp{B||NymET2u;H18=IHKx7=l { public static create(props: AddressProps): Address { const validatedProps = AddressPropsSchema.parse(props); - const { addressValue } = validatedProps; - const byteLength = addressValue.getByteLength(); - assert( - byteLength === 20, - `addressValue must be 20 bytes. Got ${byteLength}`, - ); - return new Address(props); + return new Address(validatedProps); } /** diff --git a/src/domain/primitives/math/HexNumber.ts b/src/domain/primitives/math/HexNumber.ts index 5b2226f..ac55916 100644 --- a/src/domain/primitives/math/HexNumber.ts +++ b/src/domain/primitives/math/HexNumber.ts @@ -1,14 +1,10 @@ import { ValueObject } from 'ddd-core-ts'; import { z } from 'zod'; -import { assertHexString, type HexString } from '../validation/ZodHexString'; +import { zExtended } from '../validation'; +import type { HexString } from '../validation/ZodHexString'; const HexNumberPropsSchema = z.object({ - hexNumberValue: z - .string() - .refine( - (value) => typeof value === 'string' && /^0x[0-9a-fA-F]*$/.test(value), - 'hexNumberValue must be a 0x-prefixed hex string and not undefined', - ), + hexNumberValue: zExtended.hexString() }); type HexNumberProps = z.infer; @@ -53,10 +49,6 @@ export class HexNumber extends ValueObject { public static create(props: HexNumberProps): HexNumber { const validatedProps = HexNumberPropsSchema.parse(props); - assertHexString( - validatedProps.hexNumberValue, - 'hexNumberValue must be a 0x-prefixed hex string', - ); return new HexNumber(validatedProps); } diff --git a/src/domain/primitives/units/Ether.test.ts b/src/domain/primitives/units/Ether.test.ts index e44e889..d909778 100644 --- a/src/domain/primitives/units/Ether.test.ts +++ b/src/domain/primitives/units/Ether.test.ts @@ -29,6 +29,21 @@ describe('Ether', () => { expect(token.toBigNumber()).toEqual(value); }); + it('adds another ether', () => { + const value1 = new BigNumber(1.23); + const token1 = Ether.create(value1); + const value2 = new BigNumber(2.77); + const token2 = Ether.create(value2); + expect(token1.plus(token2).toBigNumber().toNumber()).toEqual(4); + }); + + it('multiplies a bignumber', () => { + const value1 = new BigNumber(1.5); + const token1 = Ether.create(value1); + const value2 = new BigNumber(4); + expect(token1.times(value2).toBigNumber().toNumber()).toEqual(6); + }); + it('toEther() returns the same instance (identity conversion)', () => { const token = Ether.create(new BigNumber(1.23)); expect(token.toEther()).toBe(token); diff --git a/src/domain/primitives/units/Ether.ts b/src/domain/primitives/units/Ether.ts index e6c48bd..8fced2a 100644 --- a/src/domain/primitives/units/Ether.ts +++ b/src/domain/primitives/units/Ether.ts @@ -31,6 +31,27 @@ export class Ether extends ValueObject implements EVMUnit { return this; } + /** + * Adds this value to another value. + * @param other The other value to add to. + */ + public plus(other: Ether): Ether { + const thisValue = this.props.etherValue; + const otherValue = other.toBigNumber(); + const totalValue = thisValue.plus(otherValue); + return Ether.create(totalValue); + } + + /** + * Multiplies our value in ether with a big number. Returns the value in Ether. + * @param bigNumber The big number to multiply our value with. + */ + public times(bigNumber: BigNumber): Ether { + const thisValue = this.props.etherValue; + const totalValue = thisValue.times(bigNumber); + return Ether.create(totalValue); + } + public static create(etherValue: BigNumber): Ether { return new Ether({ etherValue }); } diff --git a/src/domain/primitives/units/Gwei.test.ts b/src/domain/primitives/units/Gwei.test.ts index bd50704..c4be801 100644 --- a/src/domain/primitives/units/Gwei.test.ts +++ b/src/domain/primitives/units/Gwei.test.ts @@ -48,9 +48,4 @@ describe('Gwei', () => { const token = Gwei.create(new BigNumber(1_230_000_000)); expect(token.toGwei()).toBe(token); }); - - it('deserialize() parses a decimal string', () => { - const token = Gwei.deserialize('1230000000'); - expect(token.toBigNumber().toNumber()).toEqual(1_230_000_000); - }); }); diff --git a/src/domain/primitives/units/Gwei.ts b/src/domain/primitives/units/Gwei.ts index 0ef33f4..77dcfef 100644 --- a/src/domain/primitives/units/Gwei.ts +++ b/src/domain/primitives/units/Gwei.ts @@ -55,9 +55,4 @@ export class Gwei extends ValueObject implements EVMUnit { public static create(gweiValue: BigNumber): Gwei { return new Gwei({ gweiValue }); } - - public static deserialize(gweiValue: string): Gwei { - const gweiValueBN = new BigNumber(gweiValue); - return Gwei.create(gweiValueBN); - } } diff --git a/src/domain/primitives/units/Wei.test.ts b/src/domain/primitives/units/Wei.test.ts index b048034..a6b2c8f 100644 --- a/src/domain/primitives/units/Wei.test.ts +++ b/src/domain/primitives/units/Wei.test.ts @@ -27,6 +27,23 @@ describe('Wei', () => { expect(token.toBigNumber()).toEqual(value); }); + it('adds another wei', () => { + const value1 = new BigNumber('1230000000000000000'); + const token1 = Wei.create(value1); + const value2 = new BigNumber(321); + const token2 = Wei.create(value2); + expect(token1.plus(token2).toBigNumber().toString()).toEqual( + '1230000000000000321', + ); + }); + + it('multiplies a bignumber', () => { + const value1 = new BigNumber(321); + const token1 = Wei.create(value1); + const value2 = new BigNumber(123); + expect(token1.times(value2).toBigNumber().toNumber()).toEqual(39_483); + }); + it('toWei() returns the same instance (identity conversion)', () => { const token = Wei.create(new BigNumber('1230000000000000000')); expect(token.toWei()).toBe(token); diff --git a/src/domain/primitives/units/Wei.ts b/src/domain/primitives/units/Wei.ts index f83eae6..0b8deeb 100644 --- a/src/domain/primitives/units/Wei.ts +++ b/src/domain/primitives/units/Wei.ts @@ -34,6 +34,27 @@ export class Wei extends ValueObject implements EVMUnit { return Ether.create(etherValue); } + /** + * Adds this value to another value. + * @param other The other value to add to. + */ + public plus(other: Wei): Wei { + const thisValue = this.props.weiValue; + const otherValue = other.toBigNumber(); + const totalValue = thisValue.plus(otherValue); + return Wei.create(totalValue); + } + + /** + * Multiplies our value in wei with a big number. Returns the value in Wei. + * @param bigNumber The big number to multiply our value with. + */ + public times(bigNumber: BigNumber): Wei { + const thisValue = this.props.weiValue; + const totalValue = thisValue.times(bigNumber); + return Wei.create(totalValue); + } + public static create(weiValue: BigNumber): Wei { return new Wei({ weiValue }); } From 27377d54530babbf781bc607cb13323ebc48349d Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 19:04:52 +0100 Subject: [PATCH 17/33] Fix pnpm lock --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e29827b..9188520 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: specifier: 0.9.3 version: 0.9.3 viem: - specifier: ^2.51.3 + specifier: 2.51.3 version: 2.51.3(typescript@5.4.5)(zod@3.23.0) zod: specifier: 3.23.0 From f8c5bd92602105cdd08fd8b8120a5ee87d29fec8 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 20:33:13 +0100 Subject: [PATCH 18/33] Lint --- src/domain/primitives/math/HexNumber.ts | 2 +- .../maps/MemberProfileTextRecordMap.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/primitives/math/HexNumber.ts b/src/domain/primitives/math/HexNumber.ts index ac55916..88b24b4 100644 --- a/src/domain/primitives/math/HexNumber.ts +++ b/src/domain/primitives/math/HexNumber.ts @@ -4,7 +4,7 @@ import { zExtended } from '../validation'; import type { HexString } from '../validation/ZodHexString'; const HexNumberPropsSchema = z.object({ - hexNumberValue: zExtended.hexString() + hexNumberValue: zExtended.hexString(), }); type HexNumberProps = z.infer; diff --git a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts index 8654e98..0400cdb 100644 --- a/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts +++ b/src/infrastructure/stores/EnvioMemberProfileStore/maps/MemberProfileTextRecordMap.ts @@ -31,7 +31,9 @@ export type FindMemberProfileTextRecordsResponse = z.infer< export function mapDTOToDomain(raw: unknown): MemberProfileTextRecord[] { const parsed = FindMemberProfileTextRecordsResponseSchema.parse(raw); const domain = parsed.Domain[0]; - if (!domain?.resolver) return []; + if (!domain?.resolver) { + return []; + } return domain.resolver.texts.map((text) => MemberProfileTextRecord.create({ From d0a20a4fcab17bcf8a530a5fe7dae1c795f13356 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 20:46:08 +0100 Subject: [PATCH 19/33] Fixing type error --- src/domain/primitives/math/HexNumber.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/primitives/math/HexNumber.test.ts b/src/domain/primitives/math/HexNumber.test.ts index 0dcbfdd..79be000 100644 --- a/src/domain/primitives/math/HexNumber.test.ts +++ b/src/domain/primitives/math/HexNumber.test.ts @@ -45,7 +45,8 @@ describe('HexNumber', () => { it('throws an error when an invalid hex number is created', () => { const invalidProps = { hexNumberValue: 'invalid_hex_string', - }; + // biome-ignore lint/suspicious/noExplicitAny: testing illegal arg + } as any; expect(() => HexNumber.create(invalidProps)).toThrow(); }); From 18170a773be16269997723aa67c13904836aeb13 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Thu, 28 May 2026 20:48:21 +0100 Subject: [PATCH 20/33] Lint --- src/domain/primitives/math/HexNumber.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/primitives/math/HexNumber.test.ts b/src/domain/primitives/math/HexNumber.test.ts index 79be000..7c9207a 100644 --- a/src/domain/primitives/math/HexNumber.test.ts +++ b/src/domain/primitives/math/HexNumber.test.ts @@ -45,7 +45,7 @@ describe('HexNumber', () => { it('throws an error when an invalid hex number is created', () => { const invalidProps = { hexNumberValue: 'invalid_hex_string', - // biome-ignore lint/suspicious/noExplicitAny: testing illegal arg + // biome-ignore lint/suspicious/noExplicitAny: testing illegal arg } as any; expect(() => HexNumber.create(invalidProps)).toThrow(); From 853ff346d57d4c15574c85d55f43a83a720d1ca4 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:56:47 +0100 Subject: [PATCH 21/33] Align token voting namings --- .changeset/token-voting-membership-dto.md | 11 +++ .../MemberStore.ts} | 8 +- ...mber.test.ts => TokenVotingMember.test.ts} | 16 ++-- .../{Member.ts => TokenVotingMember.ts} | 17 ++-- .../TokenVotingMember.test.ts | 55 ----------- .../token-voting-member/TokenVotingMember.ts | 91 ------------------- src/index.ts | 5 + .../AragonController/AragonController.ts | 42 ++++----- .../AragonController/maps/domain/PageDTO.ts | 13 +++ .../maps/domain/PaginatedMemberMap.ts | 26 ------ ...p.test.ts => TokenVotingMemberMap.test.ts} | 10 +- .../{MemberMap.ts => TokenVotingMemberMap.ts} | 10 +- .../use-cases/GetERC20MembershipMap.test.ts | 32 ------- .../maps/use-cases/GetERC20MembershipMap.ts | 22 ----- .../GetTokenVotingMembershipMap.test.ts | 67 ++++++++++++++ .../use-cases/GetTokenVotingMembershipMap.ts | 41 +++++++++ .../EnvioMemberStore.test.ts} | 28 +++--- .../EnvioMemberStore.ts} | 9 +- .../maps/ReverseNameMap.ts | 0 .../maps/TokenVotingMemberMap.test.ts | 6 -- .../maps/TokenVotingMemberMap.ts | 13 +-- src/use-cases/GetERC20MembershipUseCase.ts | 59 ------------ ...> GetTokenVotingMembershipUseCase.test.ts} | 45 ++++----- .../GetTokenVotingMembershipUseCase.ts | 41 +++++++++ test/AragonSubdomain.test.ts | 6 +- vitest.config.ts | 3 +- 26 files changed, 276 insertions(+), 400 deletions(-) create mode 100644 .changeset/token-voting-membership-dto.md rename src/domain/{token-voting-member/TokenVotingMemberStore.ts => member/MemberStore.ts} (71%) rename src/domain/member/{Member.test.ts => TokenVotingMember.test.ts} (73%) rename src/domain/member/{Member.ts => TokenVotingMember.ts} (71%) delete mode 100644 src/domain/token-voting-member/TokenVotingMember.test.ts delete mode 100644 src/domain/token-voting-member/TokenVotingMember.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts delete mode 100644 src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts rename src/infrastructure/controllers/AragonController/maps/domain/{MemberMap.test.ts => TokenVotingMemberMap.test.ts} (80%) rename src/infrastructure/controllers/AragonController/maps/domain/{MemberMap.ts => TokenVotingMemberMap.ts} (67%) delete mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts delete mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts create mode 100644 src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts rename src/infrastructure/stores/{EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts => EnvioMemberStore/EnvioMemberStore.test.ts} (83%) rename src/infrastructure/stores/{EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts => EnvioMemberStore/EnvioMemberStore.ts} (93%) rename src/infrastructure/stores/{EnvioTokenVotingMemberStore => EnvioMemberStore}/maps/ReverseNameMap.ts (100%) rename src/infrastructure/stores/{EnvioTokenVotingMemberStore => EnvioMemberStore}/maps/TokenVotingMemberMap.test.ts (97%) rename src/infrastructure/stores/{EnvioTokenVotingMemberStore => EnvioMemberStore}/maps/TokenVotingMemberMap.ts (90%) delete mode 100644 src/use-cases/GetERC20MembershipUseCase.ts rename src/use-cases/{GetERC20MembershipUseCase.test.ts => GetTokenVotingMembershipUseCase.test.ts} (61%) create mode 100644 src/use-cases/GetTokenVotingMembershipUseCase.ts diff --git a/.changeset/token-voting-membership-dto.md b/.changeset/token-voting-membership-dto.md new file mode 100644 index 0000000..5a5707c --- /dev/null +++ b/.changeset/token-voting-membership-dto.md @@ -0,0 +1,11 @@ +--- +"@aragon/aragon-subdomain": minor +--- + +Adopt the token-voting membership DTO as the public list type and tidy the +membership domain. The controller exposes `getTokenVotingMembership` returning +a generic `PageDTO`. Public type exports (type-only): +`TokenVotingMemberDTO`, `PageDTO`, and `GetTokenVotingMembershipRequestDTO`. +Internally the membership domain collapses to a single `TokenVotingMember` +value object served by a `MemberStore` (`findTokenVotingMembers`). Behavior is +unchanged; runtime exports remain `AragonSubdomain` + `EnvioClient`. diff --git a/src/domain/token-voting-member/TokenVotingMemberStore.ts b/src/domain/member/MemberStore.ts similarity index 71% rename from src/domain/token-voting-member/TokenVotingMemberStore.ts rename to src/domain/member/MemberStore.ts index 285ce0f..bd292cc 100644 --- a/src/domain/token-voting-member/TokenVotingMemberStore.ts +++ b/src/domain/member/MemberStore.ts @@ -2,12 +2,16 @@ import type { Page } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { TokenVotingMember } from './TokenVotingMember'; -export interface TokenVotingMemberStore { +/** + * Member-fetching boundary for the membership domain. Holds the + * token-voting query today; other member queries can be added here. + */ +export interface MemberStore { /** * Finds members of a TokenVoting plugin scoped to its ERC20Votes token contract. * Returns a page of members sorted by voting power descending. */ - findMembersByPluginAndToken( + findTokenVotingMembers( pluginAddress: string, tokenContractAddress: string, page: PageRequest, diff --git a/src/domain/member/Member.test.ts b/src/domain/member/TokenVotingMember.test.ts similarity index 73% rename from src/domain/member/Member.test.ts rename to src/domain/member/TokenVotingMember.test.ts index 6cb267f..d147222 100644 --- a/src/domain/member/Member.test.ts +++ b/src/domain/member/TokenVotingMember.test.ts @@ -1,8 +1,8 @@ import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; -import { Member } from './Member'; +import { TokenVotingMember } from './TokenVotingMember'; -describe('Member', () => { +describe('TokenVotingMember', () => { const address = Address.fromHexString( '0x0123456789abcdef0123456789abcdef01234567', ); @@ -19,7 +19,7 @@ describe('Member', () => { }; it('creates a valid member', () => { - const member = Member.create(validProps); + const member = TokenVotingMember.create(validProps); expect(member.address.equals(address)).toBe(true); expect(member.ens).toBe('alice.eth'); expect(member.votingPower.isZero).toBe(false); @@ -28,19 +28,19 @@ describe('Member', () => { }); it('allows null ens', () => { - const member = Member.create({ ...validProps, ens: null }); + const member = TokenVotingMember.create({ ...validProps, ens: null }); expect(member.ens).toBeNull(); }); it('structural equality', () => { - const a = Member.create(validProps); - const b = Member.create(validProps); + const a = TokenVotingMember.create(validProps); + const b = TokenVotingMember.create(validProps); expect(a.equals(b)).toBe(true); }); it('structural inequality when votingPower differs', () => { - const a = Member.create(validProps); - const b = Member.create({ + const a = TokenVotingMember.create(validProps); + const b = TokenVotingMember.create({ ...validProps, votingPower: VotingPower.zero(), }); diff --git a/src/domain/member/Member.ts b/src/domain/member/TokenVotingMember.ts similarity index 71% rename from src/domain/member/Member.ts rename to src/domain/member/TokenVotingMember.ts index 09aae6f..05217e0 100644 --- a/src/domain/member/Member.ts +++ b/src/domain/member/TokenVotingMember.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; -const MemberPropsSchema = z.object({ +const TokenVotingMemberPropsSchema = z.object({ address: z.instanceof(Address), ens: z.string().nullable(), votingPower: z.instanceof(VotingPower), @@ -12,9 +12,14 @@ const MemberPropsSchema = z.object({ delegationCount: z.number().int().nonnegative(), }); -type MemberProps = z.infer; +type TokenVotingMemberProps = z.infer; -export class Member extends ValueObject { +/** + * A member of a TokenVoting plugin: aggregated voting power on the + * governance token plus per-plugin activity timestamps (unix seconds), + * sourced from the indexer. + */ +export class TokenVotingMember extends ValueObject { /** * The member's wallet address. */ @@ -58,8 +63,8 @@ export class Member extends ValueObject { return this.props.delegationCount; } - static create(props: MemberProps): Member { - const validated = MemberPropsSchema.parse(props); - return new Member(validated); + static create(props: TokenVotingMemberProps): TokenVotingMember { + const validated = TokenVotingMemberPropsSchema.parse(props); + return new TokenVotingMember(validated); } } diff --git a/src/domain/token-voting-member/TokenVotingMember.test.ts b/src/domain/token-voting-member/TokenVotingMember.test.ts deleted file mode 100644 index a091aa1..0000000 --- a/src/domain/token-voting-member/TokenVotingMember.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Address } from '@/domain/primitives'; -import { VotingPower } from '@/domain/voting-power/VotingPower'; -import { TokenVotingMember } from './TokenVotingMember'; - -describe('TokenVotingMember', () => { - const address = Address.fromHexString( - '0x0123456789abcdef0123456789abcdef01234567', - ); - const pluginAddress = Address.fromHexString( - '0x1111111111111111111111111111111111111111', - ); - const tokenContractAddress = Address.fromHexString( - '0xabcdef0123456789abcdef0123456789abcdef01', - ); - const votingPower = VotingPower.fromBigInt(1000000000000000000n); - const firstActivityTimestamp = 1705320000; - const lastActivityTimestamp = 1718872200; - - const validProps = { - address, - pluginAddress, - tokenContractAddress, - votingPower, - ens: null, - firstActivityTimestamp, - lastActivityTimestamp, - delegationCount: 7, - }; - - it('creates a valid member', () => { - const member = TokenVotingMember.create(validProps); - - expect(member.address.equals(address)).toBe(true); - expect(member.pluginAddress.equals(pluginAddress)).toBe(true); - expect(member.tokenContractAddress.equals(tokenContractAddress)).toBe(true); - expect(member.votingPower.equals(votingPower)).toBe(true); - expect(member.ens).toBeNull(); - expect(member.firstActivityTimestamp).toBe(firstActivityTimestamp); - expect(member.lastActivityTimestamp).toBe(lastActivityTimestamp); - }); - - it('carries an ENS name when present', () => { - const member = TokenVotingMember.create({ - ...validProps, - ens: 'alice.eth', - }); - expect(member.ens).toBe('alice.eth'); - }); - - it('structural equality', () => { - const a = TokenVotingMember.create(validProps); - const b = TokenVotingMember.create(validProps); - expect(a.equals(b)).toBe(true); - }); -}); diff --git a/src/domain/token-voting-member/TokenVotingMember.ts b/src/domain/token-voting-member/TokenVotingMember.ts deleted file mode 100644 index e845bf4..0000000 --- a/src/domain/token-voting-member/TokenVotingMember.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { ValueObject } from 'ddd-core-ts'; -import { z } from 'zod'; -import { Address } from '@/domain/primitives'; -import { VotingPower } from '@/domain/voting-power/VotingPower'; - -const TokenVotingMemberPropsSchema = z.object({ - address: z.instanceof(Address), - pluginAddress: z.instanceof(Address), - tokenContractAddress: z.instanceof(Address), - votingPower: z.instanceof(VotingPower), - ens: z.string().nullable(), - firstActivityTimestamp: z.number().int().nonnegative(), - lastActivityTimestamp: z.number().int().nonnegative(), - delegationCount: z.number().int().nonnegative(), -}); - -type TokenVotingMemberProps = z.infer; - -/** - * A member of a TokenVoting plugin: combines on-chain delegation state - * (voting power on the token contract) with per-plugin activity - * timestamps (delegation, votes, proposal creations) sourced from the - * indexer. Timestamps are unix seconds. - */ -export class TokenVotingMember extends ValueObject { - /** - * The member's wallet address. - */ - get address(): Address { - return this.props.address; - } - - /** - * The TokenVoting plugin this member belongs to. - */ - get pluginAddress(): Address { - return this.props.pluginAddress; - } - - /** - * The ERC20Votes token contract this member holds voting power on. - */ - get tokenContractAddress(): Address { - return this.props.tokenContractAddress; - } - - /** - * The member's aggregated voting power on the token contract. - */ - get votingPower(): VotingPower { - return this.props.votingPower; - } - - /** - * The member's primary ENS name, or null if none is registered. - * Sourced from the indexer's ReverseName entity. - */ - get ens(): string | null { - return this.props.ens; - } - - /** - * Unix-seconds timestamp of the member's first observed activity for - * this plugin. - */ - get firstActivityTimestamp(): number { - return this.props.firstActivityTimestamp; - } - - /** - * Unix-seconds timestamp of the member's most recent observed - * activity for this plugin. - */ - get lastActivityTimestamp(): number { - return this.props.lastActivityTimestamp; - } - - /** - * Number of distinct accounts currently delegating their voting - * power to this member on the underlying ERC20Votes token (counts - * self-delegation). Sourced from the indexer. - */ - get delegationCount(): number { - return this.props.delegationCount; - } - - static create(props: TokenVotingMemberProps): TokenVotingMember { - const validated = TokenVotingMemberPropsSchema.parse(props); - return new TokenVotingMember(validated); - } -} diff --git a/src/index.ts b/src/index.ts index c28d983..785e329 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,8 @@ export { AragonController as AragonSubdomain } from './infrastructure/controller // Infrastructure export { EnvioClient } from './infrastructure/stores/EnvioClient'; + +// Public DTOs + request types (type-only) +export type { PageDTO } from './infrastructure/controllers/AragonController/maps/domain/PageDTO'; +export type { TokenVotingMemberDTO } from './infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap'; +export type { GetTokenVotingMembershipRequestDTO } from './infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap'; diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index c09fb72..e3339f8 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -1,30 +1,30 @@ import type { HandlerDefinition } from 'ddd-core-ts'; import { handleRequest } from 'ddd-core-ts'; -import type { Member } from '@/domain/member/Member'; +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; import type { Page } from '@/domain/primitives/pagination/Page'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberProfileStore } from '@/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore'; -import { EnvioTokenVotingMemberStore } from '@/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore'; -import type { GetERC20MembershipUseCaseProps } from '@/use-cases/GetERC20MembershipUseCase'; -import { GetERC20MembershipUseCase } from '@/use-cases/GetERC20MembershipUseCase'; +import { EnvioMemberStore } from '@/infrastructure/stores/EnvioMemberStore/EnvioMemberStore'; +import { GetTokenVotingMembershipUseCase } from '@/use-cases/GetTokenVotingMembershipUseCase'; +import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import { GetMemberProfileTextRecordsUseCase } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import type { MemberProfileTextRecordDTO } from './maps/domain/MemberProfileTextRecordMap'; import * as MemberProfileTextRecordMap from './maps/domain/MemberProfileTextRecordMap'; -import type { PaginatedMemberDTO } from './maps/domain/PaginatedMemberMap'; -import * as PaginatedMemberMap from './maps/domain/PaginatedMemberMap'; -import type { GetERC20MembershipRequestDTO } from './maps/use-cases/GetERC20MembershipMap'; -import * as GetERC20MembershipMap from './maps/use-cases/GetERC20MembershipMap'; +import type { PageDTO } from './maps/domain/PageDTO'; +import type { TokenVotingMemberDTO } from './maps/domain/TokenVotingMemberMap'; +import * as GetTokenVotingMembershipMap from './maps/use-cases/GetTokenVotingMembershipMap'; +import type { GetTokenVotingMembershipRequestDTO } from './maps/use-cases/GetTokenVotingMembershipMap'; import type { GetMemberProfileTextRecordsRequestDTO } from './maps/use-cases/GetMemberProfileTextRecordsMap'; import * as GetMemberProfileTextRecordsMap from './maps/use-cases/GetMemberProfileTextRecordsMap'; interface HandlersRecord { - getERC20Membership: HandlerDefinition< - GetERC20MembershipRequestDTO, - GetERC20MembershipUseCaseProps, - Page, - PaginatedMemberDTO + getTokenVotingMembership: HandlerDefinition< + GetTokenVotingMembershipRequestDTO, + GetTokenVotingMembershipUseCaseProps, + Page, + PageDTO >; getMemberProfileTextRecords: HandlerDefinition< GetMemberProfileTextRecordsRequestDTO, @@ -44,8 +44,8 @@ export class AragonController { * Initializes the `AragonSubdomain`. */ static load(envioClient: EnvioClient): AragonController { - const memberStore = new EnvioTokenVotingMemberStore(envioClient); - const getERC20MembershipUseCase = new GetERC20MembershipUseCase( + const memberStore = new EnvioMemberStore(envioClient); + const getTokenVotingMembershipUseCase = new GetTokenVotingMembershipUseCase( memberStore, ); @@ -54,10 +54,10 @@ export class AragonController { new GetMemberProfileTextRecordsUseCase(memberProfileStore); const handlers: HandlersRecord = { - getERC20Membership: { - requestMapper: GetERC20MembershipMap, - responseMapper: PaginatedMemberMap, - useCaseExecutor: getERC20MembershipUseCase, + getTokenVotingMembership: { + requestMapper: GetTokenVotingMembershipMap, + responseMapper: GetTokenVotingMembershipMap, + useCaseExecutor: getTokenVotingMembershipUseCase, }, getMemberProfileTextRecords: { requestMapper: GetMemberProfileTextRecordsMap, @@ -74,8 +74,8 @@ export class AragonController { * to a specific ERC20Votes token contract and ordered by current * voting power descending. */ - public getMembership(dto: GetERC20MembershipRequestDTO) { - return handleRequest(this.handlers.getERC20Membership, dto); + public getTokenVotingMembership(dto: GetTokenVotingMembershipRequestDTO) { + return handleRequest(this.handlers.getTokenVotingMembership, dto); } /** diff --git a/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts b/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts new file mode 100644 index 0000000..99902ea --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts @@ -0,0 +1,13 @@ +/** + * Generic paginated response DTO — the serialized counterpart of the + * domain `Page` primitive. Reused by any paginated use-case result. + */ +export interface PageDTO { + metadata: { + page: number; + pageSize: number; + totalPages: number; + totalRecords: number; + }; + data: T[]; +} \ No newline at end of file diff --git a/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts deleted file mode 100644 index f84c49a..0000000 --- a/src/infrastructure/controllers/AragonController/maps/domain/PaginatedMemberMap.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Member } from '@/domain/member/Member'; -import type { Page } from '@/domain/primitives/pagination/Page'; -import type { MemberDTO } from './MemberMap'; -import * as MemberMap from './MemberMap'; - -export interface PaginatedMemberDTO { - metadata: { - page: number; - pageSize: number; - totalPages: number; - totalRecords: number; - }; - data: MemberDTO[]; -} - -export function mapDomainToDTO(result: Page): PaginatedMemberDTO { - return { - metadata: { - page: result.page, - pageSize: result.pageSize, - totalPages: result.totalPages, - totalRecords: result.totalRecords, - }, - data: result.items.map(MemberMap.mapDomainToDTO), - }; -} diff --git a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts similarity index 80% rename from src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts rename to src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts index 182044a..8cc5c7e 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts @@ -1,17 +1,17 @@ -import { Member } from '@/domain/member/Member'; +import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; -import { mapDomainToDTO } from './MemberMap'; +import { mapDomainToDTO } from './TokenVotingMemberMap'; -describe('MemberMap.mapDomainToDTO', () => { +describe('TokenVotingMemberMap.mapDomainToDTO', () => { const address = Address.fromHexString( '0x0123456789abcdef0123456789abcdef01234567', ); const buildMember = ( - overrides: Partial[0]> = {}, + overrides: Partial[0]> = {}, ) => - Member.create({ + TokenVotingMember.create({ address, ens: 'alice.eth', votingPower: VotingPower.fromBigInt(5000000000000000000n), diff --git a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts similarity index 67% rename from src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts rename to src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts index 4dfe5fb..0883f4d 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/MemberMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts @@ -1,12 +1,14 @@ -import type { Member } from '@/domain/member/Member'; +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; /** - * DTO matching the frontend's ITokenMember interface. + * Token-voting membership DTO — the type the frontend's token member list + * renders from, regardless of whether the data came from the subdomain or + * the legacy backend. * * Activity fields are unix-seconds timestamps. The frontend can render * them directly without resolving block numbers via RPC. */ -export interface MemberDTO { +export interface TokenVotingMemberDTO { address: string; ens: string | null; votingPower: string | null; @@ -17,7 +19,7 @@ export interface MemberDTO { }; } -export function mapDomainToDTO(member: Member): MemberDTO { +export function mapDomainToDTO(member: TokenVotingMember): TokenVotingMemberDTO { const first = member.firstActivityTimestamp; const last = member.lastActivityTimestamp; return { diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts deleted file mode 100644 index 8d9d461..0000000 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { mapDTOToDomain } from './GetERC20MembershipMap'; - -describe('GetERC20MembershipMap.mapDTOToDomain', () => { - const pluginAddress = '0x1111111111111111111111111111111111111111'; - const tokenContractAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - - it('passes explicit page and pageSize through to the PageRequest', () => { - const result = mapDTOToDomain({ - pluginAddress, - tokenContractAddress, - page: 3, - pageSize: 50, - }); - - expect(result.page.page).toBe(3); - expect(result.page.pageSize).toBe(50); - }); - - it('defaults page to 1 and pageSize to 20 when omitted', () => { - const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); - - expect(result.page.page).toBe(1); - expect(result.page.pageSize).toBe(20); - }); - - it('forwards plugin and token addresses unchanged', () => { - const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); - - expect(result.pluginAddress).toBe(pluginAddress); - expect(result.tokenContractAddress).toBe(tokenContractAddress); - }); -}); diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts deleted file mode 100644 index c58e373..0000000 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetERC20MembershipMap.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import type { GetERC20MembershipUseCaseProps } from '@/use-cases/GetERC20MembershipUseCase'; - -export interface GetERC20MembershipRequestDTO { - pluginAddress: string; - tokenContractAddress: string; - page?: number; - pageSize?: number; -} - -export function mapDTOToDomain( - dto: GetERC20MembershipRequestDTO, -): GetERC20MembershipUseCaseProps { - return { - pluginAddress: dto.pluginAddress, - tokenContractAddress: dto.tokenContractAddress, - page: PageRequest.create({ - page: dto.page ?? 1, - pageSize: dto.pageSize ?? 20, - }), - }; -} diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts new file mode 100644 index 0000000..0dfe3c7 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts @@ -0,0 +1,67 @@ +import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { Address } from '@/domain/primitives'; +import { createPage } from '@/domain/primitives/pagination/Page'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { mapDomainToDTO, mapDTOToDomain } from './GetTokenVotingMembershipMap'; + +describe('GetTokenVotingMembershipMap', () => { + const pluginAddress = '0x1111111111111111111111111111111111111111'; + const tokenContractAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + + describe('mapDTOToDomain', () => { + it('passes explicit page and pageSize through to the PageRequest', () => { + const result = mapDTOToDomain({ + pluginAddress, + tokenContractAddress, + page: 3, + pageSize: 50, + }); + + expect(result.page.page).toBe(3); + expect(result.page.pageSize).toBe(50); + }); + + it('defaults page to 1 and pageSize to 20 when omitted', () => { + const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); + + expect(result.page.page).toBe(1); + expect(result.page.pageSize).toBe(20); + }); + + it('forwards plugin and token addresses unchanged', () => { + const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); + + expect(result.pluginAddress).toBe(pluginAddress); + expect(result.tokenContractAddress).toBe(tokenContractAddress); + }); + }); + + describe('mapDomainToDTO', () => { + it('maps a domain page to a PageDTO of TokenVotingMemberDTO', () => { + const member = TokenVotingMember.create({ + address: Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ), + ens: 'alice.eth', + votingPower: VotingPower.fromBigInt(5000000000000000000n), + firstActivityTimestamp: 1705320000, + lastActivityTimestamp: 1718872200, + delegationCount: 3, + }); + const page = createPage([member], 1, 20, 1); + + const dto = mapDomainToDTO(page); + + expect(dto.metadata).toEqual({ + page: 1, + pageSize: 20, + totalPages: 1, + totalRecords: 1, + }); + expect(dto.data).toHaveLength(1); + expect(dto.data[0].ens).toBe('alice.eth'); + expect(dto.data[0].votingPower).toBe('5000000000000000000'); + expect(dto.data[0].metrics.delegationCount).toBe(3); + }); + }); +}); \ No newline at end of file diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts new file mode 100644 index 0000000..80c7c36 --- /dev/null +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts @@ -0,0 +1,41 @@ +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import type { Page } from '@/domain/primitives/pagination/Page'; +import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; +import type { PageDTO } from '../domain/PageDTO'; +import * as TokenVotingMemberMap from '../domain/TokenVotingMemberMap'; +import type { TokenVotingMemberDTO } from '../domain/TokenVotingMemberMap'; + +export interface GetTokenVotingMembershipRequestDTO { + pluginAddress: string; + tokenContractAddress: string; + page?: number; + pageSize?: number; +} + +export function mapDTOToDomain( + dto: GetTokenVotingMembershipRequestDTO, +): GetTokenVotingMembershipUseCaseProps { + return { + pluginAddress: dto.pluginAddress, + tokenContractAddress: dto.tokenContractAddress, + page: PageRequest.create({ + page: dto.page ?? 1, + pageSize: dto.pageSize ?? 20, + }), + }; +} + +export function mapDomainToDTO( + result: Page, +): PageDTO { + return { + metadata: { + page: result.page, + pageSize: result.pageSize, + totalPages: result.totalPages, + totalRecords: result.totalRecords, + }, + data: result.items.map(TokenVotingMemberMap.mapDomainToDTO), + }; +} \ No newline at end of file diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts similarity index 83% rename from src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts rename to src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts index d6db8f7..0159f0d 100644 --- a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts @@ -1,6 +1,6 @@ import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; -import { EnvioTokenVotingMemberStore } from './EnvioTokenVotingMemberStore'; +import { EnvioMemberStore } from './EnvioMemberStore'; const PLUGIN = '0x1111111111111111111111111111111111111111'; const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; @@ -54,7 +54,7 @@ const buildDelegate = ( lastVotingPowerChangeTimestamp: '1700000100', }); -describe('EnvioTokenVotingMemberStore', () => { +describe('EnvioMemberStore', () => { const page = PageRequest.create({ page: 1, pageSize: 20 }); it('attaches ENS names from the indexer ReverseName entity to each member', async () => { @@ -74,9 +74,9 @@ describe('EnvioTokenVotingMemberStore', () => { ], }, ]); - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); - const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); expect(result.items.map((m) => m.ens)).toEqual(['alice.eth', null]); @@ -105,9 +105,9 @@ describe('EnvioTokenVotingMemberStore', () => { ], }, ]); - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); - const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); expect(result.items[0].ens).toBe('alice-default.eth'); }); @@ -125,9 +125,9 @@ describe('EnvioTokenVotingMemberStore', () => { ], }, ]); - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); - const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); expect(result.items[0].ens).toBe('alice-legacy.eth'); }); @@ -140,9 +140,9 @@ describe('EnvioTokenVotingMemberStore', () => { MemberMetrics: [], }, ]); - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); - const result = await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); expect(result.items).toHaveLength(0); expect(calls).toHaveLength(1); @@ -158,9 +158,9 @@ describe('EnvioTokenVotingMemberStore', () => { }, { ReverseName: [] }, ]); - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); - await store.findMembersByPluginAndToken(PLUGIN, TOKEN, page); + await store.findTokenVotingMembers(PLUGIN, TOKEN, page); expect(calls[1].variables).toEqual({ addresses: [ALICE] }); }); @@ -169,10 +169,10 @@ describe('EnvioTokenVotingMemberStore', () => { const envio = { query: vi.fn().mockRejectedValue(new Error('graphql exploded')), } as unknown as EnvioClient; - const store = new EnvioTokenVotingMemberStore(envio); + const store = new EnvioMemberStore(envio); await expect( - store.findMembersByPluginAndToken(PLUGIN, TOKEN, page), + store.findTokenVotingMembers(PLUGIN, TOKEN, page), ).rejects.toThrow('Error querying members from Envio'); }); }); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts similarity index 93% rename from src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts rename to src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts index ec7e248..16cccab 100644 --- a/src/infrastructure/stores/EnvioTokenVotingMemberStore/EnvioTokenVotingMemberStore.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts @@ -1,8 +1,8 @@ import type { Page } from '@/domain/primitives/pagination/Page'; import { createPage } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import type { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; -import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; +import type { MemberStore } from '@/domain/member/MemberStore'; +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import * as ReverseNameMap from './maps/ReverseNameMap'; import * as TokenVotingMemberMap from './maps/TokenVotingMemberMap'; @@ -92,10 +92,10 @@ const FIND_REVERSE_NAMES_QUERY = ` } `; -export class EnvioTokenVotingMemberStore implements TokenVotingMemberStore { +export class EnvioMemberStore implements MemberStore { constructor(private readonly envio: EnvioClient) {} - public async findMembersByPluginAndToken( + public async findTokenVotingMembers( pluginAddress: string, tokenContractAddress: string, request: PageRequest, @@ -127,7 +127,6 @@ export class EnvioTokenVotingMemberStore implements TokenVotingMemberStore { delegate, metricsByMember.get(addressLower), ensByAddress.get(addressLower) ?? null, - pluginAddressLower, ); }); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts similarity index 100% rename from src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/ReverseNameMap.ts rename to src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts similarity index 97% rename from src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts rename to src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts index a013286..f78382a 100644 --- a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts @@ -62,7 +62,6 @@ describe('mapDTOToDomain', () => { buildDelegate(), buildMetrics(), null, - PLUGIN, ); expect(member.firstActivityTimestamp).toBe(1650000000); }); @@ -72,7 +71,6 @@ describe('mapDTOToDomain', () => { buildDelegate({ firstVotingPowerChangeTimestamp: '1600000000' }), buildMetrics({ firstActivityTimestamp: '1650000000' }), null, - PLUGIN, ); expect(member.firstActivityTimestamp).toBe(1600000000); }); @@ -83,7 +81,6 @@ describe('mapDTOToDomain', () => { buildDelegate(), buildMetrics(), null, - PLUGIN, ); expect(member.lastActivityTimestamp).toBe(1750000000); }); @@ -93,7 +90,6 @@ describe('mapDTOToDomain', () => { buildDelegate({ lastVotingPowerChangeTimestamp: '1800000000' }), buildMetrics({ lastActivityTimestamp: '1750000000' }), null, - PLUGIN, ); expect(member.lastActivityTimestamp).toBe(1800000000); }); @@ -108,7 +104,6 @@ describe('mapDTOToDomain', () => { }), undefined, null, - PLUGIN, ); expect(member.firstActivityTimestamp).toBe(0); expect(member.lastActivityTimestamp).toBe(0); @@ -119,7 +114,6 @@ describe('mapDTOToDomain', () => { buildDelegate(), undefined, 'alice.eth', - PLUGIN, ); expect(member.ens).toBe('alice.eth'); }); diff --git a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts similarity index 90% rename from src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts rename to src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts index 1b29ff2..1bca12f 100644 --- a/src/infrastructure/stores/EnvioTokenVotingMemberStore/maps/TokenVotingMemberMap.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { Address, assertHexString } from '@/domain/primitives'; -import { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; +import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import { VotingPower } from '@/domain/voting-power/VotingPower'; /** @@ -99,20 +99,11 @@ export function mapDTOToDomain( delegate: ERC20VotesDelegateDTO, metrics: MemberMetricsDTO | undefined, ens: string | null, - pluginAddress: string, ): TokenVotingMember { assertHexString( delegate.delegateAddress, 'delegateAddress must be a 0x-prefixed hex string', ); - assertHexString( - delegate.tokenContractAddress, - 'tokenContractAddress must be a 0x-prefixed hex string', - ); - assertHexString( - pluginAddress, - 'pluginAddress must be a 0x-prefixed hex string', - ); // Activity is the merge of two signals: // - MemberMetrics: VoteCast / ProposalCreated activity in this plugin @@ -131,8 +122,6 @@ export function mapDTOToDomain( return TokenVotingMember.create({ address: Address.fromHexString(delegate.delegateAddress), - pluginAddress: Address.fromHexString(pluginAddress), - tokenContractAddress: Address.fromHexString(delegate.tokenContractAddress), votingPower: VotingPower.fromBigInt(BigInt(delegate.votingPower)), ens, firstActivityTimestamp, diff --git a/src/use-cases/GetERC20MembershipUseCase.ts b/src/use-cases/GetERC20MembershipUseCase.ts deleted file mode 100644 index 9089083..0000000 --- a/src/use-cases/GetERC20MembershipUseCase.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { UseCase } from 'ddd-core-ts'; -import { Member } from '@/domain/member/Member'; -import type { Page } from '@/domain/primitives/pagination/Page'; -import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import type { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; -import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; - -export interface GetERC20MembershipUseCaseProps { - pluginAddress: string; - tokenContractAddress: string; - page: PageRequest; -} - -/** - * Returns a page of members of an Aragon TokenVoting plugin, scoped - * to a specific ERC20Votes token contract and ordered by current - * voting power descending. - */ -export class GetERC20MembershipUseCase - implements UseCase> -{ - public readonly code = 'GetERC20MembershipUseCase'; - - constructor(private readonly memberStore: TokenVotingMemberStore) {} - - public async execute( - props: GetERC20MembershipUseCaseProps, - ): Promise> { - try { - const membersPage = await this.memberStore.findMembersByPluginAndToken( - props.pluginAddress, - props.tokenContractAddress, - props.page, - ); - - const members = membersPage.items.map((member) => this.toMember(member)); - - return { ...membersPage, items: members }; - } catch (cause) { - throw new Error('Error while getting ERC20 membership', { cause }); - } - } - - /** - * Converts a raw indexer member into a Member domain object. - * ENS identity is already attached by the store via the indexer's - * ReverseName entity, so no further enrichment is required here. - */ - private toMember(member: TokenVotingMember): Member { - return Member.create({ - address: member.address, - ens: member.ens, - votingPower: member.votingPower, - firstActivityTimestamp: member.firstActivityTimestamp, - lastActivityTimestamp: member.lastActivityTimestamp, - delegationCount: member.delegationCount, - }); - } -} diff --git a/src/use-cases/GetERC20MembershipUseCase.test.ts b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts similarity index 61% rename from src/use-cases/GetERC20MembershipUseCase.test.ts rename to src/use-cases/GetTokenVotingMembershipUseCase.test.ts index 76fe269..d5a1229 100644 --- a/src/use-cases/GetERC20MembershipUseCase.test.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts @@ -1,29 +1,22 @@ +import type { MemberStore } from '@/domain/member/MemberStore'; +import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import { Address } from '@/domain/primitives'; import { createPage } from '@/domain/primitives/pagination/Page'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import { TokenVotingMember } from '@/domain/token-voting-member/TokenVotingMember'; -import type { TokenVotingMemberStore } from '@/domain/token-voting-member/TokenVotingMemberStore'; import { VotingPower } from '@/domain/voting-power/VotingPower'; -import { GetERC20MembershipUseCase } from './GetERC20MembershipUseCase'; +import { GetTokenVotingMembershipUseCase } from './GetTokenVotingMembershipUseCase'; -describe('GetERC20MembershipUseCase', () => { +describe('GetTokenVotingMembershipUseCase', () => { const memberAddressValue = '0x1234567890abcdef1234567890abcdef12345678'; const memberAddress = Address.fromHexString(memberAddressValue); - const pluginAddressValue = '0x1111111111111111111111111111111111111111'; - const pluginAddressObj = Address.fromHexString(pluginAddressValue); - const tokenContractAddressValue = - '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; - const tokenContractAddressObj = Address.fromHexString( - tokenContractAddressValue, - ); + const pluginAddress = '0x1111111111111111111111111111111111111111'; + const tokenContractAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; const firstActivityTimestamp = 1705320000; const lastActivityTimestamp = 1718872200; const buildMember = (ens: string | null = null): TokenVotingMember => TokenVotingMember.create({ address: memberAddress, - pluginAddress: pluginAddressObj, - tokenContractAddress: tokenContractAddressObj, votingPower: VotingPower.fromBigInt(5000000000000000000n), ens, firstActivityTimestamp, @@ -33,18 +26,16 @@ describe('GetERC20MembershipUseCase', () => { const buildMockStore = ( member: TokenVotingMember = buildMember(), - ): TokenVotingMemberStore => ({ - findMembersByPluginAndToken: vi + ): MemberStore => ({ + findTokenVotingMembers: vi .fn() .mockResolvedValue(createPage([member], 1, 20, 1)), }); const page = PageRequest.create({ page: 1, pageSize: 20 }); - const pluginAddress = pluginAddressValue; - const tokenContractAddress = tokenContractAddressValue; it('returns a page of members', async () => { - const useCase = new GetERC20MembershipUseCase(buildMockStore()); + const useCase = new GetTokenVotingMembershipUseCase(buildMockStore()); const result = await useCase.execute({ pluginAddress, tokenContractAddress, @@ -62,10 +53,10 @@ describe('GetERC20MembershipUseCase', () => { it('passes plugin address, token address, and page to store', async () => { const store = buildMockStore(); - const useCase = new GetERC20MembershipUseCase(store); + const useCase = new GetTokenVotingMembershipUseCase(store); await useCase.execute({ pluginAddress, tokenContractAddress, page }); - expect(store.findMembersByPluginAndToken).toHaveBeenCalledWith( + expect(store.findTokenVotingMembers).toHaveBeenCalledWith( pluginAddress, tokenContractAddress, page, @@ -73,7 +64,7 @@ describe('GetERC20MembershipUseCase', () => { }); it('forwards the ENS name attached to each TokenVotingMember', async () => { - const useCase = new GetERC20MembershipUseCase( + const useCase = new GetTokenVotingMembershipUseCase( buildMockStore(buildMember('alice.eth')), ); const result = await useCase.execute({ @@ -86,15 +77,13 @@ describe('GetERC20MembershipUseCase', () => { }); it('wraps store errors', async () => { - const failingStore: TokenVotingMemberStore = { - findMembersByPluginAndToken: vi - .fn() - .mockRejectedValue(new Error('db down')), + const failingStore: MemberStore = { + findTokenVotingMembers: vi.fn().mockRejectedValue(new Error('db down')), }; - const useCase = new GetERC20MembershipUseCase(failingStore); + const useCase = new GetTokenVotingMembershipUseCase(failingStore); await expect( useCase.execute({ pluginAddress, tokenContractAddress, page }), - ).rejects.toThrow('Error while getting ERC20 membership'); + ).rejects.toThrow('Error while getting token-voting membership'); }); -}); +}); \ No newline at end of file diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.ts b/src/use-cases/GetTokenVotingMembershipUseCase.ts new file mode 100644 index 0000000..c69ec1e --- /dev/null +++ b/src/use-cases/GetTokenVotingMembershipUseCase.ts @@ -0,0 +1,41 @@ +import type { UseCase } from 'ddd-core-ts'; +import type { MemberStore } from '@/domain/member/MemberStore'; +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import type { Page } from '@/domain/primitives/pagination/Page'; +import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; + +export interface GetTokenVotingMembershipUseCaseProps { + pluginAddress: string; + tokenContractAddress: string; + page: PageRequest; +} + +/** + * Returns a page of members of an Aragon TokenVoting plugin, scoped + * to a specific ERC20Votes token contract and ordered by current + * voting power descending. + */ +export class GetTokenVotingMembershipUseCase + implements + UseCase> +{ + public readonly code = 'GetTokenVotingMembershipUseCase'; + + constructor(private readonly memberStore: MemberStore) {} + + public async execute( + props: GetTokenVotingMembershipUseCaseProps, + ): Promise> { + try { + return await this.memberStore.findTokenVotingMembers( + props.pluginAddress, + props.tokenContractAddress, + props.page, + ); + } catch (cause) { + throw new Error('Error while getting token-voting membership', { + cause, + }); + } + } +} diff --git a/test/AragonSubdomain.test.ts b/test/AragonSubdomain.test.ts index 5120bba..3d7fd58 100644 --- a/test/AragonSubdomain.test.ts +++ b/test/AragonSubdomain.test.ts @@ -25,7 +25,7 @@ function buildController(responses: unknown[]): AragonSubdomain { } describe('AragonSubdomain', () => { - describe('getMembership', () => { + describe('getTokenVotingMembership', () => { it('returns a paginated DTO of ERC20 members', async () => { const controller = buildController([ { @@ -55,14 +55,14 @@ describe('AragonSubdomain', () => { }, ]); - const response = await controller.getMembership({ + const response = await controller.getTokenVotingMembership({ pluginAddress: PLUGIN, tokenContractAddress: TOKEN, page: 1, pageSize: 15, }); - assert(response.success, 'expected getMembership to succeed'); + assert(response.success, 'expected getTokenVotingMembership to succeed'); expect(response.result.metadata).toEqual({ page: 1, pageSize: 15, diff --git a/vitest.config.ts b/vitest.config.ts index 1bd9f22..f42d840 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -23,7 +23,8 @@ export default defineConfig({ 'src/**/index.ts', 'src/domain/member-profile/MemberProfileStore.ts', 'src/domain/primitives/units/EVMUnit.ts', - 'src/domain/token-voting-member/TokenVotingMemberStore.ts', + 'src/domain/member/MemberStore.ts', + 'src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts', ], thresholds: { statements: 100, From b8d436f4234ecf48db630f12233b145cde4a9cf3 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:57:33 +0100 Subject: [PATCH 22/33] Lint --- .../AragonController/AragonController.ts | 8 ++++---- .../AragonController/maps/domain/PageDTO.ts | 2 +- .../maps/domain/TokenVotingMemberMap.ts | 4 +++- .../GetTokenVotingMembershipMap.test.ts | 2 +- .../use-cases/GetTokenVotingMembershipMap.ts | 4 ++-- .../EnvioMemberStore/EnvioMemberStore.ts | 4 ++-- .../maps/TokenVotingMemberMap.test.ts | 18 +++--------------- .../maps/TokenVotingMemberMap.ts | 2 +- .../GetTokenVotingMembershipUseCase.test.ts | 2 +- 9 files changed, 18 insertions(+), 28 deletions(-) diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index e3339f8..55542be 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -6,18 +6,18 @@ import type { Page } from '@/domain/primitives/pagination/Page'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberProfileStore } from '@/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore'; import { EnvioMemberStore } from '@/infrastructure/stores/EnvioMemberStore/EnvioMemberStore'; -import { GetTokenVotingMembershipUseCase } from '@/use-cases/GetTokenVotingMembershipUseCase'; -import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import { GetMemberProfileTextRecordsUseCase } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; +import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; +import { GetTokenVotingMembershipUseCase } from '@/use-cases/GetTokenVotingMembershipUseCase'; import type { MemberProfileTextRecordDTO } from './maps/domain/MemberProfileTextRecordMap'; import * as MemberProfileTextRecordMap from './maps/domain/MemberProfileTextRecordMap'; import type { PageDTO } from './maps/domain/PageDTO'; import type { TokenVotingMemberDTO } from './maps/domain/TokenVotingMemberMap'; -import * as GetTokenVotingMembershipMap from './maps/use-cases/GetTokenVotingMembershipMap'; -import type { GetTokenVotingMembershipRequestDTO } from './maps/use-cases/GetTokenVotingMembershipMap'; import type { GetMemberProfileTextRecordsRequestDTO } from './maps/use-cases/GetMemberProfileTextRecordsMap'; import * as GetMemberProfileTextRecordsMap from './maps/use-cases/GetMemberProfileTextRecordsMap'; +import type { GetTokenVotingMembershipRequestDTO } from './maps/use-cases/GetTokenVotingMembershipMap'; +import * as GetTokenVotingMembershipMap from './maps/use-cases/GetTokenVotingMembershipMap'; interface HandlersRecord { getTokenVotingMembership: HandlerDefinition< diff --git a/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts b/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts index 99902ea..c47d498 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts @@ -10,4 +10,4 @@ export interface PageDTO { totalRecords: number; }; data: T[]; -} \ No newline at end of file +} diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts index 0883f4d..5456bab 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts @@ -19,7 +19,9 @@ export interface TokenVotingMemberDTO { }; } -export function mapDomainToDTO(member: TokenVotingMember): TokenVotingMemberDTO { +export function mapDomainToDTO( + member: TokenVotingMember, +): TokenVotingMemberDTO { const first = member.firstActivityTimestamp; const last = member.lastActivityTimestamp; return { diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts index 0dfe3c7..88dbdf0 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts @@ -64,4 +64,4 @@ describe('GetTokenVotingMembershipMap', () => { expect(dto.data[0].metrics.delegationCount).toBe(3); }); }); -}); \ No newline at end of file +}); diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts index 80c7c36..ad2049a 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts @@ -3,8 +3,8 @@ import type { Page } from '@/domain/primitives/pagination/Page'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; import type { PageDTO } from '../domain/PageDTO'; -import * as TokenVotingMemberMap from '../domain/TokenVotingMemberMap'; import type { TokenVotingMemberDTO } from '../domain/TokenVotingMemberMap'; +import * as TokenVotingMemberMap from '../domain/TokenVotingMemberMap'; export interface GetTokenVotingMembershipRequestDTO { pluginAddress: string; @@ -38,4 +38,4 @@ export function mapDomainToDTO( }, data: result.items.map(TokenVotingMemberMap.mapDomainToDTO), }; -} \ No newline at end of file +} diff --git a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts index 16cccab..643cb98 100644 --- a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts @@ -1,8 +1,8 @@ +import type { MemberStore } from '@/domain/member/MemberStore'; +import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import type { Page } from '@/domain/primitives/pagination/Page'; import { createPage } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import type { MemberStore } from '@/domain/member/MemberStore'; -import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import * as ReverseNameMap from './maps/ReverseNameMap'; import * as TokenVotingMemberMap from './maps/TokenVotingMemberMap'; diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts index f78382a..3592aaf 100644 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts @@ -58,11 +58,7 @@ describe('parseFindMembersResponse', () => { describe('mapDTOToDomain', () => { it('takes the min of metrics first-activity and delegate first-VP-change when both are defined and the metrics value is smaller', () => { // metrics.firstActivityTimestamp (1650000000) < delegate.firstVotingPowerChangeTimestamp (1700000000) - const member = mapDTOToDomain( - buildDelegate(), - buildMetrics(), - null, - ); + const member = mapDTOToDomain(buildDelegate(), buildMetrics(), null); expect(member.firstActivityTimestamp).toBe(1650000000); }); @@ -77,11 +73,7 @@ describe('mapDTOToDomain', () => { it('takes the max of metrics last-activity and delegate last-VP-change when both are defined and the metrics value is larger', () => { // metrics.lastActivityTimestamp (1750000000) > delegate.lastVotingPowerChangeTimestamp (1700000100) - const member = mapDTOToDomain( - buildDelegate(), - buildMetrics(), - null, - ); + const member = mapDTOToDomain(buildDelegate(), buildMetrics(), null); expect(member.lastActivityTimestamp).toBe(1750000000); }); @@ -110,11 +102,7 @@ describe('mapDTOToDomain', () => { }); it('forwards the supplied ENS name to the domain object', () => { - const member = mapDTOToDomain( - buildDelegate(), - undefined, - 'alice.eth', - ); + const member = mapDTOToDomain(buildDelegate(), undefined, 'alice.eth'); expect(member.ens).toBe('alice.eth'); }); }); diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts index 1bca12f..0a3bd05 100644 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { Address, assertHexString } from '@/domain/primitives'; import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { Address, assertHexString } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; /** diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts index d5a1229..0006c03 100644 --- a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts @@ -86,4 +86,4 @@ describe('GetTokenVotingMembershipUseCase', () => { useCase.execute({ pluginAddress, tokenContractAddress, page }), ).rejects.toThrow('Error while getting token-voting membership'); }); -}); \ No newline at end of file +}); From 8bf47e106abc756e52e4ac4c1f0dd58878123690 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:23:50 +0100 Subject: [PATCH 23/33] Update changelog to aragon-domain --- .changeset/funky-streets-deny.md | 2 +- .changeset/token-voting-membership-dto.md | 2 +- CHANGELOG.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changeset/funky-streets-deny.md b/.changeset/funky-streets-deny.md index 6481253..e2bbc79 100644 --- a/.changeset/funky-streets-deny.md +++ b/.changeset/funky-streets-deny.md @@ -1,5 +1,5 @@ --- -"@aragon/aragon-subdomain": minor +"@aragon/aragon-domain": minor --- Implement ERC20 token voting membership use case diff --git a/.changeset/token-voting-membership-dto.md b/.changeset/token-voting-membership-dto.md index 5a5707c..281be90 100644 --- a/.changeset/token-voting-membership-dto.md +++ b/.changeset/token-voting-membership-dto.md @@ -1,5 +1,5 @@ --- -"@aragon/aragon-subdomain": minor +"@aragon/aragon-domain": minor --- Adopt the token-voting membership DTO as the public list type and tidy the diff --git a/CHANGELOG.md b/CHANGELOG.md index a8f00dd..c2e328b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Minor Changes -- [#15](https://github.com/aragon/aragon-domain/pull/15) [`0f63598`](https://github.com/aragon/aragon-domain/commit/0f63598d834be0e45a4008ad25af703f283cb4f0) Thanks [@tyhonchik](https://github.com/tyhonchik)! - Rename package from `@aragon/aragon-subdomain` to `@aragon/aragon-domain`. Public controller export is now `AragonDomain`. +- [#15](https://github.com/aragon/aragon-domain/pull/15) [`0f63598`](https://github.com/aragon/aragon-domain/commit/0f63598d834be0e45a4008ad25af703f283cb4f0) Thanks [@tyhonchik](https://github.com/tyhonchik)! - Rename package from `@aragon/aragon-domain` to `@aragon/aragon-domain`. Public controller export is now `AragonDomain`. ### Patch Changes @@ -14,8 +14,8 @@ ### Minor Changes -- [#3](https://github.com/aragon/aragon-subdomain/pull/3) [`bd9fff8`](https://github.com/aragon/aragon-subdomain/commit/bd9fff8beb08911fe4b796ef237211f7fb041f7d) Thanks [@asciiman](https://github.com/asciiman)! - Look up member profile ENS records +- [#3](https://github.com/aragon/aragon-domain/pull/3) [`bd9fff8`](https://github.com/aragon/aragon-domain/commit/bd9fff8beb08911fe4b796ef237211f7fb041f7d) Thanks [@asciiman](https://github.com/asciiman)! - Look up member profile ENS records ### Patch Changes -- [#1](https://github.com/aragon/aragon-subdomain/pull/1) [`11e3275`](https://github.com/aragon/aragon-subdomain/commit/11e3275bcdd0815509523f9410cbc5f004fa86e0) Thanks [@jjavieralv](https://github.com/jjavieralv)! - Adding CI flow for initial version +- [#1](https://github.com/aragon/aragon-domain/pull/1) [`11e3275`](https://github.com/aragon/aragon-domain/commit/11e3275bcdd0815509523f9410cbc5f004fa86e0) Thanks [@jjavieralv](https://github.com/jjavieralv)! - Adding CI flow for initial version From ad95b2445dae523c28b55e4ba2f52a251f64cb70 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:05:16 +0100 Subject: [PATCH 24/33] Integration tests --- test/AragonDomain.test.ts | 157 -------------------- test/getMemberProfileTextRecords.test.ts | 64 ++++++++ test/getTokenVotingMembership.test.ts | 119 +++++++++++++++ test/support/buildDomain.ts | 38 +++++ test/support/constants.ts | 14 ++ test/support/fixtures/memberProfile.ts | 44 ++++++ test/support/fixtures/tokenVotingMembers.ts | 69 +++++++++ 7 files changed, 348 insertions(+), 157 deletions(-) delete mode 100644 test/AragonDomain.test.ts create mode 100644 test/getMemberProfileTextRecords.test.ts create mode 100644 test/getTokenVotingMembership.test.ts create mode 100644 test/support/buildDomain.ts create mode 100644 test/support/constants.ts create mode 100644 test/support/fixtures/memberProfile.ts create mode 100644 test/support/fixtures/tokenVotingMembers.ts diff --git a/test/AragonDomain.test.ts b/test/AragonDomain.test.ts deleted file mode 100644 index 2f21b30..0000000 --- a/test/AragonDomain.test.ts +++ /dev/null @@ -1,157 +0,0 @@ -import assert from 'node:assert'; -import { AragonDomain, EnvioClient } from '../src'; - -const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; -const PLUGIN = '0x17a1688c56087ade762721180e1cc1e831c73719'; -const TOKEN = '0x0a830e9f2baa2ebaf8d33c0806283dea9c08952f'; -const DEFAULT_EVM_COIN_TYPE = '2147483648'; // 0x80000000 - -/** - * Builds an AragonDomain backed by an EnvioClient whose `query` - * method is replaced with a FIFO queue of canned responses. The - * underlying GraphQLClient is constructed but never used at the - * network layer. - */ -function buildController(responses: unknown[]): AragonDomain { - const envio = new EnvioClient('https://unused.example.invalid'); - const queue = [...responses]; - vi.spyOn(envio, 'query').mockImplementation(async () => { - if (queue.length === 0) { - throw new Error('EnvioClient.query called more times than expected'); - } - return queue.shift() as never; - }); - return AragonDomain.load(envio); -} - -describe('AragonDomain', () => { - describe('getTokenVotingMembership', () => { - it('returns a paginated DTO of ERC20 members', async () => { - const controller = buildController([ - { - ERC20VotesDelegate: [ - { - id: `1-${TOKEN}-${ALICE}`, - chainId: 1, - tokenContractAddress: TOKEN, - delegateAddress: ALICE, - votingPower: '5000000000000000000', - delegationCount: 2, - firstVotingPowerChangeTimestamp: '1700000000', - lastVotingPowerChangeTimestamp: '1700000100', - }, - ], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }, - { - ReverseName: [ - { - address: ALICE, - coinType: DEFAULT_EVM_COIN_TYPE, - name: 'alice.eth', - }, - ], - }, - ]); - - const response = await controller.getTokenVotingMembership({ - pluginAddress: PLUGIN, - tokenContractAddress: TOKEN, - page: 1, - pageSize: 15, - }); - - assert(response.success, 'expected getTokenVotingMembership to succeed'); - expect(response.result.metadata).toEqual({ - page: 1, - pageSize: 15, - totalPages: 1, - totalRecords: 1, - }); - expect(response.result.data).toHaveLength(1); - expect(response.result.data[0]).toEqual( - expect.objectContaining({ - ens: 'alice.eth', - votingPower: '5000000000000000000', - }), - ); - expect(response.result.data[0].address).toMatch(/^0x[0-9a-fA-F]{40}$/); - }); - }); - - describe('getMemberProfileTextRecords', () => { - it('returns the live text records as a DTO list', async () => { - const controller = buildController([ - { - Domain: [ - { - id: 'domain-id', - name: 'ea1.aragon.eth', - resolver: { - version: '0', - texts: [ - { key: 'avatar', value: 'ipfs://x', version: '0' }, - { - key: 'url', - value: 'https://aragon.org', - version: '0', - }, - ], - }, - }, - ], - }, - ]); - - const response = await controller.getMemberProfileTextRecords({ - subdomain: 'ea1.aragon.eth', - }); - - assert( - response.success, - 'expected getMemberProfileTextRecords to succeed', - ); - expect(response.result).toEqual([ - { key: 'avatar', value: 'ipfs://x' }, - { key: 'url', value: 'https://aragon.org' }, - ]); - }); - - it('returns [] when the subdomain is unknown to the indexer', async () => { - const controller = buildController([{ Domain: [] }]); - - const response = await controller.getMemberProfileTextRecords({ - subdomain: 'ea1.aragon.eth', - }); - - assert(response.success); - expect(response.result).toEqual([]); - }); - - it('returns [] when the subdomain has no resolver yet', async () => { - const controller = buildController([ - { - Domain: [{ id: 'domain-id', name: 'ea1.aragon.eth', resolver: null }], - }, - ]); - - const response = await controller.getMemberProfileTextRecords({ - subdomain: 'ea1.aragon.eth', - }); - - assert(response.success); - expect(response.result).toEqual([]); - }); - - it('returns a failed response when the subdomain is not under .aragon.eth', async () => { - const controller = buildController([]); - - const response = await controller.getMemberProfileTextRecords({ - subdomain: 'vitalik.eth', - }); - - expect(response.success).toBe(false); - }); - }); -}); diff --git a/test/getMemberProfileTextRecords.test.ts b/test/getMemberProfileTextRecords.test.ts new file mode 100644 index 0000000..7e10e0a --- /dev/null +++ b/test/getMemberProfileTextRecords.test.ts @@ -0,0 +1,64 @@ +import assert from 'node:assert'; +import { buildDomain } from './support/buildDomain'; +import { + domainResponse, + noResolverResponse, + unknownDomainResponse, +} from './support/fixtures/memberProfile'; + +const SUBDOMAIN = 'ea1.aragon.eth'; + +describe('AragonDomain.getMemberProfileTextRecords', () => { + it('returns the live text records as a DTO list', async () => { + const { domain } = buildDomain([ + domainResponse(SUBDOMAIN, [ + { key: 'avatar', value: 'ipfs://x' }, + { key: 'url', value: 'https://aragon.org' }, + ]), + ]); + + const response = await domain.getMemberProfileTextRecords({ + subdomain: SUBDOMAIN, + }); + + assert(response.success, 'expected getMemberProfileTextRecords to succeed'); + expect(response.result).toEqual([ + { key: 'avatar', value: 'ipfs://x' }, + { key: 'url', value: 'https://aragon.org' }, + ]); + }); + + it('returns [] when the subdomain is unknown to the indexer', async () => { + const { domain } = buildDomain([unknownDomainResponse()]); + + const response = await domain.getMemberProfileTextRecords({ + subdomain: SUBDOMAIN, + }); + + assert(response.success); + expect(response.result).toEqual([]); + }); + + it('returns [] when the subdomain has no resolver yet', async () => { + const { domain } = buildDomain([noResolverResponse(SUBDOMAIN)]); + + const response = await domain.getMemberProfileTextRecords({ + subdomain: SUBDOMAIN, + }); + + assert(response.success); + expect(response.result).toEqual([]); + }); + + it('returns a failed response when the subdomain is not under .aragon.eth', async () => { + // Rejected by the request mapper before any query is issued. + const { domain, query } = buildDomain([]); + + const response = await domain.getMemberProfileTextRecords({ + subdomain: 'vitalik.eth', + }); + + expect(response.success).toBe(false); + expect(query).not.toHaveBeenCalled(); + }); +}); diff --git a/test/getTokenVotingMembership.test.ts b/test/getTokenVotingMembership.test.ts new file mode 100644 index 0000000..3908979 --- /dev/null +++ b/test/getTokenVotingMembership.test.ts @@ -0,0 +1,119 @@ +import assert from 'node:assert'; +import { buildDomain } from './support/buildDomain'; +import { + ALICE, + BOB, + DEFAULT_EVM_COIN_TYPE, + PLUGIN, + TOKEN, +} from './support/constants'; +import { + delegate, + findMembersResponse, + reverseName, + reverseNamesResponse, +} from './support/fixtures/tokenVotingMembers'; + +describe('AragonDomain.getTokenVotingMembership', () => { + it('returns a paginated DTO of token-voting members', async () => { + const { domain } = buildDomain([ + findMembersResponse({ + delegates: [delegate(ALICE, { votingPower: '5000000000000000000' })], + }), + reverseNamesResponse([ + reverseName(ALICE, DEFAULT_EVM_COIN_TYPE, 'alice.eth'), + ]), + ]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 15, + }); + + assert(response.success, 'expected getTokenVotingMembership to succeed'); + expect(response.result.metadata).toEqual({ + page: 1, + pageSize: 15, + totalPages: 1, + totalRecords: 1, + }); + expect(response.result.data).toHaveLength(1); + expect(response.result.data[0]).toEqual( + expect.objectContaining({ + ens: 'alice.eth', + votingPower: '5000000000000000000', + }), + ); + expect(response.result.data[0].address).toMatch(/^0x[0-9a-fA-F]{40}$/); + }); + + it('attaches the ENS name from the indexer ReverseName entity', async () => { + const { domain } = buildDomain([ + findMembersResponse({ delegates: [delegate(ALICE), delegate(BOB)] }), + reverseNamesResponse([ + reverseName(ALICE, DEFAULT_EVM_COIN_TYPE, 'alice.eth'), + ]), + ]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 20, + }); + + assert(response.success); + expect(response.result.data.map((m) => m.ens)).toEqual(['alice.eth', null]); + }); + + it('reflects a larger chain-wide total in the pagination metadata', async () => { + const { domain } = buildDomain([ + findMembersResponse({ delegates: [delegate(ALICE)], totalRecords: 42 }), + reverseNamesResponse(), + ]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 20, + }); + + assert(response.success); + expect(response.result.metadata.totalRecords).toBe(42); + expect(response.result.metadata.totalPages).toBe(3); + }); + + it('skips the reverse-name query when the page is empty', async () => { + const { domain, query } = buildDomain([findMembersResponse()]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 20, + }); + + assert(response.success); + expect(response.result.data).toHaveLength(0); + // Only the members query ran; no second round-trip for ENS names. + expect(query).toHaveBeenCalledTimes(1); + }); + + it('returns a failed response when the indexer query errors', async () => { + // Empty queue → the store's first query throws, surfacing as a + // failed ResultOrError rather than a rejected promise. + const { domain } = buildDomain([]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 20, + }); + + expect(response.success).toBe(false); + }); +}); diff --git a/test/support/buildDomain.ts b/test/support/buildDomain.ts new file mode 100644 index 0000000..0e17464 --- /dev/null +++ b/test/support/buildDomain.ts @@ -0,0 +1,38 @@ +import { AragonDomain, EnvioClient } from '../../src'; + +export interface BuiltDomain { + /** The public facade. Every integration test drives the stack through this. */ + domain: AragonDomain; + /** Spy over `EnvioClient.query` — assert call count / variables against it. */ + query: ReturnType; +} + +/** + * Builds an `AragonDomain` backed by an `EnvioClient` whose `query` + * method is replaced with a FIFO queue of canned responses. The + * underlying GraphQLClient is constructed but never touches the + * network. + * + * This is the single integration seam for the suite: only the GraphQL + * transport is faked, so the full stack runs — controller, request / + * response mappers, use case, store, store-side mappers, and domain + * objects. Entry is always `AragonDomain.load(...)` plus a public + * facade method; nothing reaches inside the controller. + * + * Responses are consumed in call order. Draining past the end is a + * test bug (the code issued more queries than the scenario set up), so + * it throws rather than returning `undefined`. + */ +export function buildDomain(responses: unknown[]): BuiltDomain { + const envio = new EnvioClient('https://unused.example.invalid'); + const queue = [...responses]; + const query = vi.fn(async () => { + if (queue.length === 0) { + throw new Error('EnvioClient.query called more times than expected'); + } + return queue.shift(); + }); + vi.spyOn(envio, 'query').mockImplementation(query as never); + + return { domain: AragonDomain.load(envio), query }; +} diff --git a/test/support/constants.ts b/test/support/constants.ts new file mode 100644 index 0000000..e93f54e --- /dev/null +++ b/test/support/constants.ts @@ -0,0 +1,14 @@ +/** + * Shared addresses and constants for the integration test suite. + */ + +export const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; +export const BOB = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + +export const PLUGIN = '0x17a1688c56087ade762721180e1cc1e831c73719'; +export const TOKEN = '0x0a830e9f2baa2ebaf8d33c0806283dea9c08952f'; + +/** Legacy ENS coinType (SLIP-44 ETH). */ +export const ETH_COIN_TYPE = '60'; +/** ENSIP-19 default EVM reverse coinType (0x80000000). */ +export const DEFAULT_EVM_COIN_TYPE = '2147483648'; diff --git a/test/support/fixtures/memberProfile.ts b/test/support/fixtures/memberProfile.ts new file mode 100644 index 0000000..f9842e8 --- /dev/null +++ b/test/support/fixtures/memberProfile.ts @@ -0,0 +1,44 @@ +/** + * Canned Envio responses for the `getMemberProfileTextRecords` flow. + * + * The store issues a single `Domain` query, so each scenario queues + * exactly one of these builders. + */ + +export interface TextRecord { + key: string; + value: string; + version?: string; +} + +/** + * A `Domain` response carrying a resolver with the given text records. + */ +export function domainResponse( + name: string, + texts: TextRecord[], + resolverVersion = '0', +) { + return { + Domain: [ + { + id: 'domain-id', + name, + resolver: { + version: resolverVersion, + texts: texts.map((t) => ({ version: '0', ...t })), + }, + }, + ], + }; +} + +/** The subdomain is unknown to the indexer — no `Domain` row at all. */ +export function unknownDomainResponse() { + return { Domain: [] }; +} + +/** The subdomain exists but has no resolver attached yet. */ +export function noResolverResponse(name: string) { + return { Domain: [{ id: 'domain-id', name, resolver: null }] }; +} diff --git a/test/support/fixtures/tokenVotingMembers.ts b/test/support/fixtures/tokenVotingMembers.ts new file mode 100644 index 0000000..394c394 --- /dev/null +++ b/test/support/fixtures/tokenVotingMembers.ts @@ -0,0 +1,69 @@ +import { TOKEN } from '../constants'; + +/** + * Canned Envio responses for the `getTokenVotingMembership` flow. + */ + +export interface DelegateOverrides { + votingPower?: string; + delegationCount?: number; + firstVotingPowerChangeTimestamp?: string; + lastVotingPowerChangeTimestamp?: string; +} + +/** A single `ERC20VotesDelegate` row as the indexer returns it. */ +export function delegate(address: string, overrides: DelegateOverrides = {}) { + return { + id: `1-${TOKEN}-${address}`, + chainId: 1, + tokenContractAddress: TOKEN, + delegateAddress: address, + votingPower: overrides.votingPower ?? '5000000000000000000', + delegationCount: overrides.delegationCount ?? 1, + firstVotingPowerChangeTimestamp: + overrides.firstVotingPowerChangeTimestamp ?? '1700000000', + lastVotingPowerChangeTimestamp: + overrides.lastVotingPowerChangeTimestamp ?? '1700000100', + }; +} + +export interface FindMembersResponseInput { + delegates?: ReturnType[]; + metrics?: unknown[]; + /** + * The chain-wide total used for pagination. Defaults to the number + * of `delegates` on this page; override to simulate more pages. + */ + totalRecords?: number; +} + +/** + * First query response: the page of delegates, the id-only set used + * for total-count, and the plugin's MemberMetrics. + */ +export function findMembersResponse({ + delegates = [], + metrics = [], + totalRecords, +}: FindMembersResponseInput = {}) { + const total = totalRecords ?? delegates.length; + return { + ERC20VotesDelegate: delegates, + AllERC20VotesDelegate: Array.from({ length: total }, (_, i) => ({ + id: `total-${i}`, + })), + MemberMetrics: metrics, + }; +} + +/** A single `ReverseName` row. */ +export function reverseName(address: string, coinType: string, name: string) { + return { address, coinType, name }; +} + +/** Second query response: the reverse-name rows for the page. */ +export function reverseNamesResponse( + rows: ReturnType[] = [], +) { + return { ReverseName: rows }; +} From 710d6aaee2789bf92bc34084eb8618e7e1067d00 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:43:29 +0100 Subject: [PATCH 25/33] Handle ENS with viem instead of through the indexer --- src/domain/ens/ENSName.test.ts | 31 +++ src/domain/ens/ENSName.ts | 41 ++++ src/domain/ens/ENSStore.ts | 22 +++ src/domain/member/MemberStore.ts | 17 +- src/domain/member/TokenVotingMember.test.ts | 30 ++- src/domain/member/TokenVotingMember.ts | 31 ++- .../member/TokenVotingMemberRecord.test.ts | 32 ++++ src/domain/member/TokenVotingMemberRecord.ts | 62 ++++++ src/index.ts | 1 + src/infrastructure/config/RpcUrls.ts | 4 + .../AragonController/AragonController.ts | 9 +- .../maps/domain/TokenVotingMemberMap.test.ts | 3 +- .../maps/domain/TokenVotingMemberMap.ts | 2 +- .../GetTokenVotingMembershipMap.test.ts | 15 +- .../use-cases/GetTokenVotingMembershipMap.ts | 13 +- .../EnvioMemberStore/EnvioMemberStore.test.ts | 117 ++++-------- .../EnvioMemberStore/EnvioMemberStore.ts | 96 ++-------- .../EnvioMemberStore/maps/ReverseNameMap.ts | 44 ----- .../maps/TokenVotingMemberMap.test.ts | 130 +++++++------ .../maps/TokenVotingMemberMap.ts | 130 +++++-------- .../stores/ViemENSStore/ViemENSStore.test.ts | 177 ++++++++++++++++++ .../stores/ViemENSStore/ViemENSStore.ts | 100 ++++++++++ .../GetTokenVotingMembershipUseCase.test.ts | 95 ++++++++-- .../GetTokenVotingMembershipUseCase.ts | 39 +++- test/getTokenVotingMembership.test.ts | 70 +++++-- test/support/buildDomain.ts | 27 ++- test/support/constants.ts | 5 - test/support/fixtures/tokenVotingMembers.ts | 16 +- tsconfig.json | 3 +- 29 files changed, 917 insertions(+), 445 deletions(-) create mode 100644 src/domain/ens/ENSName.test.ts create mode 100644 src/domain/ens/ENSName.ts create mode 100644 src/domain/ens/ENSStore.ts create mode 100644 src/domain/member/TokenVotingMemberRecord.test.ts create mode 100644 src/domain/member/TokenVotingMemberRecord.ts create mode 100644 src/infrastructure/config/RpcUrls.ts delete mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts create mode 100644 src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts create mode 100644 src/infrastructure/stores/ViemENSStore/ViemENSStore.ts diff --git a/src/domain/ens/ENSName.test.ts b/src/domain/ens/ENSName.test.ts new file mode 100644 index 0000000..965f399 --- /dev/null +++ b/src/domain/ens/ENSName.test.ts @@ -0,0 +1,31 @@ +import { ENSName } from './ENSName'; + +describe('ENSName', () => { + it('stores a valid name', () => { + expect(ENSName.fromString('alice.eth').toString()).toBe('alice.eth'); + }); + + it('normalizes to the ENSIP-15 canonical form (case folding)', () => { + expect(ENSName.fromString('Alice.ETH').toString()).toBe('alice.eth'); + }); + + it('preserves a non-.eth primary name (e.g. an L2 basename)', () => { + expect(ENSName.fromString('alice.base.eth').toString()).toBe( + 'alice.base.eth', + ); + }); + + it('throws on an empty name', () => { + expect(() => ENSName.fromString('')).toThrow(); + }); + + it('throws on a name with disallowed characters', () => { + expect(() => ENSName.fromString('has space.eth')).toThrow(); + }); + + it('compares equal for equal names', () => { + expect( + ENSName.fromString('alice.eth').equals(ENSName.fromString('Alice.eth')), + ).toBe(true); + }); +}); diff --git a/src/domain/ens/ENSName.ts b/src/domain/ens/ENSName.ts new file mode 100644 index 0000000..8a5ae68 --- /dev/null +++ b/src/domain/ens/ENSName.ts @@ -0,0 +1,41 @@ +import { ValueObject } from 'ddd-core-ts'; +import { normalize as ensNormalize } from 'viem/ens'; +import { z } from 'zod'; + +/** + * Normalizes a raw ENS name to its ENSIP-15 canonical form. + */ +function canonicalize(input: string): string { + return ensNormalize(input.trim()); +} + +const ENSNamePropsSchema = z + .object({ + value: z.string().min(1, 'name must not be empty'), + }) + .transform(({ value }) => ({ value: canonicalize(value) })); + +type ENSNameProps = z.output; +type ENSNameInput = z.input; + +/** + * A normalized ENS name — e.g. a primary name obtained via reverse + * resolution. Stored in its ENSIP-15 canonical form so that equal names + * compare equal and any downstream hashing/display stays consistent. + * + * `create` throws if the input is not a valid ENS name. + */ +export class ENSName extends ValueObject { + toString(): string { + return this.props.value; + } + + static create(props: ENSNameInput): ENSName { + const validated = ENSNamePropsSchema.parse(props); + return new ENSName(validated); + } + + static fromString(input: string): ENSName { + return ENSName.create({ value: input }); + } +} diff --git a/src/domain/ens/ENSStore.ts b/src/domain/ens/ENSStore.ts new file mode 100644 index 0000000..31c15d1 --- /dev/null +++ b/src/domain/ens/ENSStore.ts @@ -0,0 +1,22 @@ +import type { Address, HexString } from '@/domain/primitives'; +import type { ENSName } from './ENSName'; + +export interface ENSStore { + /** + * Resolves the primary ENS name for each address. + * + * Returns a map keyed by checksummed hex address. Addresses with no + * primary name, or whose name fails resolution or normalization, are + * absent from the map. A missing key means "no name", never an error. + * + * @param coinType ENSIP-11 coin type selecting which chain's primary + * name to read. Implementations default to `60` (legacy Ethereum + * mainnet), which covers the overwhelming majority of primary names + * today. L2 / ENSIP-19 multichain names are only reached when a + * chain-specific coin type is passed. + */ + lookUpPrimaryNames( + addresses: Address[], + coinType?: bigint, + ): Promise>; +} diff --git a/src/domain/member/MemberStore.ts b/src/domain/member/MemberStore.ts index bd292cc..5174d97 100644 --- a/src/domain/member/MemberStore.ts +++ b/src/domain/member/MemberStore.ts @@ -1,19 +1,16 @@ +import type { Address } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; -import type { TokenVotingMember } from './TokenVotingMember'; +import type { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; -/** - * Member-fetching boundary for the membership domain. Holds the - * token-voting query today; other member queries can be added here. - */ export interface MemberStore { /** - * Finds members of a TokenVoting plugin scoped to its ERC20Votes token contract. - * Returns a page of members sorted by voting power descending. + * Finds members of a TokenVoting plugin scoped to its token contract. + * Returns a page of member records sorted by voting power descending. */ findTokenVotingMembers( - pluginAddress: string, - tokenContractAddress: string, + pluginAddress: Address, + tokenContractAddress: Address, page: PageRequest, - ): Promise>; + ): Promise>; } diff --git a/src/domain/member/TokenVotingMember.test.ts b/src/domain/member/TokenVotingMember.test.ts index d147222..82ff152 100644 --- a/src/domain/member/TokenVotingMember.test.ts +++ b/src/domain/member/TokenVotingMember.test.ts @@ -1,6 +1,8 @@ +import { ENSName } from '@/domain/ens/ENSName'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; import { TokenVotingMember } from './TokenVotingMember'; +import { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; describe('TokenVotingMember', () => { const address = Address.fromHexString( @@ -11,17 +13,26 @@ describe('TokenVotingMember', () => { const validProps = { address, - ens: 'alice.eth', + ens: ENSName.fromString('alice.eth'), votingPower: VotingPower.fromBigInt(1000000000000000000n), firstActivityTimestamp, lastActivityTimestamp, delegationCount: 3, }; + const buildRecord = () => + TokenVotingMemberRecord.create({ + address, + votingPower: VotingPower.fromBigInt(1000000000000000000n), + firstActivityTimestamp, + lastActivityTimestamp, + delegationCount: 3, + }); + it('creates a valid member', () => { const member = TokenVotingMember.create(validProps); expect(member.address.equals(address)).toBe(true); - expect(member.ens).toBe('alice.eth'); + expect(member.ens?.toString()).toBe('alice.eth'); expect(member.votingPower.isZero).toBe(false); expect(member.firstActivityTimestamp).toBe(firstActivityTimestamp); expect(member.lastActivityTimestamp).toBe(lastActivityTimestamp); @@ -32,6 +43,21 @@ describe('TokenVotingMember', () => { expect(member.ens).toBeNull(); }); + it('composes a record and a resolved name via fromRecord', () => { + const member = TokenVotingMember.fromRecord( + buildRecord(), + ENSName.fromString('alice.eth'), + ); + expect(member.address.equals(address)).toBe(true); + expect(member.ens?.toString()).toBe('alice.eth'); + expect(member.delegationCount).toBe(3); + }); + + it('composes a record with no name (null) via fromRecord', () => { + const member = TokenVotingMember.fromRecord(buildRecord(), null); + expect(member.ens).toBeNull(); + }); + it('structural equality', () => { const a = TokenVotingMember.create(validProps); const b = TokenVotingMember.create(validProps); diff --git a/src/domain/member/TokenVotingMember.ts b/src/domain/member/TokenVotingMember.ts index 05217e0..fd31d9b 100644 --- a/src/domain/member/TokenVotingMember.ts +++ b/src/domain/member/TokenVotingMember.ts @@ -1,11 +1,13 @@ import { ValueObject } from 'ddd-core-ts'; import { z } from 'zod'; +import { ENSName } from '@/domain/ens/ENSName'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; +import type { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; const TokenVotingMemberPropsSchema = z.object({ address: z.instanceof(Address), - ens: z.string().nullable(), + ens: z.instanceof(ENSName).nullable(), votingPower: z.instanceof(VotingPower), firstActivityTimestamp: z.number().int().nonnegative(), lastActivityTimestamp: z.number().int().nonnegative(), @@ -15,9 +17,7 @@ const TokenVotingMemberPropsSchema = z.object({ type TokenVotingMemberProps = z.infer; /** - * A member of a TokenVoting plugin: aggregated voting power on the - * governance token plus per-plugin activity timestamps (unix seconds), - * sourced from the indexer. + * A member of a TokenVoting plugin. */ export class TokenVotingMember extends ValueObject { /** @@ -28,9 +28,10 @@ export class TokenVotingMember extends ValueObject { } /** - * The member's ENS name, or null if not resolved. + * The member's primary ENS name, or null when the address has no + * primary name (or it could not be resolved). */ - get ens(): string | null { + get ens(): ENSName | null { return this.props.ens; } @@ -67,4 +68,22 @@ export class TokenVotingMember extends ValueObject { const validated = TokenVotingMemberPropsSchema.parse(props); return new TokenVotingMember(validated); } + + /** + * Composes a fully-resolved member from its indexed on-chain record and + * primary ENS name. + */ + static fromRecord( + record: TokenVotingMemberRecord, + ens: ENSName | null, + ): TokenVotingMember { + return TokenVotingMember.create({ + address: record.address, + ens, + votingPower: record.votingPower, + firstActivityTimestamp: record.firstActivityTimestamp, + lastActivityTimestamp: record.lastActivityTimestamp, + delegationCount: record.delegationCount, + }); + } } diff --git a/src/domain/member/TokenVotingMemberRecord.test.ts b/src/domain/member/TokenVotingMemberRecord.test.ts new file mode 100644 index 0000000..31a9a75 --- /dev/null +++ b/src/domain/member/TokenVotingMemberRecord.test.ts @@ -0,0 +1,32 @@ +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; + +describe('TokenVotingMemberRecord', () => { + const address = Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ); + + const validProps = { + address, + votingPower: VotingPower.fromBigInt(1000000000000000000n), + firstActivityTimestamp: 1705320000, + lastActivityTimestamp: 1718872200, + delegationCount: 3, + }; + + it('creates a valid record', () => { + const record = TokenVotingMemberRecord.create(validProps); + expect(record.address.equals(address)).toBe(true); + expect(record.votingPower.isZero).toBe(false); + expect(record.firstActivityTimestamp).toBe(1705320000); + expect(record.lastActivityTimestamp).toBe(1718872200); + expect(record.delegationCount).toBe(3); + }); + + it('rejects a negative delegation count', () => { + expect(() => + TokenVotingMemberRecord.create({ ...validProps, delegationCount: -1 }), + ).toThrow(); + }); +}); diff --git a/src/domain/member/TokenVotingMemberRecord.ts b/src/domain/member/TokenVotingMemberRecord.ts new file mode 100644 index 0000000..986cc1e --- /dev/null +++ b/src/domain/member/TokenVotingMemberRecord.ts @@ -0,0 +1,62 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { Address } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; + +const TokenVotingMemberRecordPropsSchema = z.object({ + address: z.instanceof(Address), + votingPower: z.instanceof(VotingPower), + firstActivityTimestamp: z.number().int().nonnegative(), + lastActivityTimestamp: z.number().int().nonnegative(), + delegationCount: z.number().int().nonnegative(), +}); + +type TokenVotingMemberRecordProps = z.infer< + typeof TokenVotingMemberRecordPropsSchema +>; + +/** + * A TokenVoting member as derived purely from on-chain state. + */ +export class TokenVotingMemberRecord extends ValueObject { + /** + * The member's wallet address. + */ + get address(): Address { + return this.props.address; + } + + /** + * The member's voting power. + */ + get votingPower(): VotingPower { + return this.props.votingPower; + } + + /** + * Unix-seconds timestamp of the member's first observed activity. + */ + get firstActivityTimestamp(): number { + return this.props.firstActivityTimestamp; + } + + /** + * Unix-seconds timestamp of the member's most recent observed activity. + */ + get lastActivityTimestamp(): number { + return this.props.lastActivityTimestamp; + } + + /** + * Number of distinct accounts currently delegating their voting power + * to this member (counts self-delegation). + */ + get delegationCount(): number { + return this.props.delegationCount; + } + + static create(props: TokenVotingMemberRecordProps): TokenVotingMemberRecord { + const validated = TokenVotingMemberRecordPropsSchema.parse(props); + return new TokenVotingMemberRecord(validated); + } +} diff --git a/src/index.ts b/src/index.ts index 97446d1..ed76844 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { AragonController as AragonDomain } from './infrastructure/controllers/AragonController/AragonController'; // Infrastructure +export type { RpcUrls } from './infrastructure/config/RpcUrls'; export { EnvioClient } from './infrastructure/stores/EnvioClient'; // Public DTOs + request types (type-only) diff --git a/src/infrastructure/config/RpcUrls.ts b/src/infrastructure/config/RpcUrls.ts new file mode 100644 index 0000000..c77aa4c --- /dev/null +++ b/src/infrastructure/config/RpcUrls.ts @@ -0,0 +1,4 @@ +/** + * RPC endpoint URLs keyed by EVM chain id. + */ +export type RpcUrls = Partial>; diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index d064136..be59250 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -3,9 +3,11 @@ import { handleRequest } from 'ddd-core-ts'; import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import type { MemberProfileTextRecord } from '@/domain/member-profile/MemberProfileTextRecord'; import type { Page } from '@/domain/primitives/pagination/Page'; +import type { RpcUrls } from '@/infrastructure/config/RpcUrls'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberProfileStore } from '@/infrastructure/stores/EnvioMemberProfileStore/EnvioMemberProfileStore'; import { EnvioMemberStore } from '@/infrastructure/stores/EnvioMemberStore/EnvioMemberStore'; +import { ViemENSStore } from '@/infrastructure/stores/ViemENSStore/ViemENSStore'; import type { GetMemberProfileTextRecordsUseCaseProps } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import { GetMemberProfileTextRecordsUseCase } from '@/use-cases/GetMemberProfileTextRecordsUseCase'; import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; @@ -42,11 +44,16 @@ export class AragonController { /** * Initializes the `AragonDomain`. + * + * @param envioClient Indexer client backing the on-chain member data. + * @param rpcUrls RPC endpoint URLs keyed by chain id. */ - static load(envioClient: EnvioClient): AragonController { + static load(envioClient: EnvioClient, rpcUrls: RpcUrls): AragonController { const memberStore = new EnvioMemberStore(envioClient); + const ensStore = ViemENSStore.fromRpcUrls(rpcUrls); const getTokenVotingMembershipUseCase = new GetTokenVotingMembershipUseCase( memberStore, + ensStore, ); const memberProfileStore = new EnvioMemberProfileStore(envioClient); diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts index 8cc5c7e..0f1f5fb 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts @@ -1,3 +1,4 @@ +import { ENSName } from '@/domain/ens/ENSName'; import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; @@ -13,7 +14,7 @@ describe('TokenVotingMemberMap.mapDomainToDTO', () => { ) => TokenVotingMember.create({ address, - ens: 'alice.eth', + ens: ENSName.fromString('alice.eth'), votingPower: VotingPower.fromBigInt(5000000000000000000n), firstActivityTimestamp: 1705320000, lastActivityTimestamp: 1718872200, diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts index 5456bab..1500777 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts @@ -26,7 +26,7 @@ export function mapDomainToDTO( const last = member.lastActivityTimestamp; return { address: member.address.toHexString(), - ens: member.ens, + ens: member.ens ? member.ens.toString() : null, votingPower: member.votingPower.value.toBigNumber().toFixed(0), metrics: { firstActivityTimestamp: first === 0 ? null : first, diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts index 88dbdf0..ab312e6 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts @@ -1,3 +1,4 @@ +import { ENSName } from '@/domain/ens/ENSName'; import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; import { Address } from '@/domain/primitives'; import { createPage } from '@/domain/primitives/pagination/Page'; @@ -28,11 +29,17 @@ describe('GetTokenVotingMembershipMap', () => { expect(result.page.pageSize).toBe(20); }); - it('forwards plugin and token addresses unchanged', () => { + it('converts plugin and token addresses to Address value objects', () => { const result = mapDTOToDomain({ pluginAddress, tokenContractAddress }); - expect(result.pluginAddress).toBe(pluginAddress); - expect(result.tokenContractAddress).toBe(tokenContractAddress); + expect( + result.pluginAddress.equals(Address.fromHexString(pluginAddress)), + ).toBe(true); + expect( + result.tokenContractAddress.equals( + Address.fromHexString(tokenContractAddress), + ), + ).toBe(true); }); }); @@ -42,7 +49,7 @@ describe('GetTokenVotingMembershipMap', () => { address: Address.fromHexString( '0x0123456789abcdef0123456789abcdef01234567', ), - ens: 'alice.eth', + ens: ENSName.fromString('alice.eth'), votingPower: VotingPower.fromBigInt(5000000000000000000n), firstActivityTimestamp: 1705320000, lastActivityTimestamp: 1718872200, diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts index ad2049a..7980fd1 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts @@ -1,4 +1,5 @@ import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { Address, assertHexString } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; @@ -16,9 +17,17 @@ export interface GetTokenVotingMembershipRequestDTO { export function mapDTOToDomain( dto: GetTokenVotingMembershipRequestDTO, ): GetTokenVotingMembershipUseCaseProps { + assertHexString( + dto.pluginAddress, + 'Expected plugin address to be a hex string', + ); + assertHexString( + dto.tokenContractAddress, + 'Expected token contract address to be a hex string', + ); return { - pluginAddress: dto.pluginAddress, - tokenContractAddress: dto.tokenContractAddress, + pluginAddress: Address.fromHexString(dto.pluginAddress), + tokenContractAddress: Address.fromHexString(dto.tokenContractAddress), page: PageRequest.create({ page: dto.page ?? 1, pageSize: dto.pageSize ?? 20, diff --git a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts index 0159f0d..f053499 100644 --- a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts @@ -1,3 +1,4 @@ +import { Address } from '@/domain/primitives'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; import { EnvioMemberStore } from './EnvioMemberStore'; @@ -5,12 +6,12 @@ import { EnvioMemberStore } from './EnvioMemberStore'; const PLUGIN = '0x1111111111111111111111111111111111111111'; const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; +const PLUGIN_ADDRESS = Address.fromHexString(PLUGIN); +const TOKEN_ADDRESS = Address.fromHexString(TOKEN); + const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; const BOB = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; -const ETH_COIN_TYPE = '60'; -const DEFAULT_EVM_COIN_TYPE = '2147483648'; // 0x80000000 - interface QueryCall { document: string; variables: Record; @@ -22,9 +23,8 @@ interface MockClient { } /** - * Builds an EnvioClient stub that returns each `response` in order - * for the corresponding call. The first call resolves with the - * members response; the second with the reverse-names response. + * Builds an EnvioClient stub that returns each `response` in order for + * the corresponding call. */ function buildMockEnvioClient(responses: unknown[]): MockClient { const calls: QueryCall[] = []; @@ -57,82 +57,60 @@ const buildDelegate = ( describe('EnvioMemberStore', () => { const page = PageRequest.create({ page: 1, pageSize: 20 }); - it('attaches ENS names from the indexer ReverseName entity to each member', async () => { + it('returns a page of member records in a single query (no ENS round-trip)', async () => { const { envio, calls } = buildMockEnvioClient([ { ERC20VotesDelegate: [buildDelegate(ALICE), buildDelegate(BOB)], AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }], MemberMetrics: [], }, - { - ReverseName: [ - { - address: ALICE, - coinType: DEFAULT_EVM_COIN_TYPE, - name: 'alice.eth', - }, - ], - }, ]); const store = new EnvioMemberStore(envio); - const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); - - expect(result.items.map((m) => m.ens)).toEqual(['alice.eth', null]); + const result = await store.findTokenVotingMembers( + PLUGIN_ADDRESS, + TOKEN_ADDRESS, + page, + ); - // The reverse-name query was scoped to just the page's addresses. - expect(calls).toHaveLength(2); - expect(calls[1].variables).toEqual({ addresses: [ALICE, BOB] }); + expect(result.items).toHaveLength(2); + expect(result.totalRecords).toBe(2); + expect(calls).toHaveLength(1); }); - it('prefers the ENSIP-19 default.reverse name (coinType 0x80000000) over the legacy coinType 60 entry', async () => { - // Indexer is ordered by coinType desc, so the modern row arrives - // first. Verifying that the dedupe keeps the first row per address. + it('merges MemberMetrics activity into the matching delegate record', async () => { const { envio } = buildMockEnvioClient([ { ERC20VotesDelegate: [buildDelegate(ALICE)], AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }, - { - ReverseName: [ + MemberMetrics: [ { - address: ALICE, - coinType: DEFAULT_EVM_COIN_TYPE, - name: 'alice-default.eth', + id: `1-${PLUGIN}-${ALICE}`, + chainId: 1, + pluginAddress: PLUGIN, + memberAddress: ALICE, + firstActivityTimestamp: '1650000000', + lastActivityTimestamp: '1750000000', }, - { address: ALICE, coinType: ETH_COIN_TYPE, name: 'alice-legacy.eth' }, ], }, ]); const store = new EnvioMemberStore(envio); - const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); - - expect(result.items[0].ens).toBe('alice-default.eth'); + const result = await store.findTokenVotingMembers( + PLUGIN_ADDRESS, + TOKEN_ADDRESS, + page, + ); + + // metrics first-activity (1650000000) is earlier than the delegate's + // first VP change (1700000000); metrics last-activity (1750000000) is + // later than the delegate's last VP change (1700000100). + expect(result.items[0].firstActivityTimestamp).toBe(1650000000); + expect(result.items[0].lastActivityTimestamp).toBe(1750000000); }); - it('falls back to legacy coinType 60 when no default.reverse row exists', async () => { - const { envio } = buildMockEnvioClient([ - { - ERC20VotesDelegate: [buildDelegate(ALICE)], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }, - { - ReverseName: [ - { address: ALICE, coinType: ETH_COIN_TYPE, name: 'alice-legacy.eth' }, - ], - }, - ]); - const store = new EnvioMemberStore(envio); - - const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); - - expect(result.items[0].ens).toBe('alice-legacy.eth'); - }); - - it('skips the reverse-name query when the page is empty', async () => { + it('returns an empty page when there are no delegates', async () => { const { envio, calls } = buildMockEnvioClient([ { ERC20VotesDelegate: [], @@ -142,29 +120,16 @@ describe('EnvioMemberStore', () => { ]); const store = new EnvioMemberStore(envio); - const result = await store.findTokenVotingMembers(PLUGIN, TOKEN, page); + const result = await store.findTokenVotingMembers( + PLUGIN_ADDRESS, + TOKEN_ADDRESS, + page, + ); expect(result.items).toHaveLength(0); expect(calls).toHaveLength(1); }); - it('lowercases the addresses passed to the reverse-name query', async () => { - const ALICE_MIXED = '0x0123456789ABCDEF0123456789ABCDEF01234567'; - const { envio, calls } = buildMockEnvioClient([ - { - ERC20VotesDelegate: [buildDelegate(ALICE_MIXED)], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }, - { ReverseName: [] }, - ]); - const store = new EnvioMemberStore(envio); - - await store.findTokenVotingMembers(PLUGIN, TOKEN, page); - - expect(calls[1].variables).toEqual({ addresses: [ALICE] }); - }); - it('wraps query failures with a member-level error', async () => { const envio = { query: vi.fn().mockRejectedValue(new Error('graphql exploded')), @@ -172,7 +137,7 @@ describe('EnvioMemberStore', () => { const store = new EnvioMemberStore(envio); await expect( - store.findTokenVotingMembers(PLUGIN, TOKEN, page), + store.findTokenVotingMembers(PLUGIN_ADDRESS, TOKEN_ADDRESS, page), ).rejects.toThrow('Error querying members from Envio'); }); }); diff --git a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts index 643cb98..ff8fd1d 100644 --- a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts @@ -1,25 +1,16 @@ import type { MemberStore } from '@/domain/member/MemberStore'; -import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import type { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; +import type { Address } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import { createPage } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; -import * as ReverseNameMap from './maps/ReverseNameMap'; import * as TokenVotingMemberMap from './maps/TokenVotingMemberMap'; /** * Fetches a page of delegates by token (ordered by VP desc), every * delegate's id (for total-count), and the MemberMetrics for the - * plugin — all in one round-trip. - * - * The query straddles the indexer's generic / Aragon partition: - * - ERC20VotesDelegate is generic (per-token, chain-wide) - * - MemberMetrics is Aragon-specific (per-plugin) - * - * Hasura cannot express a cross-entity join here, so the pairing - * happens client-side. That client-side merge is also what makes the - * eventual indexer split a no-op for this file beyond pointing the - * two halves at separate GraphQL endpoints. + * plugin. */ const FIND_MEMBERS_QUERY = ` query FindMembers( @@ -69,40 +60,21 @@ const FIND_MEMBERS_QUERY = ` } `; -/** - * Fetches every `ReverseName` row that matches the given page of - * delegate addresses. An address can have up to two rows on mainnet - * (legacy `coinType=60` and ENSIP-19 `coinType=0x80000000`); we pull - * both and pick precedence client-side via `ReverseNameMap`. - * - * Ordering by `coinType` descending puts `0x80000000` (2147483648) - * ahead of `60`, so the first row per address in the dedupe pass is - * the preferred one. - */ -const FIND_REVERSE_NAMES_QUERY = ` - query FindReverseNames($addresses: [String!]!) { - ReverseName( - where: { address: { _in: $addresses } } - order_by: [{ coinType: desc }] - ) { - address - coinType - name - } - } -`; - export class EnvioMemberStore implements MemberStore { constructor(private readonly envio: EnvioClient) {} public async findTokenVotingMembers( - pluginAddress: string, - tokenContractAddress: string, + pluginAddress: Address, + tokenContractAddress: Address, request: PageRequest, - ): Promise> { + ): Promise> { try { - const pluginAddressLower = pluginAddress.toLowerCase(); - const tokenAddressLower = tokenContractAddress.toLowerCase(); + // The indexer stores addresses lowercased; serialize the primitives + // to lowercase hex for the query variables. + const pluginAddressLower = pluginAddress.toHexString().toLowerCase(); + const tokenAddressLower = tokenContractAddress + .toHexString() + .toLowerCase(); const rawMembers = await this.envio.query(FIND_MEMBERS_QUERY, { tokenContractAddress: tokenAddressLower, @@ -110,51 +82,13 @@ export class EnvioMemberStore implements MemberStore { limit: request.pageSize, offset: request.offset, }); - const data = TokenVotingMemberMap.parseFindMembersResponse(rawMembers); - - const metricsByMember = new Map( - data.MemberMetrics.map((m) => [m.memberAddress.toLowerCase(), m]), - ); - const pageAddresses = data.ERC20VotesDelegate.map((d) => - d.delegateAddress.toLowerCase(), - ); - const ensByAddress = await this.fetchReverseNames(pageAddresses); + const { records, totalRecords } = + TokenVotingMemberMap.mapDTOToDomain(rawMembers); - const members = data.ERC20VotesDelegate.map((delegate) => { - const addressLower = delegate.delegateAddress.toLowerCase(); - return TokenVotingMemberMap.mapDTOToDomain( - delegate, - metricsByMember.get(addressLower), - ensByAddress.get(addressLower) ?? null, - ); - }); - - const totalRecords = data.AllERC20VotesDelegate.length; - - return createPage(members, request.page, request.pageSize, totalRecords); + return createPage(records, request.page, request.pageSize, totalRecords); } catch (cause) { throw new Error('Error querying members from Envio', { cause }); } } - - /** - * Resolves a batch of lowercase addresses to their preferred ENS - * primary names via the indexer's `ReverseName` entity. - * - * Returns an empty map when given no addresses — avoids issuing an - * empty `_in` query. - */ - private async fetchReverseNames( - lowercaseAddresses: string[], - ): Promise> { - if (lowercaseAddresses.length === 0) { - return new Map(); - } - - const raw = await this.envio.query(FIND_REVERSE_NAMES_QUERY, { - addresses: lowercaseAddresses, - }); - return ReverseNameMap.mapDTOToDomain(raw); - } } diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts deleted file mode 100644 index 4db7c24..0000000 --- a/src/infrastructure/stores/EnvioMemberStore/maps/ReverseNameMap.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { z } from 'zod'; - -/** - * Shape of a single `ReverseName` row. - * - * An address can have up to two rows on mainnet (legacy `coinType=60` - * and ENSIP-19 `coinType=0x80000000`). The query orders by `coinType` - * descending, so the first row per address in the dedupe pass is the - * preferred one. - */ -const ReverseNameRowSchema = z.object({ - address: z.string(), - coinType: z.string(), - name: z.string(), -}); - -const FindReverseNamesResponseSchema = z.object({ - ReverseName: z.array(ReverseNameRowSchema), -}); - -export type ReverseNameDTO = z.infer; -export type FindReverseNamesResponse = z.infer< - typeof FindReverseNamesResponseSchema ->; - -/** - * Trust boundary for the indexer's `FindReverseNames` response. - * Asserts shape via Zod, dedupes to one row per address (keeping the - * highest-coinType row since the query orders descending), and - * returns the result as a `Map`. - * - * Throws a `ZodError` if the shape doesn't match what we expect. - */ -export function mapDTOToDomain(raw: unknown): Map { - const parsed = FindReverseNamesResponseSchema.parse(raw); - const namesByAddress = new Map(); - for (const row of parsed.ReverseName) { - const address = row.address.toLowerCase(); - if (!namesByAddress.has(address)) { - namesByAddress.set(address, row.name); - } - } - return namesByAddress; -} diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts index 3592aaf..fd93ca7 100644 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts @@ -2,7 +2,6 @@ import { type ERC20VotesDelegateDTO, type MemberMetricsDTO, mapDTOToDomain, - parseFindMembersResponse, } from './TokenVotingMemberMap'; const PLUGIN = '0x1111111111111111111111111111111111111111'; @@ -35,74 +34,99 @@ const buildMetrics = ( ...overrides, }); -describe('parseFindMembersResponse', () => { - it('parses a well-formed response', () => { - const parsed = parseFindMembersResponse({ - ERC20VotesDelegate: [buildDelegate()], - AllERC20VotesDelegate: [{ id: '1' }], - MemberMetrics: [buildMetrics()], - }); - - expect(parsed.ERC20VotesDelegate).toHaveLength(1); - expect(parsed.MemberMetrics).toHaveLength(1); - }); - - it('throws when the response shape does not match', () => { - expect(() => - parseFindMembersResponse({ ERC20VotesDelegate: 'oops' }), - ).toThrow(); - expect(() => parseFindMembersResponse(null)).toThrow(); - }); +const buildResponse = (overrides: { + ERC20VotesDelegate?: ERC20VotesDelegateDTO[]; + AllERC20VotesDelegate?: Array<{ id: string }>; + MemberMetrics?: MemberMetricsDTO[]; +}) => ({ + ERC20VotesDelegate: overrides.ERC20VotesDelegate ?? [], + AllERC20VotesDelegate: overrides.AllERC20VotesDelegate ?? [], + MemberMetrics: overrides.MemberMetrics ?? [], }); describe('mapDTOToDomain', () => { - it('takes the min of metrics first-activity and delegate first-VP-change when both are defined and the metrics value is smaller', () => { - // metrics.firstActivityTimestamp (1650000000) < delegate.firstVotingPowerChangeTimestamp (1700000000) - const member = mapDTOToDomain(buildDelegate(), buildMetrics(), null); - expect(member.firstActivityTimestamp).toBe(1650000000); + it('throws when the response shape does not match', () => { + expect(() => mapDTOToDomain({ ERC20VotesDelegate: 'oops' })).toThrow(); + expect(() => mapDTOToDomain(null)).toThrow(); }); - it('takes the min when the delegate value is smaller (exercises the n < min comparison branch)', () => { - const member = mapDTOToDomain( - buildDelegate({ firstVotingPowerChangeTimestamp: '1600000000' }), - buildMetrics({ firstActivityTimestamp: '1650000000' }), - null, + it('returns a record per delegate plus the chain-wide total count', () => { + const { records, totalRecords } = mapDTOToDomain( + buildResponse({ + ERC20VotesDelegate: [buildDelegate()], + AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + MemberMetrics: [buildMetrics()], + }), ); - expect(member.firstActivityTimestamp).toBe(1600000000); + + expect(records).toHaveLength(1); + expect(totalRecords).toBe(3); }); - it('takes the max of metrics last-activity and delegate last-VP-change when both are defined and the metrics value is larger', () => { - // metrics.lastActivityTimestamp (1750000000) > delegate.lastVotingPowerChangeTimestamp (1700000100) - const member = mapDTOToDomain(buildDelegate(), buildMetrics(), null); - expect(member.lastActivityTimestamp).toBe(1750000000); + it('pairs each delegate with its companion MemberMetrics row by address', () => { + const { records } = mapDTOToDomain( + buildResponse({ + ERC20VotesDelegate: [buildDelegate()], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [buildMetrics()], + }), + ); + + // metrics.firstActivityTimestamp (1650000000) is earlier than the + // delegate's first VP change (1700000000); metrics.lastActivityTimestamp + // (1750000000) is later than the delegate's last VP change (1700000100). + expect(records[0].firstActivityTimestamp).toBe(1650000000); + expect(records[0].lastActivityTimestamp).toBe(1750000000); }); - it('takes the max when the delegate value is larger (exercises the n > max comparison branch)', () => { - const member = mapDTOToDomain( - buildDelegate({ lastVotingPowerChangeTimestamp: '1800000000' }), - buildMetrics({ lastActivityTimestamp: '1750000000' }), - null, + it('takes the earliest/latest activity when the delegate signals are the extremes', () => { + const { records } = mapDTOToDomain( + buildResponse({ + ERC20VotesDelegate: [ + buildDelegate({ + firstVotingPowerChangeTimestamp: '1600000000', + lastVotingPowerChangeTimestamp: '1800000000', + }), + ], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [buildMetrics()], + }), ); - expect(member.lastActivityTimestamp).toBe(1800000000); + + expect(records[0].firstActivityTimestamp).toBe(1600000000); + expect(records[0].lastActivityTimestamp).toBe(1800000000); }); - it('returns 0 timestamps when both signals are null/undefined', () => { - // Delegate without VP-change timestamps, no metrics — the merger - // should fall through to the `?? 0` sentinel on both sides. - const member = mapDTOToDomain( - buildDelegate({ - firstVotingPowerChangeTimestamp: null, - lastVotingPowerChangeTimestamp: null, + it('reports 0 timestamps when neither signal is present', () => { + const { records } = mapDTOToDomain( + buildResponse({ + ERC20VotesDelegate: [ + buildDelegate({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), + ], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], }), - undefined, - null, ); - expect(member.firstActivityTimestamp).toBe(0); - expect(member.lastActivityTimestamp).toBe(0); + + expect(records[0].firstActivityTimestamp).toBe(0); + expect(records[0].lastActivityTimestamp).toBe(0); }); - it('forwards the supplied ENS name to the domain object', () => { - const member = mapDTOToDomain(buildDelegate(), undefined, 'alice.eth'); - expect(member.ens).toBe('alice.eth'); + it('maps voting power and delegation count onto the record', () => { + const { records } = mapDTOToDomain( + buildResponse({ + ERC20VotesDelegate: [ + buildDelegate({ votingPower: '42', delegationCount: 7 }), + ], + AllERC20VotesDelegate: [{ id: 'a' }], + MemberMetrics: [], + }), + ); + + expect(records[0].votingPower.value.toBigNumber().toFixed(0)).toBe('42'); + expect(records[0].delegationCount).toBe(7); }); }); diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts index 0a3bd05..b16ca11 100644 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts @@ -1,21 +1,13 @@ import { z } from 'zod'; -import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; -import { Address, assertHexString } from '@/domain/primitives'; +import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; +import { Address, assertHexString, zExtended } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; -/** - * Shape of an `ERC20VotesDelegate` row on the indexer. - * - * `firstVotingPowerChangeTimestamp` / `lastVotingPowerChangeTimestamp` - * are nullable: a delegate row can exist without ever having had a - * DelegateVotesChanged event (e.g. created from a `+1` to - * delegationCount via a DelegateChanged event on a zero balance). - */ const ERC20VotesDelegateSchema = z.object({ id: z.string(), chainId: z.number(), - tokenContractAddress: z.string(), - delegateAddress: z.string(), + tokenContractAddress: zExtended.hexString(), + delegateAddress: zExtended.hexString(), votingPower: z.string(), delegationCount: z.number(), firstVotingPowerChangeTimestamp: z.string().nullable(), @@ -25,23 +17,12 @@ const ERC20VotesDelegateSchema = z.object({ const MemberMetricsSchema = z.object({ id: z.string(), chainId: z.number(), - pluginAddress: z.string(), - memberAddress: z.string(), + pluginAddress: zExtended.hexString(), + memberAddress: zExtended.hexString(), firstActivityTimestamp: z.string(), lastActivityTimestamp: z.string(), }); -/** - * Shape of the `FindMembers` GraphQL response. Three top-level lists: - * - * - `ERC20VotesDelegate` — the requested page of delegates - * - `AllERC20VotesDelegate` — every delegate id (used for total count) - * - `MemberMetrics` — all member-metrics rows for the plugin - * - * The merge of `ERC20VotesDelegate` (generic / chain-wide) with - * `MemberMetrics` (Aragon-specific) happens client-side; see the - * store for the orchestration. - */ const FindMembersResponseSchema = z.object({ ERC20VotesDelegate: z.array(ERC20VotesDelegateSchema), AllERC20VotesDelegate: z.array(z.object({ id: z.string() })), @@ -52,80 +33,63 @@ export type ERC20VotesDelegateDTO = z.infer; export type MemberMetricsDTO = z.infer; export type FindMembersResponse = z.infer; -/** - * Trust boundary for the indexer's `FindMembers` response. Throws a - * `ZodError` if the shape doesn't match. - */ -export function parseFindMembersResponse(raw: unknown): FindMembersResponse { - return FindMembersResponseSchema.parse(raw); +interface FindMembersResult { + records: TokenVotingMemberRecord[]; + totalRecords: number; } -/** - * Smallest of the defined inputs (treats `null`/`undefined` as - * "no signal"). Returns `0` if neither side has a value. - */ -function minDefined(...values: Array): number { - let min: number | undefined; - for (const v of values) { - if (v == null) continue; - const n = Number(v); - if (min === undefined || n < min) min = n; - } - return min ?? 0; -} +export function mapDTOToDomain(raw: unknown): FindMembersResult { + const data = FindMembersResponseSchema.parse(raw); -/** - * Largest of the defined inputs. Returns `0` if neither side has a - * value. - */ -function maxDefined(...values: Array): number { - let max: number | undefined; - for (const v of values) { - if (v == null) continue; - const n = Number(v); - if (max === undefined || n > max) max = n; - } - return max ?? 0; + const metricsByMember = new Map( + data.MemberMetrics.map((metrics) => [ + metrics.memberAddress.toLowerCase(), + metrics, + ]), + ); + + const records = data.ERC20VotesDelegate.map((delegate) => + toRecord( + delegate, + metricsByMember.get(delegate.delegateAddress.toLowerCase()), + ), + ); + + return { records, totalRecords: data.AllERC20VotesDelegate.length }; } /** - * Maps a single `ERC20VotesDelegate` row (plus its companion - * `MemberMetrics` row and any attached ENS name) to a domain - * `TokenVotingMember`. Verifies that the address fields look like - * `0x`-prefixed hex before passing them through to the domain - * primitives — `Address.fromHexString` does the final shape check. + * Assembles a single domain `TokenVotingMemberRecord` from a delegate + * row and its optional companion `MemberMetrics` row. */ -export function mapDTOToDomain( +function toRecord( delegate: ERC20VotesDelegateDTO, metrics: MemberMetricsDTO | undefined, - ens: string | null, -): TokenVotingMember { - assertHexString( - delegate.delegateAddress, - 'delegateAddress must be a 0x-prefixed hex string', - ); - - // Activity is the merge of two signals: - // - MemberMetrics: VoteCast / ProposalCreated activity in this plugin - // - ERC20VotesDelegate: voting-power changes for this delegate-token pair - // The indexer keeps these denormalized across the generic / Aragon - // partition; the merge happens here so that a future split into two - // indexers needs no schema change beyond two GraphQL endpoints. - const firstActivityTimestamp = minDefined( +): TokenVotingMemberRecord { + // A member's activity window spans both their governance activity + // (MemberMetrics: VoteCast / ProposalCreated) and their voting-power + // changes (ERC20VotesDelegate). Take the earliest start and latest end + // across whichever signals are present; 0 means "no activity recorded". + const firstSignals = [ metrics?.firstActivityTimestamp, delegate.firstVotingPowerChangeTimestamp, - ); - const lastActivityTimestamp = maxDefined( + ] + .filter((t): t is string => t != null) + .map(Number); + const lastSignals = [ metrics?.lastActivityTimestamp, delegate.lastVotingPowerChangeTimestamp, - ); + ] + .filter((t): t is string => t != null) + .map(Number); - return TokenVotingMember.create({ + return TokenVotingMemberRecord.create({ address: Address.fromHexString(delegate.delegateAddress), votingPower: VotingPower.fromBigInt(BigInt(delegate.votingPower)), - ens, - firstActivityTimestamp, - lastActivityTimestamp, + firstActivityTimestamp: + firstSignals.length > 0 ? Math.min(...firstSignals) : 0, + lastActivityTimestamp: + lastSignals.length > 0 ? Math.max(...lastSignals) : 0, delegationCount: delegate.delegationCount, }); } diff --git a/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts b/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts new file mode 100644 index 0000000..a432bed --- /dev/null +++ b/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts @@ -0,0 +1,177 @@ +import { createPublicClient, http, type PublicClient } from 'viem'; +import { mainnet } from 'viem/chains'; +import { Address } from '@/domain/primitives'; +import { ViemENSStore } from './ViemENSStore'; + +// `createPublicClient` / `http` are stubbed so `fromRpcUrls` can be asserted +// without a real RPC; everything else (e.g. `getAddress` used by `Address`) +// is the real implementation. +vi.mock('viem', async (importOriginal) => ({ + ...(await importOriginal()), + createPublicClient: vi.fn(), + http: vi.fn((url?: string) => ({ url })), +})); + +const MAINNET_RPC = 'https://eth.example/rpc'; +const ALICE = '0x0123456789abcdef0123456789abcdef01234567'; +const BOB = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; + +const address = (hex: string) => Address.fromHexString(hex as `0x${string}`); + +/** + * A stub viem client whose `getEnsName` resolves from a + * `name-by-lowercase-address` table. Absent addresses resolve to `null`; + * addresses mapped to an `Error` reject (RPC failure). + */ +function buildClient(table: Record) { + const getEnsName = vi.fn( + async ({ address: addr }: { address: string; coinType?: bigint }) => { + const entry = table[addr.toLowerCase()]; + if (entry instanceof Error) { + throw entry; + } + return entry ?? null; + }, + ); + return { client: { getEnsName } as unknown as PublicClient, getEnsName }; +} + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('ViemENSStore.fromRpcUrls', () => { + it('builds a mainnet client with multicall batching from the chain-id URL map', () => { + vi.mocked(createPublicClient).mockReturnValue(buildClient({}).client); + + ViemENSStore.fromRpcUrls({ [mainnet.id]: MAINNET_RPC }); + + expect(http).toHaveBeenCalledWith(MAINNET_RPC); + expect(createPublicClient).toHaveBeenCalledWith( + expect.objectContaining({ chain: mainnet, batch: { multicall: true } }), + ); + }); + + it('falls back to the default endpoint when no mainnet URL is provided', () => { + vi.mocked(createPublicClient).mockReturnValue(buildClient({}).client); + + ViemENSStore.fromRpcUrls({}); + + expect(http).toHaveBeenCalledWith(undefined); + }); + + it('produces a store that resolves names through the built client', async () => { + const { client } = buildClient({ [ALICE]: 'alice.eth' }); + vi.mocked(createPublicClient).mockReturnValue(client); + + const store = ViemENSStore.fromRpcUrls({ [mainnet.id]: MAINNET_RPC }); + const names = await store.lookUpPrimaryNames([address(ALICE)]); + + expect(names.get(address(ALICE).toHexString())?.toString()).toBe( + 'alice.eth', + ); + }); +}); + +describe('ViemENSStore.lookUpPrimaryNames', () => { + it('returns a map keyed by checksummed address for resolved names', async () => { + const store = new ViemENSStore( + buildClient({ [ALICE]: 'alice.eth' }).client, + ); + + const names = await store.lookUpPrimaryNames([address(ALICE)]); + + expect(names.get(address(ALICE).toHexString())?.toString()).toBe( + 'alice.eth', + ); + }); + + it('omits addresses with no primary name', async () => { + const store = new ViemENSStore( + buildClient({ [ALICE]: 'alice.eth', [BOB]: null }).client, + ); + + const names = await store.lookUpPrimaryNames([ + address(ALICE), + address(BOB), + ]); + + expect(names.has(address(ALICE).toHexString())).toBe(true); + expect(names.has(address(BOB).toHexString())).toBe(false); + }); + + it('degrades a per-address RPC failure to "no name" without failing the page', async () => { + const store = new ViemENSStore( + buildClient({ + [ALICE]: 'alice.eth', + [BOB]: new Error('rpc exploded'), + }).client, + ); + + const names = await store.lookUpPrimaryNames([ + address(ALICE), + address(BOB), + ]); + + expect(names.get(address(ALICE).toHexString())?.toString()).toBe( + 'alice.eth', + ); + expect(names.has(address(BOB).toHexString())).toBe(false); + }); + + it('drops a name that fails ENSIP-15 normalization', async () => { + const store = new ViemENSStore( + buildClient({ [ALICE]: 'has space.eth' }).client, + ); + + const names = await store.lookUpPrimaryNames([address(ALICE)]); + + expect(names.has(address(ALICE).toHexString())).toBe(false); + }); + + it('normalizes resolved names to their canonical form', async () => { + const store = new ViemENSStore( + buildClient({ [ALICE]: 'Alice.ETH' }).client, + ); + + const names = await store.lookUpPrimaryNames([address(ALICE)]); + + expect(names.get(address(ALICE).toHexString())?.toString()).toBe( + 'alice.eth', + ); + }); + + it('queries with the checksummed address and the legacy mainnet coin type by default', async () => { + const { client, getEnsName } = buildClient({ [ALICE]: 'alice.eth' }); + const store = new ViemENSStore(client); + + await store.lookUpPrimaryNames([address(ALICE)]); + + expect(getEnsName).toHaveBeenCalledWith({ + address: address(ALICE).toHexString(), + coinType: 60n, + }); + }); + + it('forwards an explicit coin type', async () => { + const { client, getEnsName } = buildClient({ [ALICE]: 'alice.eth' }); + const store = new ViemENSStore(client); + + await store.lookUpPrimaryNames([address(ALICE)], 2147483648n); + + expect(getEnsName).toHaveBeenCalledWith({ + address: address(ALICE).toHexString(), + coinType: 2147483648n, + }); + }); + + it('returns an empty map and issues no calls for no addresses', async () => { + const { client, getEnsName } = buildClient({}); + const store = new ViemENSStore(client); + + const names = await store.lookUpPrimaryNames([]); + + expect(names.size).toBe(0); + expect(getEnsName).not.toHaveBeenCalled(); + }); +}); diff --git a/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts b/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts new file mode 100644 index 0000000..5123cf7 --- /dev/null +++ b/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts @@ -0,0 +1,100 @@ +import { createPublicClient, http, type PublicClient } from 'viem'; +import { mainnet } from 'viem/chains'; +import { ENSName } from '@/domain/ens/ENSName'; +import type { ENSStore } from '@/domain/ens/ENSStore'; +import type { Address, HexString } from '@/domain/primitives'; +import type { RpcUrls } from '@/infrastructure/config/RpcUrls'; + +/** + * Legacy Ethereum mainnet coin type (ENSIP-11). The `addr.reverse` + * namespace, where the overwhelming majority of primary names live today. + */ +const LEGACY_MAINNET_COIN_TYPE = 60n; + +/** + * Resolves wallet addresses to their primary ENS names via viem's + * `getEnsName`. ENS reverse resolution executes on Ethereum L1, even for L2 + * names (which arrive via CCIP-Read), so this store always resolves against + * mainnet — `fromRpcUrls` reads the mainnet URL from the injected map. + * + * This class accounts for the safety considerations in the [SEAL security + * framework](https://frameworks.securityalliance.org/ens/overview): + * + * 1. Forward-resolution verification — `getEnsName` calls the + * UniversalResolver's `reverseWithGateways`, whose `reverse()` + * forward-resolves the candidate name back to the address on-chain and + * reverts (`ReverseAddressMismatch`) on a mismatch. This guarantee + * holds ONLY while the client's `universalResolverAddress` is the + * ENSIP-23 UniversalResolver (viem's mainnet default). Do not point it + * at a legacy resolver. + * 2. ENSIP-15 normalization — the resolved name is wrapped in `ENSName`, + * which normalizes on creation (viem `normalize`). viem does not + * auto-normalize the names it returns, so the value object owns this. + * 3. Cross-chain (ENSIP-19) — `coinType` selects which chain's primary + * name to read. We default to `60` (legacy mainnet); a chain-specific + * coin type would extend this to L2 / multichain primary names. + * + * Performance: lookups fan out concurrently and `fromRpcUrls` builds the + * client with `batch: { multicall: true }`, so a page's lookups collapse + * into a single multicall request. The store owns that flag — it is set + * here rather than trusting callers to configure the client correctly. A + * per-address failure (RPC error, CCIP-Read timeout, or an un-normalizable + * name) degrades to "no name" rather than failing the whole page; ENS is + * enrichment, not core membership data. + */ +export class ViemENSStore implements ENSStore { + constructor(private readonly client: PublicClient) {} + + /** + * Builds a store backed by a mainnet viem client constructed from the + * mainnet entry (chain id 1) of the RPC URL map, with multicall batching + * enabled. Falls back to viem's default public endpoint when no mainnet + * URL is provided. + */ + static fromRpcUrls(rpcUrls: RpcUrls): ViemENSStore { + const client = createPublicClient({ + chain: mainnet, + transport: http(rpcUrls[mainnet.id]), + batch: { multicall: true }, + }); + return new ViemENSStore(client); + } + + public async lookUpPrimaryNames( + addresses: Address[], + coinType: bigint = LEGACY_MAINNET_COIN_TYPE, + ): Promise> { + const names = new Map(); + if (addresses.length === 0) { + return names; + } + + const resolved = await Promise.all( + addresses.map((address) => this.resolvePrimaryName(address, coinType)), + ); + + for (const entry of resolved) { + if (entry != null) { + names.set(entry.key, entry.name); + } + } + + return names; + } + + private async resolvePrimaryName( + address: Address, + coinType: bigint, + ): Promise<{ key: HexString; name: ENSName } | null> { + const key = address.toHexString(); + try { + const name = await this.client.getEnsName({ address: key, coinType }); + if (name == null) { + return null; + } + return { key, name: ENSName.fromString(name) }; + } catch { + return null; + } + } +} diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts index 0006c03..1d61303 100644 --- a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts @@ -1,5 +1,7 @@ +import { ENSName } from '@/domain/ens/ENSName'; +import type { ENSStore } from '@/domain/ens/ENSStore'; import type { MemberStore } from '@/domain/member/MemberStore'; -import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; import { Address } from '@/domain/primitives'; import { createPage } from '@/domain/primitives/pagination/Page'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; @@ -9,33 +11,45 @@ import { GetTokenVotingMembershipUseCase } from './GetTokenVotingMembershipUseCa describe('GetTokenVotingMembershipUseCase', () => { const memberAddressValue = '0x1234567890abcdef1234567890abcdef12345678'; const memberAddress = Address.fromHexString(memberAddressValue); - const pluginAddress = '0x1111111111111111111111111111111111111111'; - const tokenContractAddress = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; + const pluginAddress = Address.fromHexString( + '0x1111111111111111111111111111111111111111', + ); + const tokenContractAddress = Address.fromHexString( + '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd', + ); const firstActivityTimestamp = 1705320000; const lastActivityTimestamp = 1718872200; - const buildMember = (ens: string | null = null): TokenVotingMember => - TokenVotingMember.create({ + const buildRecord = (): TokenVotingMemberRecord => + TokenVotingMemberRecord.create({ address: memberAddress, votingPower: VotingPower.fromBigInt(5000000000000000000n), - ens, firstActivityTimestamp, lastActivityTimestamp, delegationCount: 4, }); - const buildMockStore = ( - member: TokenVotingMember = buildMember(), + const buildMemberStore = ( + record: TokenVotingMemberRecord = buildRecord(), ): MemberStore => ({ findTokenVotingMembers: vi .fn() - .mockResolvedValue(createPage([member], 1, 20, 1)), + .mockResolvedValue(createPage([record], 1, 20, 1)), + }); + + const buildEnsStore = ( + names: Map = new Map(), + ): ENSStore => ({ + lookUpPrimaryNames: vi.fn().mockResolvedValue(names), }); const page = PageRequest.create({ page: 1, pageSize: 20 }); it('returns a page of members', async () => { - const useCase = new GetTokenVotingMembershipUseCase(buildMockStore()); + const useCase = new GetTokenVotingMembershipUseCase( + buildMemberStore(), + buildEnsStore(), + ); const result = await useCase.execute({ pluginAddress, tokenContractAddress, @@ -51,36 +65,79 @@ describe('GetTokenVotingMembershipUseCase', () => { expect(result.totalRecords).toBe(1); }); - it('passes plugin address, token address, and page to store', async () => { - const store = buildMockStore(); - const useCase = new GetTokenVotingMembershipUseCase(store); + it('passes plugin address, token address, and page to the member store', async () => { + const memberStore = buildMemberStore(); + const useCase = new GetTokenVotingMembershipUseCase( + memberStore, + buildEnsStore(), + ); await useCase.execute({ pluginAddress, tokenContractAddress, page }); - expect(store.findTokenVotingMembers).toHaveBeenCalledWith( + expect(memberStore.findTokenVotingMembers).toHaveBeenCalledWith( pluginAddress, tokenContractAddress, page, ); }); - it('forwards the ENS name attached to each TokenVotingMember', async () => { + it('enriches each member with the ENS name resolved for its address', async () => { + const names = new Map([ + [memberAddress.toHexString(), ENSName.fromString('alice.eth')], + ]); + const ensStore = buildEnsStore(names); + const useCase = new GetTokenVotingMembershipUseCase( + buildMemberStore(), + ensStore, + ); + + const result = await useCase.execute({ + pluginAddress, + tokenContractAddress, + page, + }); + + expect(result.items[0].ens?.toString()).toBe('alice.eth'); + // ENS lookups are scoped to the page's addresses. + expect(ensStore.lookUpPrimaryNames).toHaveBeenCalledWith([memberAddress]); + }); + + it('leaves ens null when the address has no resolved name', async () => { const useCase = new GetTokenVotingMembershipUseCase( - buildMockStore(buildMember('alice.eth')), + buildMemberStore(), + buildEnsStore(new Map()), ); + const result = await useCase.execute({ pluginAddress, tokenContractAddress, page, }); - expect(result.items[0].ens).toBe('alice.eth'); + expect(result.items[0].ens).toBeNull(); }); - it('wraps store errors', async () => { + it('wraps member-store errors', async () => { const failingStore: MemberStore = { findTokenVotingMembers: vi.fn().mockRejectedValue(new Error('db down')), }; - const useCase = new GetTokenVotingMembershipUseCase(failingStore); + const useCase = new GetTokenVotingMembershipUseCase( + failingStore, + buildEnsStore(), + ); + + await expect( + useCase.execute({ pluginAddress, tokenContractAddress, page }), + ).rejects.toThrow('Error while getting token-voting membership'); + }); + + it('wraps ENS-store errors', async () => { + const ensStore: ENSStore = { + lookUpPrimaryNames: vi.fn().mockRejectedValue(new Error('rpc down')), + }; + const useCase = new GetTokenVotingMembershipUseCase( + buildMemberStore(), + ensStore, + ); await expect( useCase.execute({ pluginAddress, tokenContractAddress, page }), diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.ts b/src/use-cases/GetTokenVotingMembershipUseCase.ts index c69ec1e..e7cbc4e 100644 --- a/src/use-cases/GetTokenVotingMembershipUseCase.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.ts @@ -1,19 +1,22 @@ import type { UseCase } from 'ddd-core-ts'; +import type { ENSStore } from '@/domain/ens/ENSStore'; import type { MemberStore } from '@/domain/member/MemberStore'; -import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import type { Address } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; +import { createPage } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; export interface GetTokenVotingMembershipUseCaseProps { - pluginAddress: string; - tokenContractAddress: string; + pluginAddress: Address; + tokenContractAddress: Address; page: PageRequest; } /** * Returns a page of members of an Aragon TokenVoting plugin, scoped - * to a specific ERC20Votes token contract and ordered by current - * voting power descending. + * to a specific token contract and ordered by current voting power + * descending. */ export class GetTokenVotingMembershipUseCase implements @@ -21,17 +24,39 @@ export class GetTokenVotingMembershipUseCase { public readonly code = 'GetTokenVotingMembershipUseCase'; - constructor(private readonly memberStore: MemberStore) {} + constructor( + private readonly memberStore: MemberStore, + private readonly ensStore: ENSStore, + ) {} public async execute( props: GetTokenVotingMembershipUseCaseProps, ): Promise> { try { - return await this.memberStore.findTokenVotingMembers( + const records = await this.memberStore.findTokenVotingMembers( props.pluginAddress, props.tokenContractAddress, props.page, ); + + const namesByAddress = await this.ensStore.lookUpPrimaryNames( + records.items.map((record) => record.address), + ); + + const members = records.items.map((record) => + TokenVotingMember.fromRecord( + record, + // ENSStore keys its map by checksummed hex address. + namesByAddress.get(record.address.toHexString()) ?? null, + ), + ); + + return createPage( + members, + records.page, + records.pageSize, + records.totalRecords, + ); } catch (cause) { throw new Error('Error while getting token-voting membership', { cause, diff --git a/test/getTokenVotingMembership.test.ts b/test/getTokenVotingMembership.test.ts index 3908979..95c94d8 100644 --- a/test/getTokenVotingMembership.test.ts +++ b/test/getTokenVotingMembership.test.ts @@ -1,28 +1,41 @@ import assert from 'node:assert'; +import { createPublicClient } from 'viem'; import { buildDomain } from './support/buildDomain'; -import { - ALICE, - BOB, - DEFAULT_EVM_COIN_TYPE, - PLUGIN, - TOKEN, -} from './support/constants'; +import { ALICE, BOB, PLUGIN, TOKEN } from './support/constants'; import { delegate, findMembersResponse, - reverseName, - reverseNamesResponse, } from './support/fixtures/tokenVotingMembers'; +// Stubbing `createPublicClient` so we can return a fake `getEnsName`that +// we can mock. +vi.mock('viem', async (importOriginal) => ({ + ...(await importOriginal()), + createPublicClient: vi.fn(), + http: vi.fn(), +})); + +function stubEnsNames(table: Record) { + const getEnsName = vi.fn( + async ({ address }: { address: string; coinType?: bigint }) => + table[address.toLowerCase()] ?? null, + ); + vi.mocked(createPublicClient).mockReturnValue({ getEnsName } as never); + return getEnsName; +} + +beforeEach(() => { + vi.clearAllMocks(); + stubEnsNames({}); +}); + describe('AragonDomain.getTokenVotingMembership', () => { it('returns a paginated DTO of token-voting members', async () => { + stubEnsNames({ [ALICE]: 'alice.eth' }); const { domain } = buildDomain([ findMembersResponse({ delegates: [delegate(ALICE, { votingPower: '5000000000000000000' })], }), - reverseNamesResponse([ - reverseName(ALICE, DEFAULT_EVM_COIN_TYPE, 'alice.eth'), - ]), ]); const response = await domain.getTokenVotingMembership({ @@ -49,12 +62,10 @@ describe('AragonDomain.getTokenVotingMembership', () => { expect(response.result.data[0].address).toMatch(/^0x[0-9a-fA-F]{40}$/); }); - it('attaches the ENS name from the indexer ReverseName entity', async () => { + it('attaches the primary ENS name resolved via the ENS client', async () => { + const getEnsName = stubEnsNames({ [ALICE]: 'alice.eth' }); const { domain } = buildDomain([ findMembersResponse({ delegates: [delegate(ALICE), delegate(BOB)] }), - reverseNamesResponse([ - reverseName(ALICE, DEFAULT_EVM_COIN_TYPE, 'alice.eth'), - ]), ]); const response = await domain.getTokenVotingMembership({ @@ -65,13 +76,14 @@ describe('AragonDomain.getTokenVotingMembership', () => { }); assert(response.success); + // ALICE resolves; BOB has no primary name -> null. expect(response.result.data.map((m) => m.ens)).toEqual(['alice.eth', null]); + expect(getEnsName).toHaveBeenCalledTimes(2); }); it('reflects a larger chain-wide total in the pagination metadata', async () => { const { domain } = buildDomain([ findMembersResponse({ delegates: [delegate(ALICE)], totalRecords: 42 }), - reverseNamesResponse(), ]); const response = await domain.getTokenVotingMembership({ @@ -86,7 +98,27 @@ describe('AragonDomain.getTokenVotingMembership', () => { expect(response.result.metadata.totalPages).toBe(3); }); - it('skips the reverse-name query when the page is empty', async () => { + it('resolves ENS out-of-band: one indexer query, names via the ENS client', async () => { + const getEnsName = stubEnsNames({ [ALICE]: 'alice.eth' }); + const { domain, query } = buildDomain([ + findMembersResponse({ delegates: [delegate(ALICE)] }), + ]); + + const response = await domain.getTokenVotingMembership({ + pluginAddress: PLUGIN, + tokenContractAddress: TOKEN, + page: 1, + pageSize: 20, + }); + + assert(response.success); + expect(response.result.data[0].ens).toBe('alice.eth'); + expect(query).toHaveBeenCalledTimes(1); + expect(getEnsName).toHaveBeenCalledTimes(1); + }); + + it('issues no ENS lookups when the page is empty', async () => { + const getEnsName = stubEnsNames({}); const { domain, query } = buildDomain([findMembersResponse()]); const response = await domain.getTokenVotingMembership({ @@ -98,8 +130,8 @@ describe('AragonDomain.getTokenVotingMembership', () => { assert(response.success); expect(response.result.data).toHaveLength(0); - // Only the members query ran; no second round-trip for ENS names. expect(query).toHaveBeenCalledTimes(1); + expect(getEnsName).not.toHaveBeenCalled(); }); it('returns a failed response when the indexer query errors', async () => { diff --git a/test/support/buildDomain.ts b/test/support/buildDomain.ts index 0e17464..aeca91b 100644 --- a/test/support/buildDomain.ts +++ b/test/support/buildDomain.ts @@ -1,4 +1,4 @@ -import { AragonDomain, EnvioClient } from '../../src'; +import { AragonDomain, EnvioClient, type RpcUrls } from '../../src'; export interface BuiltDomain { /** The public facade. Every integration test drives the stack through this. */ @@ -8,22 +8,17 @@ export interface BuiltDomain { } /** - * Builds an `AragonDomain` backed by an `EnvioClient` whose `query` - * method is replaced with a FIFO queue of canned responses. The - * underlying GraphQLClient is constructed but never touches the - * network. + * Builds an `AragonDomain` backed by an `EnvioClient` whose `query` method + * is replaced with a FIFO queue of canned responses (the GraphQLClient is + * constructed but never touches the network). * - * This is the single integration seam for the suite: only the GraphQL - * transport is faked, so the full stack runs — controller, request / - * response mappers, use case, store, store-side mappers, and domain - * objects. Entry is always `AragonDomain.load(...)` plus a public - * facade method; nothing reaches inside the controller. - * - * Responses are consumed in call order. Draining past the end is a - * test bug (the code issued more queries than the scenario set up), so - * it throws rather than returning `undefined`. + * Envio responses are consumed in call order. Draining past the end is a + * test bug, so it throws rather than returning `undefined`. */ -export function buildDomain(responses: unknown[]): BuiltDomain { +export function buildDomain( + responses: unknown[], + rpcUrls: RpcUrls = {}, +): BuiltDomain { const envio = new EnvioClient('https://unused.example.invalid'); const queue = [...responses]; const query = vi.fn(async () => { @@ -34,5 +29,5 @@ export function buildDomain(responses: unknown[]): BuiltDomain { }); vi.spyOn(envio, 'query').mockImplementation(query as never); - return { domain: AragonDomain.load(envio), query }; + return { domain: AragonDomain.load(envio, rpcUrls), query }; } diff --git a/test/support/constants.ts b/test/support/constants.ts index e93f54e..58d0e52 100644 --- a/test/support/constants.ts +++ b/test/support/constants.ts @@ -7,8 +7,3 @@ export const BOB = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; export const PLUGIN = '0x17a1688c56087ade762721180e1cc1e831c73719'; export const TOKEN = '0x0a830e9f2baa2ebaf8d33c0806283dea9c08952f'; - -/** Legacy ENS coinType (SLIP-44 ETH). */ -export const ETH_COIN_TYPE = '60'; -/** ENSIP-19 default EVM reverse coinType (0x80000000). */ -export const DEFAULT_EVM_COIN_TYPE = '2147483648'; diff --git a/test/support/fixtures/tokenVotingMembers.ts b/test/support/fixtures/tokenVotingMembers.ts index 394c394..a4b0413 100644 --- a/test/support/fixtures/tokenVotingMembers.ts +++ b/test/support/fixtures/tokenVotingMembers.ts @@ -38,8 +38,8 @@ export interface FindMembersResponseInput { } /** - * First query response: the page of delegates, the id-only set used - * for total-count, and the plugin's MemberMetrics. + * The members query response: the page of delegates, the id-only + * set used for total-count, and the plugin's MemberMetrics. */ export function findMembersResponse({ delegates = [], @@ -55,15 +55,3 @@ export function findMembersResponse({ MemberMetrics: metrics, }; } - -/** A single `ReverseName` row. */ -export function reverseName(address: string, coinType: string, name: string) { - return { address, coinType, name }; -} - -/** Second query response: the reverse-name rows for the page. */ -export function reverseNamesResponse( - rows: ReturnType[] = [], -) { - return { ReverseName: rows }; -} diff --git a/tsconfig.json b/tsconfig.json index f1be642..ac4fd35 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { "target": "es2022", - "module": "commonjs", + "module": "preserve", + "moduleResolution": "bundler", "lib": ["es2022"], "strict": true, "esModuleInterop": true, From 05cb5dbb0f9c2f45504dd9f41ccd194d6a7b3105 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:36:47 +0100 Subject: [PATCH 26/33] Move token voting member activity calculations to domain --- .../member/MemberGovernanceMetrics.test.ts | 30 ++++ src/domain/member/MemberGovernanceMetrics.ts | 45 ++++++ src/domain/member/MemberStore.ts | 11 +- src/domain/member/TokenVotingMember.test.ts | 147 ++++++++++++------ src/domain/member/TokenVotingMember.ts | 86 +++++----- .../member/TokenVotingMemberRecord.test.ts | 43 +++-- src/domain/member/TokenVotingMemberRecord.ts | 34 ++-- .../maps/domain/TokenVotingMemberMap.test.ts | 40 +++-- .../GetTokenVotingMembershipMap.test.ts | 24 +-- .../EnvioMemberStore/EnvioMemberStore.test.ts | 22 +-- .../EnvioMemberStore/EnvioMemberStore.ts | 40 +++-- .../maps/MemberGovernanceMetricsMap.test.ts | 32 ++++ .../maps/MemberGovernanceMetricsMap.ts | 37 +++++ .../maps/TokenVotingMemberMap.test.ts | 132 ---------------- .../maps/TokenVotingMemberMap.ts | 95 ----------- .../maps/TokenVotingMemberRecordMap.test.ts | 54 +++++++ .../maps/TokenVotingMemberRecordMap.ts | 55 +++++++ .../GetTokenVotingMembershipUseCase.test.ts | 23 ++- .../GetTokenVotingMembershipUseCase.ts | 19 +-- test/support/fixtures/tokenVotingMembers.ts | 4 +- 20 files changed, 568 insertions(+), 405 deletions(-) create mode 100644 src/domain/member/MemberGovernanceMetrics.test.ts create mode 100644 src/domain/member/MemberGovernanceMetrics.ts create mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.test.ts create mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.ts delete mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts delete mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts create mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts create mode 100644 src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.ts diff --git a/src/domain/member/MemberGovernanceMetrics.test.ts b/src/domain/member/MemberGovernanceMetrics.test.ts new file mode 100644 index 0000000..91a8325 --- /dev/null +++ b/src/domain/member/MemberGovernanceMetrics.test.ts @@ -0,0 +1,30 @@ +import { Address } from '@/domain/primitives'; +import { MemberGovernanceMetrics } from './MemberGovernanceMetrics'; + +describe('MemberGovernanceMetrics', () => { + const memberAddress = Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ); + + it('exposes its properties', () => { + const metrics = MemberGovernanceMetrics.create({ + memberAddress, + firstActivityTimestamp: 1650000000, + lastActivityTimestamp: 1750000000, + }); + + expect(metrics.memberAddress.equals(memberAddress)).toBe(true); + expect(metrics.firstActivityTimestamp).toBe(1650000000); + expect(metrics.lastActivityTimestamp).toBe(1750000000); + }); + + it('rejects a negative timestamp', () => { + expect(() => + MemberGovernanceMetrics.create({ + memberAddress, + firstActivityTimestamp: -1, + lastActivityTimestamp: 1750000000, + }), + ).toThrow(); + }); +}); diff --git a/src/domain/member/MemberGovernanceMetrics.ts b/src/domain/member/MemberGovernanceMetrics.ts new file mode 100644 index 0000000..68d0ff1 --- /dev/null +++ b/src/domain/member/MemberGovernanceMetrics.ts @@ -0,0 +1,45 @@ +import { ValueObject } from 'ddd-core-ts'; +import { z } from 'zod'; +import { Address } from '@/domain/primitives'; + +const MemberGovernanceMetricsPropsSchema = z.object({ + memberAddress: z.instanceof(Address), + firstActivityTimestamp: z.number().int().nonnegative(), + lastActivityTimestamp: z.number().int().nonnegative(), +}); + +type MemberGovernanceMetricsProps = z.infer< + typeof MemberGovernanceMetricsPropsSchema +>; + +/** + * A member's governance activity (votes cast or proposals created) within a + * plugin. + */ +export class MemberGovernanceMetrics extends ValueObject { + /** + * The member's account address. + */ + get memberAddress(): Address { + return this.props.memberAddress; + } + + /** + * Unix-seconds timestamp of the member's first governance action. + */ + get firstActivityTimestamp(): number { + return this.props.firstActivityTimestamp; + } + + /** + * Unix-seconds timestamp of the member's most recent governance action. + */ + get lastActivityTimestamp(): number { + return this.props.lastActivityTimestamp; + } + + static create(props: MemberGovernanceMetricsProps): MemberGovernanceMetrics { + const validated = MemberGovernanceMetricsPropsSchema.parse(props); + return new MemberGovernanceMetrics(validated); + } +} diff --git a/src/domain/member/MemberStore.ts b/src/domain/member/MemberStore.ts index 5174d97..9fec408 100644 --- a/src/domain/member/MemberStore.ts +++ b/src/domain/member/MemberStore.ts @@ -1,16 +1,23 @@ import type { Address } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; +import type { MemberGovernanceMetrics } from './MemberGovernanceMetrics'; import type { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; +export interface TokenVotingMemberData { + record: TokenVotingMemberRecord; + metrics: MemberGovernanceMetrics | null; +} + export interface MemberStore { /** * Finds members of a TokenVoting plugin scoped to its token contract. - * Returns a page of member records sorted by voting power descending. + * Returns a page of on-chain records paired with their governance + * metrics, sorted by voting power descending. */ findTokenVotingMembers( pluginAddress: Address, tokenContractAddress: Address, page: PageRequest, - ): Promise>; + ): Promise>; } diff --git a/src/domain/member/TokenVotingMember.test.ts b/src/domain/member/TokenVotingMember.test.ts index 82ff152..c69893d 100644 --- a/src/domain/member/TokenVotingMember.test.ts +++ b/src/domain/member/TokenVotingMember.test.ts @@ -1,6 +1,7 @@ import { ENSName } from '@/domain/ens/ENSName'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; +import { MemberGovernanceMetrics } from './MemberGovernanceMetrics'; import { TokenVotingMember } from './TokenVotingMember'; import { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; @@ -8,68 +9,118 @@ describe('TokenVotingMember', () => { const address = Address.fromHexString( '0x0123456789abcdef0123456789abcdef01234567', ); - const firstActivityTimestamp = 1705320000; - const lastActivityTimestamp = 1718872200; - const validProps = { - address, - ens: ENSName.fromString('alice.eth'), - votingPower: VotingPower.fromBigInt(1000000000000000000n), - firstActivityTimestamp, - lastActivityTimestamp, - delegationCount: 3, - }; - - const buildRecord = () => + const buildRecord = ( + overrides: Partial< + Parameters[0] + > = {}, + ) => TokenVotingMemberRecord.create({ address, votingPower: VotingPower.fromBigInt(1000000000000000000n), - firstActivityTimestamp, - lastActivityTimestamp, delegationCount: 3, + firstVotingPowerChangeTimestamp: 1700000000, + lastVotingPowerChangeTimestamp: 1700000100, + ...overrides, }); - it('creates a valid member', () => { - const member = TokenVotingMember.create(validProps); - expect(member.address.equals(address)).toBe(true); - expect(member.ens?.toString()).toBe('alice.eth'); - expect(member.votingPower.isZero).toBe(false); - expect(member.firstActivityTimestamp).toBe(firstActivityTimestamp); - expect(member.lastActivityTimestamp).toBe(lastActivityTimestamp); - }); + const buildMetrics = ( + overrides: Partial< + Parameters[0] + > = {}, + ) => + MemberGovernanceMetrics.create({ + memberAddress: address, + firstActivityTimestamp: 1650000000, + lastActivityTimestamp: 1750000000, + ...overrides, + }); - it('allows null ens', () => { - const member = TokenVotingMember.create({ ...validProps, ens: null }); - expect(member.ens).toBeNull(); - }); + describe('getters delegate to the source objects', () => { + it('pulls identity, voting power, and delegation count from the record', () => { + const member = TokenVotingMember.create(buildRecord(), null, null); + expect(member.address.equals(address)).toBe(true); + expect(member.votingPower.isZero).toBe(false); + expect(member.delegationCount).toBe(3); + }); - it('composes a record and a resolved name via fromRecord', () => { - const member = TokenVotingMember.fromRecord( - buildRecord(), - ENSName.fromString('alice.eth'), - ); - expect(member.address.equals(address)).toBe(true); - expect(member.ens?.toString()).toBe('alice.eth'); - expect(member.delegationCount).toBe(3); - }); + it('pulls the ENS name from the composed name', () => { + const named = TokenVotingMember.create( + buildRecord(), + null, + ENSName.fromString('alice.eth'), + ); + expect(named.ens?.toString()).toBe('alice.eth'); - it('composes a record with no name (null) via fromRecord', () => { - const member = TokenVotingMember.fromRecord(buildRecord(), null); - expect(member.ens).toBeNull(); + const anonymous = TokenVotingMember.create(buildRecord(), null, null); + expect(anonymous.ens).toBeNull(); + }); }); - it('structural equality', () => { - const a = TokenVotingMember.create(validProps); - const b = TokenVotingMember.create(validProps); - expect(a.equals(b)).toBe(true); + describe('activity window is derived in the getters', () => { + it('spans the earliest/latest across governance metrics and VP changes', () => { + const member = TokenVotingMember.create( + buildRecord(), + buildMetrics(), + null, + ); + // earliest first: metrics 1650000000 < record 1700000000 + expect(member.firstActivityTimestamp).toBe(1650000000); + // latest last: metrics 1750000000 > record 1700000100 + expect(member.lastActivityTimestamp).toBe(1750000000); + }); + + it('uses the VP-change window when there are no governance metrics', () => { + const member = TokenVotingMember.create(buildRecord(), null, null); + expect(member.firstActivityTimestamp).toBe(1700000000); + expect(member.lastActivityTimestamp).toBe(1700000100); + }); + + it('uses the governance window when the record has no VP changes', () => { + const member = TokenVotingMember.create( + buildRecord({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), + buildMetrics(), + null, + ); + expect(member.firstActivityTimestamp).toBe(1650000000); + expect(member.lastActivityTimestamp).toBe(1750000000); + }); + + it('reports 0 timestamps when no activity signal is present', () => { + const member = TokenVotingMember.create( + buildRecord({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), + null, + null, + ); + expect(member.firstActivityTimestamp).toBe(0); + expect(member.lastActivityTimestamp).toBe(0); + }); }); - it('structural inequality when votingPower differs', () => { - const a = TokenVotingMember.create(validProps); - const b = TokenVotingMember.create({ - ...validProps, - votingPower: VotingPower.zero(), + describe('equals', () => { + it('is structural over the composed objects', () => { + const record = buildRecord(); + const metrics = buildMetrics(); + const a = TokenVotingMember.create(record, metrics, null); + const b = TokenVotingMember.create(record, metrics, null); + expect(a.equals(b)).toBe(true); + }); + + it('differs when the composed record differs', () => { + const metrics = buildMetrics(); + const a = TokenVotingMember.create(buildRecord(), metrics, null); + const b = TokenVotingMember.create( + buildRecord({ votingPower: VotingPower.zero() }), + metrics, + null, + ); + expect(a.equals(b)).toBe(false); }); - expect(a.equals(b)).toBe(false); }); }); diff --git a/src/domain/member/TokenVotingMember.ts b/src/domain/member/TokenVotingMember.ts index fd31d9b..ad802c4 100644 --- a/src/domain/member/TokenVotingMember.ts +++ b/src/domain/member/TokenVotingMember.ts @@ -1,17 +1,15 @@ import { ValueObject } from 'ddd-core-ts'; import { z } from 'zod'; import { ENSName } from '@/domain/ens/ENSName'; -import { Address } from '@/domain/primitives'; -import { VotingPower } from '@/domain/voting-power/VotingPower'; -import type { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; +import type { Address } from '@/domain/primitives'; +import type { VotingPower } from '@/domain/voting-power/VotingPower'; +import { MemberGovernanceMetrics } from './MemberGovernanceMetrics'; +import { TokenVotingMemberRecord } from './TokenVotingMemberRecord'; const TokenVotingMemberPropsSchema = z.object({ - address: z.instanceof(Address), + record: z.instanceof(TokenVotingMemberRecord), + metrics: z.instanceof(MemberGovernanceMetrics).nullable(), ens: z.instanceof(ENSName).nullable(), - votingPower: z.instanceof(VotingPower), - firstActivityTimestamp: z.number().int().nonnegative(), - lastActivityTimestamp: z.number().int().nonnegative(), - delegationCount: z.number().int().nonnegative(), }); type TokenVotingMemberProps = z.infer; @@ -24,7 +22,7 @@ export class TokenVotingMember extends ValueObject { * The member's wallet address. */ get address(): Address { - return this.props.address; + return this.props.record.address; } /** @@ -39,51 +37,67 @@ export class TokenVotingMember extends ValueObject { * The member's voting power. */ get votingPower(): VotingPower { - return this.props.votingPower; + return this.props.record.votingPower; } /** - * Unix-seconds timestamp of the member's first observed activity. + * Number of distinct accounts currently delegating their voting power + * to this member (counts self-delegation). */ - get firstActivityTimestamp(): number { - return this.props.firstActivityTimestamp; + get delegationCount(): number { + return this.props.record.delegationCount; } /** - * Unix-seconds timestamp of the member's most recent observed activity. + * Unix-seconds timestamp of the member's first observed activity. + * "Activity" includes voting power changes, votes cast, and proposals + * created. `0` means "no activity recorded". */ - get lastActivityTimestamp(): number { - return this.props.lastActivityTimestamp; + get firstActivityTimestamp(): number { + return earliest([ + this.props.metrics?.firstActivityTimestamp, + this.props.record.firstVotingPowerChangeTimestamp, + ]); } /** - * Number of distinct accounts currently delegating their voting power - * to this member (counts self-delegation). + * Unix-seconds timestamp of the member's most recent observed activity. + * "Activity" includes voting power changes, votes cast, and proposals + * created. `0` means "no activity recorded". */ - get delegationCount(): number { - return this.props.delegationCount; - } - - static create(props: TokenVotingMemberProps): TokenVotingMember { - const validated = TokenVotingMemberPropsSchema.parse(props); - return new TokenVotingMember(validated); + get lastActivityTimestamp(): number { + return latest([ + this.props.metrics?.lastActivityTimestamp, + this.props.record.lastVotingPowerChangeTimestamp, + ]); } - /** - * Composes a fully-resolved member from its indexed on-chain record and - * primary ENS name. - */ - static fromRecord( + static create( record: TokenVotingMemberRecord, + metrics: MemberGovernanceMetrics | null, ens: ENSName | null, ): TokenVotingMember { - return TokenVotingMember.create({ - address: record.address, + const validated = TokenVotingMemberPropsSchema.parse({ + record, + metrics, ens, - votingPower: record.votingPower, - firstActivityTimestamp: record.firstActivityTimestamp, - lastActivityTimestamp: record.lastActivityTimestamp, - delegationCount: record.delegationCount, }); + return new TokenVotingMember(validated); } } + +/** + * Earliest of the defined unix-seconds signals; `0` when none is present. + */ +function earliest(signals: Array): number { + const defined = signals.filter((signal): signal is number => signal != null); + return defined.length > 0 ? Math.min(...defined) : 0; +} + +/** + * Latest of the defined unix-seconds signals; `0` when none is present. + */ +function latest(signals: Array): number { + const defined = signals.filter((signal): signal is number => signal != null); + return defined.length > 0 ? Math.max(...defined) : 0; +} diff --git a/src/domain/member/TokenVotingMemberRecord.test.ts b/src/domain/member/TokenVotingMemberRecord.test.ts index 31a9a75..54c50e8 100644 --- a/src/domain/member/TokenVotingMemberRecord.test.ts +++ b/src/domain/member/TokenVotingMemberRecord.test.ts @@ -7,26 +7,39 @@ describe('TokenVotingMemberRecord', () => { '0x0123456789abcdef0123456789abcdef01234567', ); - const validProps = { - address, - votingPower: VotingPower.fromBigInt(1000000000000000000n), - firstActivityTimestamp: 1705320000, - lastActivityTimestamp: 1718872200, - delegationCount: 3, - }; + const build = ( + overrides: Partial< + Parameters[0] + > = {}, + ) => + TokenVotingMemberRecord.create({ + address, + votingPower: VotingPower.fromBigInt(1000000000000000000n), + delegationCount: 2, + firstVotingPowerChangeTimestamp: 1700000000, + lastVotingPowerChangeTimestamp: 1700000100, + ...overrides, + }); - it('creates a valid record', () => { - const record = TokenVotingMemberRecord.create(validProps); + it('exposes its properties', () => { + const record = build(); expect(record.address.equals(address)).toBe(true); expect(record.votingPower.isZero).toBe(false); - expect(record.firstActivityTimestamp).toBe(1705320000); - expect(record.lastActivityTimestamp).toBe(1718872200); - expect(record.delegationCount).toBe(3); + expect(record.delegationCount).toBe(2); + expect(record.firstVotingPowerChangeTimestamp).toBe(1700000000); + expect(record.lastVotingPowerChangeTimestamp).toBe(1700000100); + }); + + it('allows null voting-power-change timestamps', () => { + const record = build({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }); + expect(record.firstVotingPowerChangeTimestamp).toBeNull(); + expect(record.lastVotingPowerChangeTimestamp).toBeNull(); }); it('rejects a negative delegation count', () => { - expect(() => - TokenVotingMemberRecord.create({ ...validProps, delegationCount: -1 }), - ).toThrow(); + expect(() => build({ delegationCount: -1 })).toThrow(); }); }); diff --git a/src/domain/member/TokenVotingMemberRecord.ts b/src/domain/member/TokenVotingMemberRecord.ts index 986cc1e..5eb07d1 100644 --- a/src/domain/member/TokenVotingMemberRecord.ts +++ b/src/domain/member/TokenVotingMemberRecord.ts @@ -6,9 +6,9 @@ import { VotingPower } from '@/domain/voting-power/VotingPower'; const TokenVotingMemberRecordPropsSchema = z.object({ address: z.instanceof(Address), votingPower: z.instanceof(VotingPower), - firstActivityTimestamp: z.number().int().nonnegative(), - lastActivityTimestamp: z.number().int().nonnegative(), delegationCount: z.number().int().nonnegative(), + firstVotingPowerChangeTimestamp: z.number().int().nonnegative().nullable(), + lastVotingPowerChangeTimestamp: z.number().int().nonnegative().nullable(), }); type TokenVotingMemberRecordProps = z.infer< @@ -16,43 +16,47 @@ type TokenVotingMemberRecordProps = z.infer< >; /** - * A TokenVoting member as derived purely from on-chain state. + * A TokenVoting member as derived purely from on-chain + * delegate state: their voting power, delegation count, and the window + * of their voting-power changes. */ export class TokenVotingMemberRecord extends ValueObject { /** - * The member's wallet address. + * The member's account address. */ get address(): Address { return this.props.address; } /** - * The member's voting power. + * The member's current voting power. */ get votingPower(): VotingPower { return this.props.votingPower; } /** - * Unix-seconds timestamp of the member's first observed activity. + * Number of distinct accounts delegating their voting power to this + * member (counts self-delegation). */ - get firstActivityTimestamp(): number { - return this.props.firstActivityTimestamp; + get delegationCount(): number { + return this.props.delegationCount; } /** - * Unix-seconds timestamp of the member's most recent observed activity. + * Unix-seconds timestamp of the member's first voting-power change, + * or null when no such change has been observed. */ - get lastActivityTimestamp(): number { - return this.props.lastActivityTimestamp; + get firstVotingPowerChangeTimestamp(): number | null { + return this.props.firstVotingPowerChangeTimestamp; } /** - * Number of distinct accounts currently delegating their voting power - * to this member (counts self-delegation). + * Unix-seconds timestamp of the member's most recent voting-power + * change, or null when no such change has been observed. */ - get delegationCount(): number { - return this.props.delegationCount; + get lastVotingPowerChangeTimestamp(): number | null { + return this.props.lastVotingPowerChangeTimestamp; } static create(props: TokenVotingMemberRecordProps): TokenVotingMemberRecord { diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts index 0f1f5fb..ddc4b1a 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.test.ts @@ -1,5 +1,6 @@ import { ENSName } from '@/domain/ens/ENSName'; import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; import { Address } from '@/domain/primitives'; import { VotingPower } from '@/domain/voting-power/VotingPower'; import { mapDomainToDTO } from './TokenVotingMemberMap'; @@ -10,17 +11,29 @@ describe('TokenVotingMemberMap.mapDomainToDTO', () => { ); const buildMember = ( - overrides: Partial[0]> = {}, - ) => - TokenVotingMember.create({ - address, - ens: ENSName.fromString('alice.eth'), - votingPower: VotingPower.fromBigInt(5000000000000000000n), - firstActivityTimestamp: 1705320000, - lastActivityTimestamp: 1718872200, - delegationCount: 3, - ...overrides, - }); + opts: { + ens?: ENSName | null; + firstVotingPowerChangeTimestamp?: number | null; + lastVotingPowerChangeTimestamp?: number | null; + } = {}, + ) => { + const { + ens = ENSName.fromString('alice.eth'), + firstVotingPowerChangeTimestamp = 1705320000, + lastVotingPowerChangeTimestamp = 1718872200, + } = opts; + return TokenVotingMember.create( + TokenVotingMemberRecord.create({ + address, + votingPower: VotingPower.fromBigInt(5000000000000000000n), + delegationCount: 3, + firstVotingPowerChangeTimestamp, + lastVotingPowerChangeTimestamp, + }), + null, + ens, + ); + }; it('maps a Member with full activity to a DTO', () => { const dto = mapDomainToDTO(buildMember()); @@ -37,7 +50,10 @@ describe('TokenVotingMemberMap.mapDomainToDTO', () => { it('serializes zero activity timestamps as null (no recorded activity)', () => { const dto = mapDomainToDTO( - buildMember({ firstActivityTimestamp: 0, lastActivityTimestamp: 0 }), + buildMember({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), ); expect(dto.metrics.firstActivityTimestamp).toBeNull(); diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts index ab312e6..4d9ba38 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.test.ts @@ -1,5 +1,6 @@ import { ENSName } from '@/domain/ens/ENSName'; import { TokenVotingMember } from '@/domain/member/TokenVotingMember'; +import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; import { Address } from '@/domain/primitives'; import { createPage } from '@/domain/primitives/pagination/Page'; import { VotingPower } from '@/domain/voting-power/VotingPower'; @@ -45,16 +46,19 @@ describe('GetTokenVotingMembershipMap', () => { describe('mapDomainToDTO', () => { it('maps a domain page to a PageDTO of TokenVotingMemberDTO', () => { - const member = TokenVotingMember.create({ - address: Address.fromHexString( - '0x0123456789abcdef0123456789abcdef01234567', - ), - ens: ENSName.fromString('alice.eth'), - votingPower: VotingPower.fromBigInt(5000000000000000000n), - firstActivityTimestamp: 1705320000, - lastActivityTimestamp: 1718872200, - delegationCount: 3, - }); + const member = TokenVotingMember.create( + TokenVotingMemberRecord.create({ + address: Address.fromHexString( + '0x0123456789abcdef0123456789abcdef01234567', + ), + votingPower: VotingPower.fromBigInt(5000000000000000000n), + delegationCount: 3, + firstVotingPowerChangeTimestamp: 1705320000, + lastVotingPowerChangeTimestamp: 1718872200, + }), + null, + ENSName.fromString('alice.eth'), + ); const page = createPage([member], 1, 20, 1); const dto = mapDomainToDTO(page); diff --git a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts index f053499..8fb9de9 100644 --- a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.test.ts @@ -57,12 +57,12 @@ const buildDelegate = ( describe('EnvioMemberStore', () => { const page = PageRequest.create({ page: 1, pageSize: 20 }); - it('returns a page of member records in a single query (no ENS round-trip)', async () => { + it('returns a page of record/metrics pairs in a single query (no ENS round-trip)', async () => { const { envio, calls } = buildMockEnvioClient([ { ERC20VotesDelegate: [buildDelegate(ALICE), buildDelegate(BOB)], AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }], - MemberMetrics: [], + MemberGovernanceMetrics: [], }, ]); const store = new EnvioMemberStore(envio); @@ -76,14 +76,16 @@ describe('EnvioMemberStore', () => { expect(result.items).toHaveLength(2); expect(result.totalRecords).toBe(2); expect(calls).toHaveLength(1); + // No governance metrics for these members → null on each pair. + expect(result.items[0].metrics).toBeNull(); }); - it('merges MemberMetrics activity into the matching delegate record', async () => { + it('pairs MemberGovernanceMetrics with the matching record by address', async () => { const { envio } = buildMockEnvioClient([ { ERC20VotesDelegate: [buildDelegate(ALICE)], AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [ + MemberGovernanceMetrics: [ { id: `1-${PLUGIN}-${ALICE}`, chainId: 1, @@ -103,11 +105,11 @@ describe('EnvioMemberStore', () => { page, ); - // metrics first-activity (1650000000) is earlier than the delegate's - // first VP change (1700000000); metrics last-activity (1750000000) is - // later than the delegate's last VP change (1700000100). - expect(result.items[0].firstActivityTimestamp).toBe(1650000000); - expect(result.items[0].lastActivityTimestamp).toBe(1750000000); + const { record, metrics } = result.items[0]; + expect(record.address.toHexString().toLowerCase()).toBe(ALICE); + expect(record.firstVotingPowerChangeTimestamp).toBe(1700000000); + expect(metrics?.firstActivityTimestamp).toBe(1650000000); + expect(metrics?.lastActivityTimestamp).toBe(1750000000); }); it('returns an empty page when there are no delegates', async () => { @@ -115,7 +117,7 @@ describe('EnvioMemberStore', () => { { ERC20VotesDelegate: [], AllERC20VotesDelegate: [], - MemberMetrics: [], + MemberGovernanceMetrics: [], }, ]); const store = new EnvioMemberStore(envio); diff --git a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts index ff8fd1d..cf5fdaa 100644 --- a/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts +++ b/src/infrastructure/stores/EnvioMemberStore/EnvioMemberStore.ts @@ -1,16 +1,17 @@ -import type { MemberStore } from '@/domain/member/MemberStore'; -import type { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; +import type { + MemberStore, + TokenVotingMemberData, +} from '@/domain/member/MemberStore'; import type { Address } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import { createPage } from '@/domain/primitives/pagination/Page'; import type { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { EnvioClient } from '@/infrastructure/stores/EnvioClient'; -import * as TokenVotingMemberMap from './maps/TokenVotingMemberMap'; +import * as MemberGovernanceMetricsMap from './maps/MemberGovernanceMetricsMap'; +import * as TokenVotingMemberRecordMap from './maps/TokenVotingMemberRecordMap'; /** - * Fetches a page of delegates by token (ordered by VP desc), every - * delegate's id (for total-count), and the MemberMetrics for the - * plugin. + * Fetches a page of members by token (ordered by VP desc). */ const FIND_MEMBERS_QUERY = ` query FindMembers( @@ -45,7 +46,7 @@ const FIND_MEMBERS_QUERY = ` ) { id } - MemberMetrics( + MemberGovernanceMetrics( where: { pluginAddress: { _eq: $pluginAddress } } @@ -67,7 +68,7 @@ export class EnvioMemberStore implements MemberStore { pluginAddress: Address, tokenContractAddress: Address, request: PageRequest, - ): Promise> { + ): Promise> { try { // The indexer stores addresses lowercased; serialize the primitives // to lowercase hex for the query variables. @@ -76,7 +77,7 @@ export class EnvioMemberStore implements MemberStore { .toHexString() .toLowerCase(); - const rawMembers = await this.envio.query(FIND_MEMBERS_QUERY, { + const raw = await this.envio.query(FIND_MEMBERS_QUERY, { tokenContractAddress: tokenAddressLower, pluginAddress: pluginAddressLower, limit: request.pageSize, @@ -84,9 +85,26 @@ export class EnvioMemberStore implements MemberStore { }); const { records, totalRecords } = - TokenVotingMemberMap.mapDTOToDomain(rawMembers); + TokenVotingMemberRecordMap.mapDTOToDomain(raw); + const metrics = MemberGovernanceMetricsMap.mapDTOToDomain(raw); - return createPage(records, request.page, request.pageSize, totalRecords); + const metricsByMember = new Map( + metrics.map((entry) => [ + entry.memberAddress.toHexString().toLowerCase(), + entry, + ]), + ); + + // Pair each on-chain record with its governance metrics (one query, + // so the join happens here); ENS is resolved in the use case. + const data = records.map((record) => ({ + record, + metrics: + metricsByMember.get(record.address.toHexString().toLowerCase()) ?? + null, + })); + + return createPage(data, request.page, request.pageSize, totalRecords); } catch (cause) { throw new Error('Error querying members from Envio', { cause }); } diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.test.ts new file mode 100644 index 0000000..c6b970d --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.test.ts @@ -0,0 +1,32 @@ +import { mapDTOToDomain } from './MemberGovernanceMetricsMap'; + +const PLUGIN = '0x1111111111111111111111111111111111111111'; +const MEMBER = '0x0123456789abcdef0123456789abcdef01234567'; + +const buildMetrics = (overrides: Record = {}) => ({ + id: `1-${PLUGIN}-${MEMBER}`, + chainId: 1, + pluginAddress: PLUGIN, + memberAddress: MEMBER, + firstActivityTimestamp: '1650000000', + lastActivityTimestamp: '1750000000', + ...overrides, +}); + +describe('MemberGovernanceMetricsMap.mapDTOToDomain', () => { + it('throws when the response shape does not match', () => { + expect(() => mapDTOToDomain({ MemberGovernanceMetrics: 'oops' })).toThrow(); + expect(() => mapDTOToDomain(null)).toThrow(); + }); + + it('maps governance-metrics rows to domain objects', () => { + const metrics = mapDTOToDomain({ + MemberGovernanceMetrics: [buildMetrics()], + }); + + expect(metrics).toHaveLength(1); + expect(metrics[0].memberAddress.toHexString().toLowerCase()).toBe(MEMBER); + expect(metrics[0].firstActivityTimestamp).toBe(1650000000); + expect(metrics[0].lastActivityTimestamp).toBe(1750000000); + }); +}); diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.ts new file mode 100644 index 0000000..9babf03 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberStore/maps/MemberGovernanceMetricsMap.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { MemberGovernanceMetrics } from '@/domain/member/MemberGovernanceMetrics'; +import { Address, zExtended } from '@/domain/primitives'; + +const MemberGovernanceMetricsSchema = z.object({ + id: z.string(), + chainId: z.number(), + pluginAddress: zExtended.hexString(), + memberAddress: zExtended.hexString(), + firstActivityTimestamp: z.string(), + lastActivityTimestamp: z.string(), +}); + +/** + * Slice of the `FindMembers` response owned by this mapper: the + * governance-metrics rows for the plugin. Other top-level lists in the + * response are ignored. + */ +const ResponseSchema = z.object({ + MemberGovernanceMetrics: z.array(MemberGovernanceMetricsSchema), +}); + +export type MemberGovernanceMetricsDTO = z.infer< + typeof MemberGovernanceMetricsSchema +>; + +export function mapDTOToDomain(raw: unknown): MemberGovernanceMetrics[] { + const data = ResponseSchema.parse(raw); + + return data.MemberGovernanceMetrics.map((metrics) => + MemberGovernanceMetrics.create({ + memberAddress: Address.fromHexString(metrics.memberAddress), + firstActivityTimestamp: Number(metrics.firstActivityTimestamp), + lastActivityTimestamp: Number(metrics.lastActivityTimestamp), + }), + ); +} diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts deleted file mode 100644 index fd93ca7..0000000 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.test.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - type ERC20VotesDelegateDTO, - type MemberMetricsDTO, - mapDTOToDomain, -} from './TokenVotingMemberMap'; - -const PLUGIN = '0x1111111111111111111111111111111111111111'; -const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; -const MEMBER = '0x0123456789abcdef0123456789abcdef01234567'; - -const buildDelegate = ( - overrides: Partial = {}, -): ERC20VotesDelegateDTO => ({ - id: `1-${TOKEN}-${MEMBER}`, - chainId: 1, - tokenContractAddress: TOKEN, - delegateAddress: MEMBER, - votingPower: '5000000000000000000', - delegationCount: 1, - firstVotingPowerChangeTimestamp: '1700000000', - lastVotingPowerChangeTimestamp: '1700000100', - ...overrides, -}); - -const buildMetrics = ( - overrides: Partial = {}, -): MemberMetricsDTO => ({ - id: `1-${PLUGIN}-${MEMBER}`, - chainId: 1, - pluginAddress: PLUGIN, - memberAddress: MEMBER, - firstActivityTimestamp: '1650000000', - lastActivityTimestamp: '1750000000', - ...overrides, -}); - -const buildResponse = (overrides: { - ERC20VotesDelegate?: ERC20VotesDelegateDTO[]; - AllERC20VotesDelegate?: Array<{ id: string }>; - MemberMetrics?: MemberMetricsDTO[]; -}) => ({ - ERC20VotesDelegate: overrides.ERC20VotesDelegate ?? [], - AllERC20VotesDelegate: overrides.AllERC20VotesDelegate ?? [], - MemberMetrics: overrides.MemberMetrics ?? [], -}); - -describe('mapDTOToDomain', () => { - it('throws when the response shape does not match', () => { - expect(() => mapDTOToDomain({ ERC20VotesDelegate: 'oops' })).toThrow(); - expect(() => mapDTOToDomain(null)).toThrow(); - }); - - it('returns a record per delegate plus the chain-wide total count', () => { - const { records, totalRecords } = mapDTOToDomain( - buildResponse({ - ERC20VotesDelegate: [buildDelegate()], - AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], - MemberMetrics: [buildMetrics()], - }), - ); - - expect(records).toHaveLength(1); - expect(totalRecords).toBe(3); - }); - - it('pairs each delegate with its companion MemberMetrics row by address', () => { - const { records } = mapDTOToDomain( - buildResponse({ - ERC20VotesDelegate: [buildDelegate()], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [buildMetrics()], - }), - ); - - // metrics.firstActivityTimestamp (1650000000) is earlier than the - // delegate's first VP change (1700000000); metrics.lastActivityTimestamp - // (1750000000) is later than the delegate's last VP change (1700000100). - expect(records[0].firstActivityTimestamp).toBe(1650000000); - expect(records[0].lastActivityTimestamp).toBe(1750000000); - }); - - it('takes the earliest/latest activity when the delegate signals are the extremes', () => { - const { records } = mapDTOToDomain( - buildResponse({ - ERC20VotesDelegate: [ - buildDelegate({ - firstVotingPowerChangeTimestamp: '1600000000', - lastVotingPowerChangeTimestamp: '1800000000', - }), - ], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [buildMetrics()], - }), - ); - - expect(records[0].firstActivityTimestamp).toBe(1600000000); - expect(records[0].lastActivityTimestamp).toBe(1800000000); - }); - - it('reports 0 timestamps when neither signal is present', () => { - const { records } = mapDTOToDomain( - buildResponse({ - ERC20VotesDelegate: [ - buildDelegate({ - firstVotingPowerChangeTimestamp: null, - lastVotingPowerChangeTimestamp: null, - }), - ], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }), - ); - - expect(records[0].firstActivityTimestamp).toBe(0); - expect(records[0].lastActivityTimestamp).toBe(0); - }); - - it('maps voting power and delegation count onto the record', () => { - const { records } = mapDTOToDomain( - buildResponse({ - ERC20VotesDelegate: [ - buildDelegate({ votingPower: '42', delegationCount: 7 }), - ], - AllERC20VotesDelegate: [{ id: 'a' }], - MemberMetrics: [], - }), - ); - - expect(records[0].votingPower.value.toBigNumber().toFixed(0)).toBe('42'); - expect(records[0].delegationCount).toBe(7); - }); -}); diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts deleted file mode 100644 index b16ca11..0000000 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberMap.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { z } from 'zod'; -import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; -import { Address, assertHexString, zExtended } from '@/domain/primitives'; -import { VotingPower } from '@/domain/voting-power/VotingPower'; - -const ERC20VotesDelegateSchema = z.object({ - id: z.string(), - chainId: z.number(), - tokenContractAddress: zExtended.hexString(), - delegateAddress: zExtended.hexString(), - votingPower: z.string(), - delegationCount: z.number(), - firstVotingPowerChangeTimestamp: z.string().nullable(), - lastVotingPowerChangeTimestamp: z.string().nullable(), -}); - -const MemberMetricsSchema = z.object({ - id: z.string(), - chainId: z.number(), - pluginAddress: zExtended.hexString(), - memberAddress: zExtended.hexString(), - firstActivityTimestamp: z.string(), - lastActivityTimestamp: z.string(), -}); - -const FindMembersResponseSchema = z.object({ - ERC20VotesDelegate: z.array(ERC20VotesDelegateSchema), - AllERC20VotesDelegate: z.array(z.object({ id: z.string() })), - MemberMetrics: z.array(MemberMetricsSchema), -}); - -export type ERC20VotesDelegateDTO = z.infer; -export type MemberMetricsDTO = z.infer; -export type FindMembersResponse = z.infer; - -interface FindMembersResult { - records: TokenVotingMemberRecord[]; - totalRecords: number; -} - -export function mapDTOToDomain(raw: unknown): FindMembersResult { - const data = FindMembersResponseSchema.parse(raw); - - const metricsByMember = new Map( - data.MemberMetrics.map((metrics) => [ - metrics.memberAddress.toLowerCase(), - metrics, - ]), - ); - - const records = data.ERC20VotesDelegate.map((delegate) => - toRecord( - delegate, - metricsByMember.get(delegate.delegateAddress.toLowerCase()), - ), - ); - - return { records, totalRecords: data.AllERC20VotesDelegate.length }; -} - -/** - * Assembles a single domain `TokenVotingMemberRecord` from a delegate - * row and its optional companion `MemberMetrics` row. - */ -function toRecord( - delegate: ERC20VotesDelegateDTO, - metrics: MemberMetricsDTO | undefined, -): TokenVotingMemberRecord { - // A member's activity window spans both their governance activity - // (MemberMetrics: VoteCast / ProposalCreated) and their voting-power - // changes (ERC20VotesDelegate). Take the earliest start and latest end - // across whichever signals are present; 0 means "no activity recorded". - const firstSignals = [ - metrics?.firstActivityTimestamp, - delegate.firstVotingPowerChangeTimestamp, - ] - .filter((t): t is string => t != null) - .map(Number); - const lastSignals = [ - metrics?.lastActivityTimestamp, - delegate.lastVotingPowerChangeTimestamp, - ] - .filter((t): t is string => t != null) - .map(Number); - - return TokenVotingMemberRecord.create({ - address: Address.fromHexString(delegate.delegateAddress), - votingPower: VotingPower.fromBigInt(BigInt(delegate.votingPower)), - firstActivityTimestamp: - firstSignals.length > 0 ? Math.min(...firstSignals) : 0, - lastActivityTimestamp: - lastSignals.length > 0 ? Math.max(...lastSignals) : 0, - delegationCount: delegate.delegationCount, - }); -} diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts new file mode 100644 index 0000000..6d21ba1 --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts @@ -0,0 +1,54 @@ +import { mapDTOToDomain } from './TokenVotingMemberRecordMap'; + +const TOKEN = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'; +const MEMBER = '0x0123456789abcdef0123456789abcdef01234567'; + +const buildDelegate = (overrides: Record = {}) => ({ + id: `1-${TOKEN}-${MEMBER}`, + chainId: 1, + tokenContractAddress: TOKEN, + delegateAddress: MEMBER, + votingPower: '5000000000000000000', + delegationCount: 1, + firstVotingPowerChangeTimestamp: '1700000000', + lastVotingPowerChangeTimestamp: '1700000100', + ...overrides, +}); + +describe('TokenVotingMemberRecordMap.mapDTOToDomain', () => { + it('throws when the response shape does not match', () => { + expect(() => mapDTOToDomain({ ERC20VotesDelegate: 'oops' })).toThrow(); + expect(() => mapDTOToDomain(null)).toThrow(); + }); + + it('maps records and reports the chain-wide total', () => { + const { records, totalRecords } = mapDTOToDomain({ + ERC20VotesDelegate: [buildDelegate()], + AllERC20VotesDelegate: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], + }); + + expect(records).toHaveLength(1); + expect(totalRecords).toBe(3); + expect(records[0].votingPower.value.toBigNumber().toFixed(0)).toBe( + '5000000000000000000', + ); + expect(records[0].delegationCount).toBe(1); + expect(records[0].firstVotingPowerChangeTimestamp).toBe(1700000000); + expect(records[0].lastVotingPowerChangeTimestamp).toBe(1700000100); + }); + + it('preserves null voting-power-change timestamps', () => { + const { records } = mapDTOToDomain({ + ERC20VotesDelegate: [ + buildDelegate({ + firstVotingPowerChangeTimestamp: null, + lastVotingPowerChangeTimestamp: null, + }), + ], + AllERC20VotesDelegate: [{ id: 'a' }], + }); + + expect(records[0].firstVotingPowerChangeTimestamp).toBeNull(); + expect(records[0].lastVotingPowerChangeTimestamp).toBeNull(); + }); +}); diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.ts new file mode 100644 index 0000000..4cc1ead --- /dev/null +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.ts @@ -0,0 +1,55 @@ +import { z } from 'zod'; +import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; +import { Address, zExtended } from '@/domain/primitives'; +import { VotingPower } from '@/domain/voting-power/VotingPower'; + +const ERC20VotesDelegateSchema = z.object({ + id: z.string(), + chainId: z.number(), + tokenContractAddress: zExtended.hexString(), + delegateAddress: zExtended.hexString(), + votingPower: z.string(), + delegationCount: z.number(), + firstVotingPowerChangeTimestamp: z.string().nullable(), + lastVotingPowerChangeTimestamp: z.string().nullable(), +}); + +/** + * Slice of the `FindMembers` response owned by this mapper: the requested + * page of delegates plus the id-only list used for the chain-wide total + * count. Other top-level lists in the response are ignored. + */ +const ResponseSchema = z.object({ + ERC20VotesDelegate: z.array(ERC20VotesDelegateSchema), + AllERC20VotesDelegate: z.array(z.object({ id: z.string() })), +}); + +export interface TokenVotingMemberRecordsResult { + records: TokenVotingMemberRecord[]; + totalRecords: number; +} + +export function mapDTOToDomain(raw: unknown): TokenVotingMemberRecordsResult { + const data = ResponseSchema.parse(raw); + + const records = data.ERC20VotesDelegate.map((delegate) => + TokenVotingMemberRecord.create({ + address: Address.fromHexString(delegate.delegateAddress), + votingPower: VotingPower.fromBigInt(BigInt(delegate.votingPower)), + delegationCount: delegate.delegationCount, + firstVotingPowerChangeTimestamp: toTimestamp( + delegate.firstVotingPowerChangeTimestamp, + ), + lastVotingPowerChangeTimestamp: toTimestamp( + delegate.lastVotingPowerChangeTimestamp, + ), + }), + ); + + return { records, totalRecords: data.AllERC20VotesDelegate.length }; +} + +/** Parses a nullable unix-seconds string into a number, preserving null. */ +function toTimestamp(value: string | null): number | null { + return value != null ? Number(value) : null; +} diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts index 1d61303..2165844 100644 --- a/src/use-cases/GetTokenVotingMembershipUseCase.test.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.test.ts @@ -1,6 +1,9 @@ import { ENSName } from '@/domain/ens/ENSName'; import type { ENSStore } from '@/domain/ens/ENSStore'; -import type { MemberStore } from '@/domain/member/MemberStore'; +import type { + MemberStore, + TokenVotingMemberData, +} from '@/domain/member/MemberStore'; import { TokenVotingMemberRecord } from '@/domain/member/TokenVotingMemberRecord'; import { Address } from '@/domain/primitives'; import { createPage } from '@/domain/primitives/pagination/Page'; @@ -20,21 +23,25 @@ describe('GetTokenVotingMembershipUseCase', () => { const firstActivityTimestamp = 1705320000; const lastActivityTimestamp = 1718872200; - const buildRecord = (): TokenVotingMemberRecord => - TokenVotingMemberRecord.create({ + // The member store returns record/metrics pairs (no ENS); here the + // activity window comes from the record's voting-power changes. + const buildData = (): TokenVotingMemberData => ({ + record: TokenVotingMemberRecord.create({ address: memberAddress, votingPower: VotingPower.fromBigInt(5000000000000000000n), - firstActivityTimestamp, - lastActivityTimestamp, delegationCount: 4, - }); + firstVotingPowerChangeTimestamp: firstActivityTimestamp, + lastVotingPowerChangeTimestamp: lastActivityTimestamp, + }), + metrics: null, + }); const buildMemberStore = ( - record: TokenVotingMemberRecord = buildRecord(), + data: TokenVotingMemberData = buildData(), ): MemberStore => ({ findTokenVotingMembers: vi .fn() - .mockResolvedValue(createPage([record], 1, 20, 1)), + .mockResolvedValue(createPage([data], 1, 20, 1)), }); const buildEnsStore = ( diff --git a/src/use-cases/GetTokenVotingMembershipUseCase.ts b/src/use-cases/GetTokenVotingMembershipUseCase.ts index e7cbc4e..e1688dc 100644 --- a/src/use-cases/GetTokenVotingMembershipUseCase.ts +++ b/src/use-cases/GetTokenVotingMembershipUseCase.ts @@ -33,29 +33,30 @@ export class GetTokenVotingMembershipUseCase props: GetTokenVotingMembershipUseCaseProps, ): Promise> { try { - const records = await this.memberStore.findTokenVotingMembers( + const memberPage = await this.memberStore.findTokenVotingMembers( props.pluginAddress, props.tokenContractAddress, props.page, ); const namesByAddress = await this.ensStore.lookUpPrimaryNames( - records.items.map((record) => record.address), + memberPage.items.map((data) => data.record.address), ); - const members = records.items.map((record) => - TokenVotingMember.fromRecord( - record, + const members = memberPage.items.map((data) => + TokenVotingMember.create( + data.record, + data.metrics, // ENSStore keys its map by checksummed hex address. - namesByAddress.get(record.address.toHexString()) ?? null, + namesByAddress.get(data.record.address.toHexString()) ?? null, ), ); return createPage( members, - records.page, - records.pageSize, - records.totalRecords, + memberPage.page, + memberPage.pageSize, + memberPage.totalRecords, ); } catch (cause) { throw new Error('Error while getting token-voting membership', { diff --git a/test/support/fixtures/tokenVotingMembers.ts b/test/support/fixtures/tokenVotingMembers.ts index a4b0413..2212e61 100644 --- a/test/support/fixtures/tokenVotingMembers.ts +++ b/test/support/fixtures/tokenVotingMembers.ts @@ -39,7 +39,7 @@ export interface FindMembersResponseInput { /** * The members query response: the page of delegates, the id-only - * set used for total-count, and the plugin's MemberMetrics. + * set used for total-count, and the plugin's MemberGovernanceMetrics. */ export function findMembersResponse({ delegates = [], @@ -52,6 +52,6 @@ export function findMembersResponse({ AllERC20VotesDelegate: Array.from({ length: total }, (_, i) => ({ id: `total-${i}`, })), - MemberMetrics: metrics, + MemberGovernanceMetrics: metrics, }; } From 8cb3a4985d11099cd3075ea575d71eb14e33f4f3 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:02:33 +0100 Subject: [PATCH 27/33] Disable changelog for snapshot --- .github/workflows/publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0d3077a..063961c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -73,6 +73,10 @@ jobs: - name: Get base version id: base-version run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" + - name: Disable changelog + run: | + tmp=$(mktemp) + jq '.changelog = false' .changeset/config.json > "$tmp" && mv "$tmp" .changeset/config.json - name: Update version run: pnpm changeset version --snapshot env: From 3cde41c8f93b0c5462cbc5ba57c45f1784c3106f Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:33:44 +0100 Subject: [PATCH 28/33] Remove extra changeset --- .changeset/token-voting-membership-dto.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .changeset/token-voting-membership-dto.md diff --git a/.changeset/token-voting-membership-dto.md b/.changeset/token-voting-membership-dto.md deleted file mode 100644 index 281be90..0000000 --- a/.changeset/token-voting-membership-dto.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@aragon/aragon-domain": minor ---- - -Adopt the token-voting membership DTO as the public list type and tidy the -membership domain. The controller exposes `getTokenVotingMembership` returning -a generic `PageDTO`. Public type exports (type-only): -`TokenVotingMemberDTO`, `PageDTO`, and `GetTokenVotingMembershipRequestDTO`. -Internally the membership domain collapses to a single `TokenVotingMember` -value object served by a `MemberStore` (`findTokenVotingMembers`). Behavior is -unchanged; runtime exports remain `AragonSubdomain` + `EnvioClient`. From 5471b85cbb30afc6d00e55fc369904e8a10362bf Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:36:48 +0100 Subject: [PATCH 29/33] Remove unused arg --- src/domain/ens/ENSStore.ts | 11 +---------- .../stores/ViemENSStore/ViemENSStore.ts | 5 +++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/domain/ens/ENSStore.ts b/src/domain/ens/ENSStore.ts index 31c15d1..b868c56 100644 --- a/src/domain/ens/ENSStore.ts +++ b/src/domain/ens/ENSStore.ts @@ -8,15 +8,6 @@ export interface ENSStore { * Returns a map keyed by checksummed hex address. Addresses with no * primary name, or whose name fails resolution or normalization, are * absent from the map. A missing key means "no name", never an error. - * - * @param coinType ENSIP-11 coin type selecting which chain's primary - * name to read. Implementations default to `60` (legacy Ethereum - * mainnet), which covers the overwhelming majority of primary names - * today. L2 / ENSIP-19 multichain names are only reached when a - * chain-specific coin type is passed. */ - lookUpPrimaryNames( - addresses: Address[], - coinType?: bigint, - ): Promise>; + lookUpPrimaryNames(addresses: Address[]): Promise>; } diff --git a/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts b/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts index 5123cf7..94bc643 100644 --- a/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts +++ b/src/infrastructure/stores/ViemENSStore/ViemENSStore.ts @@ -62,7 +62,6 @@ export class ViemENSStore implements ENSStore { public async lookUpPrimaryNames( addresses: Address[], - coinType: bigint = LEGACY_MAINNET_COIN_TYPE, ): Promise> { const names = new Map(); if (addresses.length === 0) { @@ -70,7 +69,9 @@ export class ViemENSStore implements ENSStore { } const resolved = await Promise.all( - addresses.map((address) => this.resolvePrimaryName(address, coinType)), + addresses.map((address) => + this.resolvePrimaryName(address, LEGACY_MAINNET_COIN_TYPE), + ), ); for (const entry of resolved) { From f551a8413c0625ac9cb0f92adb8474d59add5919 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:13:15 +0100 Subject: [PATCH 30/33] Clean up code --- src/domain/voting-power/VotingPower.test.ts | 14 ++---------- src/domain/voting-power/VotingPower.ts | 22 +++++-------------- src/index.ts | 2 +- .../AragonController/AragonController.ts | 2 +- .../maps/domain/{PageDTO.ts => PageMap.ts} | 0 .../maps/domain/TokenVotingMemberMap.ts | 2 +- .../use-cases/GetTokenVotingMembershipMap.ts | 2 +- .../maps/TokenVotingMemberRecordMap.test.ts | 2 +- 8 files changed, 13 insertions(+), 33 deletions(-) rename src/infrastructure/controllers/AragonController/maps/domain/{PageDTO.ts => PageMap.ts} (100%) diff --git a/src/domain/voting-power/VotingPower.test.ts b/src/domain/voting-power/VotingPower.test.ts index 7a86b2a..9716714 100644 --- a/src/domain/voting-power/VotingPower.test.ts +++ b/src/domain/voting-power/VotingPower.test.ts @@ -3,7 +3,7 @@ import { VotingPower } from './VotingPower'; describe('VotingPower', () => { it('creates from bigint', () => { const vp = VotingPower.fromBigInt(1000000000000000000n); - expect(vp.value.toBigNumber().toString()).toBe('1000000000000000000'); + expect(vp.toWei().toBigNumber().toString()).toBe('1000000000000000000'); }); it('creates zero', () => { @@ -16,16 +16,6 @@ describe('VotingPower', () => { expect(vp.isZero).toBe(false); }); - it('compares greater than or equal', () => { - const high = VotingPower.fromBigInt(100n); - const low = VotingPower.fromBigInt(50n); - const equal = VotingPower.fromBigInt(100n); - - expect(high.isGreaterThanOrEqual(low)).toBe(true); - expect(high.isGreaterThanOrEqual(equal)).toBe(true); - expect(low.isGreaterThanOrEqual(high)).toBe(false); - }); - it('structural equality', () => { const a = VotingPower.fromBigInt(100n); const b = VotingPower.fromBigInt(100n); @@ -45,6 +35,6 @@ describe('VotingPower', () => { it('converts to ether', () => { const vp = VotingPower.fromBigInt(1230000000000000000n); - expect(vp.value.toEther().toBigNumber().toNumber()).toBeCloseTo(1.23); + expect(vp.toWei().toEther().toBigNumber().toNumber()).toBeCloseTo(1.23); }); }); diff --git a/src/domain/voting-power/VotingPower.ts b/src/domain/voting-power/VotingPower.ts index 3d0454c..53b3162 100644 --- a/src/domain/voting-power/VotingPower.ts +++ b/src/domain/voting-power/VotingPower.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { Wei } from '@/domain/primitives'; const VotingPowerPropsSchema = z.object({ - value: z.instanceof(Wei), + weiValue: z.instanceof(Wei), }); type VotingPowerProps = z.infer; @@ -13,35 +13,25 @@ export class VotingPower extends ValueObject { /** * The voting power in wei. */ - get value(): Wei { - return this.props.value; + toWei(): Wei { + return this.props.weiValue; } /** * Whether this voting power is zero. */ get isZero(): boolean { - return this.props.value.toBigNumber().isZero(); + return this.props.weiValue.toBigNumber().isZero(); } /** - * Structural equality based on the underlying wei value. + * True if the internal wei value is equal to the other's. */ equals(other?: VotingPower): boolean { if (!other) { return false; } - return this.props.value.toBigNumber().isEqualTo(other.value.toBigNumber()); - } - - /** - * Compares this voting power to another. - * Returns true if this voting power is greater than or equal to the other. - */ - isGreaterThanOrEqual(other: VotingPower): boolean { - return this.props.value - .toBigNumber() - .isGreaterThanOrEqualTo(other.value.toBigNumber()); + return this.props.weiValue.equals(other.toWei()); } static create(value: Wei): VotingPower { diff --git a/src/index.ts b/src/index.ts index 0c4a64c..ac2eaf3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,7 @@ export { EnvioClient } from './infrastructure/stores/EnvioClient'; // DTOs export type { MemberProfileTextRecordDTO } from './infrastructure/controllers/AragonController/maps/domain/MemberProfileTextRecordMap'; -export type { PageDTO } from './infrastructure/controllers/AragonController/maps/domain/PageDTO'; +export type { PageDTO } from './infrastructure/controllers/AragonController/maps/domain/PageMap'; export type { TokenVotingMemberDTO } from './infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap'; export type { GetMemberProfileTextRecordsRequestDTO } from './infrastructure/controllers/AragonController/maps/use-cases/GetMemberProfileTextRecordsMap'; export type { GetTokenVotingMembershipRequestDTO } from './infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap'; diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index be59250..d706548 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -14,7 +14,7 @@ import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenV import { GetTokenVotingMembershipUseCase } from '@/use-cases/GetTokenVotingMembershipUseCase'; import type { MemberProfileTextRecordDTO } from './maps/domain/MemberProfileTextRecordMap'; import * as MemberProfileTextRecordMap from './maps/domain/MemberProfileTextRecordMap'; -import type { PageDTO } from './maps/domain/PageDTO'; +import type { PageDTO } from './maps/domain/PageMap'; import type { TokenVotingMemberDTO } from './maps/domain/TokenVotingMemberMap'; import type { GetMemberProfileTextRecordsRequestDTO } from './maps/use-cases/GetMemberProfileTextRecordsMap'; import * as GetMemberProfileTextRecordsMap from './maps/use-cases/GetMemberProfileTextRecordsMap'; diff --git a/src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts b/src/infrastructure/controllers/AragonController/maps/domain/PageMap.ts similarity index 100% rename from src/infrastructure/controllers/AragonController/maps/domain/PageDTO.ts rename to src/infrastructure/controllers/AragonController/maps/domain/PageMap.ts diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts index 1500777..35a8093 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts @@ -27,7 +27,7 @@ export function mapDomainToDTO( return { address: member.address.toHexString(), ens: member.ens ? member.ens.toString() : null, - votingPower: member.votingPower.value.toBigNumber().toFixed(0), + votingPower: member.votingPower.toWei().toBigNumber().toFixed(0), metrics: { firstActivityTimestamp: first === 0 ? null : first, lastActivityTimestamp: last === 0 ? null : last, diff --git a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts index 7980fd1..d7c46c1 100644 --- a/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/use-cases/GetTokenVotingMembershipMap.ts @@ -3,7 +3,7 @@ import { Address, assertHexString } from '@/domain/primitives'; import type { Page } from '@/domain/primitives/pagination/Page'; import { PageRequest } from '@/domain/primitives/pagination/PageRequest'; import type { GetTokenVotingMembershipUseCaseProps } from '@/use-cases/GetTokenVotingMembershipUseCase'; -import type { PageDTO } from '../domain/PageDTO'; +import type { PageDTO } from '../domain/PageMap'; import type { TokenVotingMemberDTO } from '../domain/TokenVotingMemberMap'; import * as TokenVotingMemberMap from '../domain/TokenVotingMemberMap'; diff --git a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts index 6d21ba1..9abb028 100644 --- a/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts +++ b/src/infrastructure/stores/EnvioMemberStore/maps/TokenVotingMemberRecordMap.test.ts @@ -29,7 +29,7 @@ describe('TokenVotingMemberRecordMap.mapDTOToDomain', () => { expect(records).toHaveLength(1); expect(totalRecords).toBe(3); - expect(records[0].votingPower.value.toBigNumber().toFixed(0)).toBe( + expect(records[0].votingPower.toWei().toBigNumber().toFixed(0)).toBe( '5000000000000000000', ); expect(records[0].delegationCount).toBe(1); From 90fc64ab0b8d9a948eca81d1e513b7ad40a09cbe Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:18:19 +0100 Subject: [PATCH 31/33] Jsdoc --- src/domain/member/TokenVotingMember.ts | 2 +- .../maps/domain/TokenVotingMemberMap.ts | 39 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/domain/member/TokenVotingMember.ts b/src/domain/member/TokenVotingMember.ts index ad802c4..ef179d1 100644 --- a/src/domain/member/TokenVotingMember.ts +++ b/src/domain/member/TokenVotingMember.ts @@ -19,7 +19,7 @@ type TokenVotingMemberProps = z.infer; */ export class TokenVotingMember extends ValueObject { /** - * The member's wallet address. + * The member's account address. */ get address(): Address { return this.props.record.address; diff --git a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts index 35a8093..a011bb3 100644 --- a/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts +++ b/src/infrastructure/controllers/AragonController/maps/domain/TokenVotingMemberMap.ts @@ -1,20 +1,47 @@ import type { TokenVotingMember } from '@/domain/member/TokenVotingMember'; /** - * Token-voting membership DTO — the type the frontend's token member list - * renders from, regardless of whether the data came from the subdomain or - * the legacy backend. - * - * Activity fields are unix-seconds timestamps. The frontend can render - * them directly without resolving block numbers via RPC. + * A member of a TokenVoting plugin. */ export interface TokenVotingMemberDTO { + /** + * A member of a TokenVoting plugin. + */ address: string; + + /** + * The member's primary ENS name, or null when the address has no + * primary name (or it could not be resolved). + */ ens: string | null; + + /** + * The member's voting power. + */ votingPower: string | null; + + /** + * The member's governance metrics. + */ metrics: { + /** + * Unix-seconds timestamp of the member's first observed activity. + * "Activity" includes voting power changes, votes cast, and proposals + * created. `0` means "no activity recorded". + */ firstActivityTimestamp: number | null; + + /** + * Unix-seconds timestamp of the member's most recent observed activity. + * "Activity" includes voting power changes, votes cast, and proposals + * created. `0` means "no activity recorded". + */ lastActivityTimestamp: number | null; + + /** + * Number of distinct accounts currently delegating their voting power + * to this member (counts self-delegation). + */ delegationCount: number; }; } From e0d2826725073ea1803988ea7184471b7fbe0c74 Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 16:20:56 +0100 Subject: [PATCH 32/33] Jsdoc --- .../controllers/AragonController/AragonController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/controllers/AragonController/AragonController.ts b/src/infrastructure/controllers/AragonController/AragonController.ts index d706548..1fda5ce 100644 --- a/src/infrastructure/controllers/AragonController/AragonController.ts +++ b/src/infrastructure/controllers/AragonController/AragonController.ts @@ -78,8 +78,8 @@ export class AragonController { /** * Returns a page of members of an Aragon TokenVoting plugin, scoped - * to a specific ERC20Votes token contract and ordered by current - * voting power descending. + * to a specific token contract and ordered by current voting power + * descending. */ public getTokenVotingMembership(dto: GetTokenVotingMembershipRequestDTO) { return handleRequest(this.handlers.getTokenVotingMembership, dto); From 2649ca2c7d97f663a48113c4ad8e743841b783ce Mon Sep 17 00:00:00 2001 From: O Senhor <645336+asciiman@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:32:24 +0100 Subject: [PATCH 33/33] Fix errors --- src/domain/primitives/units/Ether.test.ts | 13 +++++++++++++ src/domain/primitives/units/Ether.ts | 11 +++++++++++ src/domain/primitives/units/Gwei.test.ts | 13 +++++++++++++ src/domain/primitives/units/Gwei.ts | 11 +++++++++++ src/domain/primitives/units/Wei.test.ts | 13 +++++++++++++ src/domain/primitives/units/Wei.ts | 11 +++++++++++ src/domain/voting-power/VotingPower.ts | 4 ++-- .../stores/ViemENSStore/ViemENSStore.test.ts | 14 +------------- 8 files changed, 75 insertions(+), 15 deletions(-) diff --git a/src/domain/primitives/units/Ether.test.ts b/src/domain/primitives/units/Ether.test.ts index d909778..b44e1b5 100644 --- a/src/domain/primitives/units/Ether.test.ts +++ b/src/domain/primitives/units/Ether.test.ts @@ -48,4 +48,17 @@ describe('Ether', () => { const token = Ether.create(new BigNumber(1.23)); expect(token.toEther()).toBe(token); }); + + it('equals compares by numeric value across instances', () => { + const a = Ether.create(new BigNumber(1.23)); + const b = Ether.create(new BigNumber(1.23)); + const c = Ether.create(new BigNumber(4.56)); + expect(a.equals(b)).toBe(true); + expect(a.equals(c)).toBe(false); + }); + + it('equals(undefined) returns false', () => { + const token = Ether.create(new BigNumber(1.23)); + expect(token.equals(undefined)).toBe(false); + }); }); diff --git a/src/domain/primitives/units/Ether.ts b/src/domain/primitives/units/Ether.ts index 8fced2a..a6da027 100644 --- a/src/domain/primitives/units/Ether.ts +++ b/src/domain/primitives/units/Ether.ts @@ -31,6 +31,17 @@ export class Ether extends ValueObject implements EVMUnit { return this; } + /** + * Value-based equality: the inherited shallow props comparison would + * compare the inner BigNumber by reference, so compare numerically. + */ + public equals(other?: Ether): boolean { + if (!other) { + return false; + } + return this.props.etherValue.isEqualTo(other.toBigNumber()); + } + /** * Adds this value to another value. * @param other The other value to add to. diff --git a/src/domain/primitives/units/Gwei.test.ts b/src/domain/primitives/units/Gwei.test.ts index c4be801..9fc5add 100644 --- a/src/domain/primitives/units/Gwei.test.ts +++ b/src/domain/primitives/units/Gwei.test.ts @@ -48,4 +48,17 @@ describe('Gwei', () => { const token = Gwei.create(new BigNumber(1_230_000_000)); expect(token.toGwei()).toBe(token); }); + + it('equals compares by numeric value across instances', () => { + const a = Gwei.create(new BigNumber(100)); + const b = Gwei.create(new BigNumber(100)); + const c = Gwei.create(new BigNumber(200)); + expect(a.equals(b)).toBe(true); + expect(a.equals(c)).toBe(false); + }); + + it('equals(undefined) returns false', () => { + const token = Gwei.create(new BigNumber(100)); + expect(token.equals(undefined)).toBe(false); + }); }); diff --git a/src/domain/primitives/units/Gwei.ts b/src/domain/primitives/units/Gwei.ts index 77dcfef..7542a4f 100644 --- a/src/domain/primitives/units/Gwei.ts +++ b/src/domain/primitives/units/Gwei.ts @@ -31,6 +31,17 @@ export class Gwei extends ValueObject implements EVMUnit { return Ether.create(etherValue); } + /** + * Value-based equality: the inherited shallow props comparison would + * compare the inner BigNumber by reference, so compare numerically. + */ + public equals(other?: Gwei): boolean { + if (!other) { + return false; + } + return this.props.gweiValue.isEqualTo(other.toBigNumber()); + } + /** * Adds this value to another value. * @param other The other value to add to. diff --git a/src/domain/primitives/units/Wei.test.ts b/src/domain/primitives/units/Wei.test.ts index a6b2c8f..c8fd4fb 100644 --- a/src/domain/primitives/units/Wei.test.ts +++ b/src/domain/primitives/units/Wei.test.ts @@ -48,4 +48,17 @@ describe('Wei', () => { const token = Wei.create(new BigNumber('1230000000000000000')); expect(token.toWei()).toBe(token); }); + + it('equals compares by numeric value across instances', () => { + const a = Wei.create(new BigNumber(100)); + const b = Wei.create(new BigNumber(100)); + const c = Wei.create(new BigNumber(200)); + expect(a.equals(b)).toBe(true); + expect(a.equals(c)).toBe(false); + }); + + it('equals(undefined) returns false', () => { + const token = Wei.create(new BigNumber(100)); + expect(token.equals(undefined)).toBe(false); + }); }); diff --git a/src/domain/primitives/units/Wei.ts b/src/domain/primitives/units/Wei.ts index 0b8deeb..3222116 100644 --- a/src/domain/primitives/units/Wei.ts +++ b/src/domain/primitives/units/Wei.ts @@ -34,6 +34,17 @@ export class Wei extends ValueObject implements EVMUnit { return Ether.create(etherValue); } + /** + * Value-based equality: the inherited shallow props comparison would + * compare the inner BigNumber by reference, so compare numerically. + */ + public equals(other?: Wei): boolean { + if (!other) { + return false; + } + return this.props.weiValue.isEqualTo(other.toBigNumber()); + } + /** * Adds this value to another value. * @param other The other value to add to. diff --git a/src/domain/voting-power/VotingPower.ts b/src/domain/voting-power/VotingPower.ts index 53b3162..a95d5a6 100644 --- a/src/domain/voting-power/VotingPower.ts +++ b/src/domain/voting-power/VotingPower.ts @@ -34,8 +34,8 @@ export class VotingPower extends ValueObject { return this.props.weiValue.equals(other.toWei()); } - static create(value: Wei): VotingPower { - const validated = VotingPowerPropsSchema.parse({ value }); + static create(weiValue: Wei): VotingPower { + const validated = VotingPowerPropsSchema.parse({ weiValue }); return new VotingPower(validated); } diff --git a/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts b/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts index a432bed..0a86b5b 100644 --- a/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts +++ b/src/infrastructure/stores/ViemENSStore/ViemENSStore.test.ts @@ -141,7 +141,7 @@ describe('ViemENSStore.lookUpPrimaryNames', () => { ); }); - it('queries with the checksummed address and the legacy mainnet coin type by default', async () => { + it('queries with the checksummed address and the legacy mainnet coin type', async () => { const { client, getEnsName } = buildClient({ [ALICE]: 'alice.eth' }); const store = new ViemENSStore(client); @@ -153,18 +153,6 @@ describe('ViemENSStore.lookUpPrimaryNames', () => { }); }); - it('forwards an explicit coin type', async () => { - const { client, getEnsName } = buildClient({ [ALICE]: 'alice.eth' }); - const store = new ViemENSStore(client); - - await store.lookUpPrimaryNames([address(ALICE)], 2147483648n); - - expect(getEnsName).toHaveBeenCalledWith({ - address: address(ALICE).toHexString(), - coinType: 2147483648n, - }); - }); - it('returns an empty map and issues no calls for no addresses', async () => { const { client, getEnsName } = buildClient({}); const store = new ViemENSStore(client);