npm package: @nxt3d/ecsjs
Description: JavaScript library for ECS V2 - Ethereum Credential Service registry and resolver
Author: ECS Team
Latest version: 0.2.4-beta

# ecsjs - Ethereum Credential Service V2

[![npm version](https://img.shields.io/npm/v/@nxt3d/ecsjs.svg)](https://www.npmjs.com/package/@nxt3d/ecsjs)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**Version:** 0.2.4-beta  
**Status:** Beta - Deployed on Sepolia

JavaScript/TypeScript library for interacting with **ECS V2** (Ethereum Credential Service), a decentralized registry for "known" credential resolvers. ECS V2 is fully compatible with the [ENS Hooks standard](https://github.com/nxt3d/ensips/blob/hooks/ensips/hooks.md), enabling ENS names to securely resolve credentials from trusted resolvers.

## Installation

```bash
npm install @nxt3d/ecsjs@0.2.4-beta
```

> **Version:** 0.2.4-beta - [View on NPM](https://www.npmjs.com/package/@nxt3d/ecsjs)  
> **Important:** ECS V1 is deprecated and incompatible with V2.  
> **Note:** This package includes viem as a dependency, so you don't need to install it separately.

## What is ECS V2?

**ECS V2** is a simplified, decentralized registry that maps labels (e.g., `name-stars`) to standard ENS resolvers. These resolvers serve verifiable credential data, either onchain or offchain (via CCIP-Read).

### Goals of V2

* **Simplicity:** The complex multi-level registry has been replaced with a flat, single-label registry. Labels (e.g., `name-stars`) map directly to resolvers.
* **Standard Resolvers:** Credential resolvers are now just standard [ENSIP-10 (Extended Resolver)](https://docs.ens.domains/ens-improvement-proposals/ensip-10-wildcard-resolution) contracts. This means any existing ENS tooling can interact with them.
* **Flexible Data:** Credential providers can define their own schema and keys. There's no forced structure for credential data.
* **Hooks Integration:** ECS serves as the registry for [Hooks](https://github.com/nxt3d/ensips/blob/hooks/ensips/hooks.md). Hooks in ENS text records can reference ECS resolvers to fetch trusted data.
* **Resolver Trust:** The registry tracks `resolverUpdated` timestamps, allowing clients to enforce security policies based on resolver age.

## Usage Flow with Hooks

Hooks enable ENS names to redirect queries to known resolvers.

1. **User** sets a text record on their ENS name (e.g., `maria.eth`) containing a **Hook**:
   ```
   hook("text(bytes32,string)", 0xc8028D202838FF7D14835c75906A07839837C160)
   ```

2. **Client** reads this record and extracts the resolver address.

3. **Client** calls `getResolverInfo(resolverAddress)` on the ECS Registry to:
   - Find its registered label (e.g., `name-stars`)
   - Check the `resolverUpdated` timestamp to verify resolver stability
   - Read the `review` field for admin-assigned ratings or certifications
   - **Make a trust decision** based on resolver age and review status

4. **Client** constructs the service name `name-stars.ecs.eth` (optional, for provenance).

5. **Client** queries the resolver directly: `text(node, "credential-key")`.
   - Note: Single-label resolvers ignore the `node` parameter, so any value (including `0x0`) works.

6. **Resolver** returns the verified credential data.

This creates a trusted link to the record, where `maria.eth` doesn't store the record herself; instead, the record can be resolved against a "known" trusted resolver.

## Quick Start

```typescript
import { 
  createECSClient, 
  sepolia,
  getResolverInfo, 
  resolveCredential 
} from '@nxt3d/ecsjs'

const client = createECSClient({
  chain: sepolia,
  rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'
})

// User has hook pointing to resolver
const resolverAddress = '0xc8028D202838FF7D14835c75906A07839837C160'
const credentialKey = 'eth.ecs.name-stars.starts:vitalik.eth'

// Get label and resolve credential in one call
const credential = await resolveCredential(client, resolverAddress, credentialKey)
// Returns: "100"

// Or get resolver info
const { label, resolverUpdated, review } = await getResolverInfo(client, resolverAddress)
// Returns: { label: "name-stars", resolverUpdated: 1764948384n, review: "" }

// You can also use viem's ENS functions directly
const ensName = `${label}.ecs.eth`
const textValue = await client.getEnsText({
  name: ensName,
  key: credentialKey
})
// Returns: "100"
```

## Resolver Trust and Freshness

**ECS strictly enforces a one-to-one relationship between labels and resolvers.** While label owners can change resolvers (necessary for upgrades), this introduces a security concern. The registry tracks `resolverUpdated` timestamps, allowing clients to enforce security policies based on resolver age.

**Security-conscious clients can require resolvers to be established (e.g., 90+ days old) before trusting them.** Recent resolver changes may indicate compromise, untested deployments, or migrations requiring review.

```typescript
import { getResolverInfo, getResolverAge } from '@nxt3d/ecsjs'

const { label, resolverUpdated, review } = await getResolverInfo(client, resolverAddress)
const resolverAge = getResolverAge(resolverUpdated)
const ageInDays = Math.floor(resolverAge / 86400)

if (ageInDays < 90) { // 90 days for high security
  console.warn(`⚠️ Resolver for "${label}" changed ${ageInDays} days ago`)
  // Reject or require security review
}

// Check admin review status
if (review && review !== "verified") {
  console.warn(`⚠️ Resolver "${label}" review status: ${review}`)
}
```

## API Reference

### `createECSClient(config)`

Creates a Viem public client configured for ECS.

```typescript
import { createECSClient, sepolia } from '@nxt3d/ecsjs'

const client = createECSClient({
  chain: sepolia,
  rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'
})
```

**Parameters:**
- `config.chain`: Chain to connect to (`sepolia` or `mainnet`)
- `config.rpcUrl`: RPC URL for the chain

**Returns:** Viem `PublicClient`

---

### `getResolverInfo(client, resolverAddress)`

Get information about a resolver from its address.

```typescript
const { label, resolverUpdated, review } = await getResolverInfo(
  client,
  '0xc8028D202838FF7D14835c75906A07839837C160'
)
```

**Parameters:**
- `client`: Viem public client
- `resolverAddress`: The resolver address

**Returns:** `Promise<{ label: string, resolverUpdated: bigint, review: string }>`
- `label`: The registered label (e.g., "name-stars")
- `resolverUpdated`: Timestamp when the resolver was last updated
- `review`: Admin-assigned review status or certification

---

### `resolveCredential(client, resolverAddress, credentialKey)`

Resolve a credential from a known resolver.

```typescript
const credential = await resolveCredential(
  client,
  '0x9773397bd9366D80dAE708CA4C4413Abf88B3DAa',
  'eth.ecs.name-stars.starts:vitalik.eth'
)
```

**Parameters:**
- `client`: Viem public client
- `resolverAddress`: The resolver address
- `credentialKey`: The credential key to resolve

**Returns:** `Promise<string | null>`

---

### `getRegistryAddress(chainId)`

Get the ECS Registry address for a given chain.

```typescript
import { getRegistryAddress } from '@nxt3d/ecsjs'

const registryAddress = getRegistryAddress(11155111) // Sepolia
// Returns: "0xb09C149664773bFA88B72FA41437AdADcB8bF5B4"
```

**Parameters:**
- `chainId`: The chain ID (e.g., `11155111` for Sepolia)

**Returns:** `string` - The registry address

---

### `getResolverAge(resolverUpdated)`

Calculate the age of a resolver in seconds (helper for security checks).

```typescript
import { getResolverAge } from '@nxt3d/ecsjs'

const { resolverUpdated } = await getResolverInfo(client, resolverAddress)
const ageInSeconds = getResolverAge(resolverUpdated)
const ageInDays = Math.floor(ageInSeconds / 86400)
console.log(`Resolver is ${ageInDays} days old`)
```

**Parameters:**
- `resolverUpdated`: The `resolverUpdated` timestamp (bigint)

**Returns:** `number` - Age in seconds

## Deployments

### Sepolia Testnet

**Version:** 0.2.4-beta  
**Date:** December 7, 2025  
**Network:** Sepolia (Chain ID: 11155111)  
**Status:** ✅ Live and operational (Deployment 01 - Resolver Review System)

#### Deployed Contracts

| Contract | Address | Verified |
|----------|---------|----------|
| ECS Registry | `0xb09C149664773bFA88B72FA41437AdADcB8bF5B4` | [✅ View](https://sepolia.etherscan.io/address/0xb09C149664773bFA88B72FA41437AdADcB8bF5B4) |
| ECS Registrar | `0xD1399C6879EA5A92eB25ee8A0512c7a4fC0DDc6b` | [✅ View](https://sepolia.etherscan.io/address/0xD1399C6879EA5A92eB25ee8A0512c7a4fC0DDc6b) |
| Credential Resolver (Implementation) | `0x9eC339D221dB86e5bcfB12B7861b3F8e41a5D5d9` | [✅ View](https://sepolia.etherscan.io/address/0x9eC339D221dB86e5bcfB12B7861b3F8e41a5D5d9) |
| Credential Resolver Factory | `0x9b2d7A50bb15F5147c2cf46f05FBfD0E931AB77A` | [✅ View](https://sepolia.etherscan.io/address/0x9b2d7A50bb15F5147c2cf46f05FBfD0E931AB77A) |
| Credential Resolver (Clone - name-stars) | `0xc8028D202838FF7D14835c75906A07839837C160` | [View](https://sepolia.etherscan.io/address/0xc8028D202838FF7D14835c75906A07839837C160) |

> **New in v0.2.1:** Resolver review system for admin trust ratings + OwnableUpgradeable pattern for clones

#### Configuration

- **Root Name:** `ecs.eth`
- **Root Node:** `0xe436ba58406c69a63a9611a11eb52314c5c17ba9eaaa7dab8506fe8849517286`
- **Deployer:** `0xF8e03bd4436371E0e2F7C02E529b2172fe72b4EF`
- **Registrar Pricing:** ~0.001 ETH/year (32000 wei/second)
- **Min Commitment Age:** 60 seconds

#### Registered Labels

##### name-stars.ecs.eth

- **Status:** ✅ Registered
- **Owner:** `0xF8e03bd4436371E0e2F7C02E529b2172fe72b4EF`
- **Resolver:** `0xc8028D202838FF7D14835c75906A07839837C160` (minimal clone)
- **Expires:** December 7, 2026

**Credential Records:**
- **Key:** `eth.ecs.name-stars.starts:vitalik.eth`
- **Text Value:** `"100"`
- **Data Value:** `100` (uint256)

## Examples

### Basic Credential Resolution

```typescript
import { createECSClient, sepolia, resolveCredential } from '@nxt3d/ecsjs'

const client = createECSClient({
  chain: sepolia,
  rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'
})

// Resolve credential directly
const credential = await resolveCredential(
  client,
  '0xc8028D202838FF7D14835c75906A07839837C160',
  'eth.ecs.name-stars.starts:vitalik.eth'
)

console.log(`vitalik.eth has ${credential} stars`) // "vitalik.eth has 100 stars"
```

### With Security Checks

```typescript
import { 
  createECSClient, 
  sepolia, 
  getResolverInfo,
  getResolverAge,
  resolveCredential 
} from '@nxt3d/ecsjs'

const client = createECSClient({
  chain: sepolia,
  rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'
})

const resolverAddress = '0xc8028D202838FF7D14835c75906A07839837C160'

// Check resolver info first
const { label, resolverUpdated, review } = await getResolverInfo(client, resolverAddress)
const ageInDays = Math.floor(getResolverAge(resolverUpdated) / 86400)

console.log(`Resolver: ${label}.ecs.eth`)
console.log(`Age: ${ageInDays} days`)
console.log(`Review: ${review || 'None'}`)

// Enforce security policy
if (ageInDays < 90) {
  throw new Error(`Resolver too new: ${ageInDays} days (require 90+)`)
}

// Check review status
if (review && review !== "verified") {
  console.warn(`⚠️ Review status: ${review}`)
}

// Proceed with resolution
const credential = await resolveCredential(
  client,
  resolverAddress,
  'eth.ecs.name-stars.starts:vitalik.eth'
)

console.log(`Credential: ${credential}`)
```

### Using Viem Directly

Since ECS V2 uses standard ENS resolvers, you can also use Viem's ENS functions directly:

```typescript
import { createECSClient, sepolia, getResolverInfo } from '@nxt3d/ecsjs'

const client = createECSClient({
  chain: sepolia,
  rpcUrl: 'https://eth-sepolia.g.alchemy.com/v2/YOUR_KEY'
})

// Get the label from resolver address
const { label } = await getResolverInfo(
  client,
  '0xc8028D202838FF7D14835c75906A07839837C160'
)

// Use Viem's getEnsText directly
const ensName = `${label}.ecs.eth`
const textValue = await client.getEnsText({
  name: ensName,
  key: 'eth.ecs.name-stars.starts:vitalik.eth'
})

console.log(textValue) // "100"
```

## Development

### Building

```bash
npm run build
```

### Testing

```bash
npm test               # Run all tests
npm run test:watch     # Watch mode
npm run test:coverage  # Coverage report
```

### Linting

```bash
npm run lint
npm run lint:fix
```

## Migration from V1

ECS V2 is a complete rewrite with a different architecture. Key differences:

### V1 (0.1.x) - Deprecated
- Complex multi-level registry (`name.coinType.addr.ecs.eth`)
- Custom credential resolution
- Multi-currency support via coinType
- Credential-first design

### V2 (0.2.4-beta) - Current
- Simple flat registry (`label.ecs.eth`)
- Standard ENS resolvers (ENSIP-10)
- ENS Hooks integration
- Resolver-first design with trust tracking

**Migration is not automatic.** V2 requires new resolver deployments and a different integration approach. If you're using V1, please reach out for migration guidance.

## TypeScript Support

This library is written in TypeScript and includes full type definitions. All functions are fully typed for the best developer experience.

```typescript
import type { PublicClient, Chain } from '@nxt3d/ecsjs'
```

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

MIT License - see the [LICENSE](LICENSE) file for details.

## Links

- [ECS V2 Protocol Repository](https://github.com/nxt3d/ecs)
- [ENS Hooks Standard](https://github.com/nxt3d/ensips/blob/hooks/ensips/hooks.md)
- [Viem Documentation](https://viem.sh/)
- [ENS Documentation](https://docs.ens.domains/)

---

**Note**: ECS V2 is currently in beta on Ethereum Sepolia testnet. The library has been tested with real deployments and works correctly. Use at your own risk in production environments.