Skip to content

Commit

Permalink
feat: [sc-25579] Create TypeScript SDK for NameRank API (#472)
Browse files Browse the repository at this point in the history
* init namerank-sdk
* update lock
  • Loading branch information
Carbon225 authored Dec 11, 2024
1 parent 25733aa commit 81e7def
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/namerank-sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist/
node_modules/
npm-debug.log
.DS_Store
57 changes: 57 additions & 0 deletions packages/namerank-sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
{
"name": "@namehash/namerank",
"private": true,
"description": "A lightweight JavaScript client for the NameRank API.",
"version": "0.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/namehash/namekit.git",
"directory": "packages/namerank-sdk"
},
"homepage": "https://github.com/namehash/namekit/tree/main/packages/namerank-sdk",
"keywords": [
"ENS",
"NameKit",
"NameHash",
"NameGuard",
"NameRank",
"NFT"
],
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"sideEffects": false,
"scripts": {
"build": "tsup",
"dev": "tsup --watch --clean=false",
"test": "vitest"
},
"dependencies": {
"@namehash/ens-utils": "workspace:*",
"@namehash/nameguard": "workspace:*",
"cross-fetch": "4.0.0"
},
"devDependencies": {
"@namekit/tsconfig": "workspace:*",
"@types/node": "22.7.4",
"tsup": "8.3.5",
"typescript": "5.6.2",
"vitest": "1.6.0"
},
"publishConfig": {
"access": "public"
}
}
20 changes: 20 additions & 0 deletions packages/namerank-sdk/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from "vitest";

import { createClient, MAX_INSPECTED_NAME_CHARACTERS } from ".";

const namerank = createClient({
// undefined will default to the production endpoint
namerankEndpoint: process.env.NAMERANK_API_URI,
});

// No mocking (we should probably mock)
// Test urlencoded

describe("inspectName", () => {
it("should fetch the NameRank report for a single name", async () => {
const data = await namerank.inspectName("vitalìk.eth");

expect(data.namerank.purity_score).toBeCloseTo(0.28995870588235295, 2);
expect(data.namerank.interesting_score).toBeCloseTo(0.3520927142857143, 2);
});
});
168 changes: 168 additions & 0 deletions packages/namerank-sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import fetch from "cross-fetch";
import { Network, NameGuardReport } from "@namehash/nameguard";

const ETH_TLD = "eth";

enum LabelStatus {
Normalized = 'normalized',
Unnormalized = 'unnormalized',
Unknown = 'unknown'
}

interface NLPLabelAnalysis {
inspection: any;
status: LabelStatus;
probability: number;
log_probability: number;
word_count: number;
top_tokenization?: string[];
tokenizations: Record<string, any>[];
}

interface NameRankReport {
purity_score: number;
interesting_score: number;
analysis?: NLPLabelAnalysis;
}

interface NameRankResponse {
namerank: NameRankReport;
nameguard: NameGuardReport;
}

// TODO: Let's apply more formalization to this error class.
class NameRankError extends Error {
constructor(
public status: number,
message?: string,
) {
super(message);
}
}

const DEFAULT_ENDPOINT = "https://api.nameguard.io/namerank";
const DEFAULT_NETWORK: Network = "mainnet";
const DEFAULT_INSPECT_LABELHASH_PARENT = ETH_TLD;
export const DEFAULT_COMPUTE_NAMEGUARD_REPORT = false;
const MAX_BULK_INSPECTION_NAMES = 250;

/** includes label separators */
export const MAX_INSPECTED_NAME_CHARACTERS = 200;

/** includes duplicated unknown labels */
const MAX_INSPECTED_NAME_UNKNOWN_LABELS = 5;

export interface NameRankOptions {
namerankEndpoint?: string;
network?: Network;
}

interface InspectNameOptions {}

export class NameRank {
private namerankEndpoint: URL;
protected network: Network;
private abortController: AbortController;

constructor({
namerankEndpoint = DEFAULT_ENDPOINT,
network = DEFAULT_NETWORK,
}: NameRankOptions = {}) {
this.namerankEndpoint = new URL(namerankEndpoint);
console.log(this.namerankEndpoint);
this.network = network;
this.abortController = new AbortController();
}

/**
* Inspects a single name with NameRank.
*
* If `name` includes unknown labels then this function will attempt automated labelhash resolution through the ENS Subgraph
* using the network specified in the `NameGuard` instance. Therefore the returned `name` may not match the provided `name`, but is guaranteed to have a matching `namehash`.
*
* @param {string} name The name for NameRank to inspect.
* @param {InspectNameOptions} options The options for the inspection.
* @returns {Promise<NameRankReport>} A promise that resolves with the `NameRankReport` of the name.
* @example
* const nameRankReport = await namerank.inspectName('vitalik.eth');
*/
public inspectName(
name: string,
options?: InspectNameOptions,
): Promise<NameRankResponse> {
const network_name = this.network;
return this.rawRequest("inspect-name", "POST", { name /*, network_name */ });
}

/**
* Performs a raw HTTP request to the NameRank API.
* @param {string} path The API endpoint path.
* @param {string} method The HTTP method (e.g., 'GET', 'POST').
* @param {object} body The request body for POST requests.
* @param {object} headers Additional headers for the request.
* @returns {Promise<any>} The response from the API.
*/
async rawRequest(
path: string,
method: string = "GET",
body: object = {},
headers: object = {},
): Promise<any> {
const url = new URL(this.namerankEndpoint);
url.pathname += "/" + path;

const options: RequestInit = {
method,
headers: {
"Content-Type": "application/json",
...headers,
},
signal: this.abortController.signal,
};

if (method !== "GET") {
options.body = JSON.stringify(body);
}

console.log(url);
const response = await fetch(url, options);

if (!response.ok) {
throw new NameRankError(
response.status,
`Failed to perform request to ${path}.`,
);
}

return await response.json();
}

public abortAllRequests(): void {
this.abortController.abort();

this.abortController = new AbortController();
}
}

/**
* Creates a new instance of a NameRank client.
*
* @param {NameRankOptions} [options] - Configuration options for NameRank.
* @example
* import { createClient } from '@namehash/namerank';
* const namerank = createClient({ ... })
*/
export function createClient(options?: NameRankOptions) {
return new NameRank(options);
}

const defaultClient = createClient();

/**
* `NameRank` provides methods to inspect and rank ENS names.
* It can inspect individual names or batch names.
* @example
* import { namerank } from '@namehash/namerank';
* const nameRankReport = await namerank.inspectName('vitalik.eth');
*/
export const namerank = defaultClient;
14 changes: 14 additions & 0 deletions packages/namerank-sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@namekit/tsconfig/base.json",
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"paths": {
"@namehash/ens-utils": ["../ens-utils/dist"],
"@namehash/nameguard": ["../nameguard-sdk"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
9 changes: 9 additions & 0 deletions packages/namerank-sdk/tsup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"],
clean: true,
dts: true,
skipNodeModulesBundle: true,
});
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 81e7def

Please sign in to comment.