feat: add foreign key constraints and metrics for blockchain node
This commit is contained in:
25
packages/js/aitbc-sdk/package.json
Normal file
25
packages/js/aitbc-sdk/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@aitbc/aitbc-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "AITBC JavaScript SDK for coordinator receipts",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-fetch": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"keywords": ["aitbc", "sdk", "receipts"],
|
||||
"author": "AITBC Team",
|
||||
"license": "MIT"
|
||||
}
|
||||
23
packages/js/aitbc-sdk/src/client.test.ts
Normal file
23
packages/js/aitbc-sdk/src/client.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AitbcClient } from "./client";
|
||||
|
||||
const createClient = () =>
|
||||
new AitbcClient({
|
||||
baseUrl: "https://api.example.com",
|
||||
apiKey: "test-key",
|
||||
fetchImpl: async (input: RequestInfo | URL, init?: RequestInit) =>
|
||||
new Response(JSON.stringify({ job_id: "job", candidates: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
});
|
||||
|
||||
describe("AitbcClient", () => {
|
||||
it("sends match requests", async () => {
|
||||
const client = createClient();
|
||||
const response = await client.match({ jobId: "job" });
|
||||
expect(response.jobId).toBe("job");
|
||||
expect(response.candidates).toEqual([]);
|
||||
});
|
||||
});
|
||||
138
packages/js/aitbc-sdk/src/client.ts
Normal file
138
packages/js/aitbc-sdk/src/client.ts
Normal file
@ -0,0 +1,138 @@
|
||||
import type {
|
||||
ClientOptions,
|
||||
MatchRequest,
|
||||
MatchResponse,
|
||||
HealthResponse,
|
||||
MetricsResponse,
|
||||
WalletSignRequest,
|
||||
WalletSignResponse,
|
||||
RequestOptions,
|
||||
} from "./types";
|
||||
|
||||
const DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
};
|
||||
|
||||
export class AitbcClient {
|
||||
private readonly baseUrl: string;
|
||||
private readonly apiKey?: string;
|
||||
private readonly basicAuth?: ClientOptions["basicAuth"];
|
||||
private readonly fetchImpl: typeof fetch;
|
||||
|
||||
constructor(options: ClientOptions) {
|
||||
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
||||
this.apiKey = options.apiKey;
|
||||
this.basicAuth = options.basicAuth;
|
||||
this.fetchImpl = options.fetchImpl ?? fetch;
|
||||
}
|
||||
|
||||
async match(payload: MatchRequest, options?: RequestOptions): Promise<MatchResponse> {
|
||||
const raw = await this.request<any>("POST", "/v1/match", {
|
||||
...options,
|
||||
body: JSON.stringify({
|
||||
job_id: payload.jobId,
|
||||
requirements: payload.requirements ?? {},
|
||||
hints: payload.hints ?? {},
|
||||
top_k: payload.topK ?? 1,
|
||||
}),
|
||||
});
|
||||
return {
|
||||
jobId: raw.job_id,
|
||||
candidates: (raw.candidates ?? []).map((candidate: any) => ({
|
||||
minerId: candidate.miner_id,
|
||||
addr: candidate.addr,
|
||||
proto: candidate.proto,
|
||||
score: candidate.score,
|
||||
explain: candidate.explain,
|
||||
etaMs: candidate.eta_ms,
|
||||
price: candidate.price,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async health(options?: RequestOptions): Promise<HealthResponse> {
|
||||
const raw = await this.request<any>("GET", "/v1/health", options);
|
||||
return {
|
||||
status: raw.status,
|
||||
db: raw.db,
|
||||
redis: raw.redis,
|
||||
minersOnline: raw.miners_online,
|
||||
dbError: raw.db_error ?? null,
|
||||
redisError: raw.redis_error ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async metrics(options?: RequestOptions): Promise<MetricsResponse> {
|
||||
const response = await this.rawRequest("GET", "/metrics", options);
|
||||
const raw = await response.text();
|
||||
return { raw };
|
||||
}
|
||||
|
||||
async sign(request: WalletSignRequest, options?: RequestOptions): Promise<WalletSignResponse> {
|
||||
return this.request<WalletSignResponse>("POST", `/v1/wallets/${encodeURIComponent(request.walletId)}/sign`, {
|
||||
...options,
|
||||
body: JSON.stringify({
|
||||
password: request.password,
|
||||
message_base64: request.messageBase64,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(method: string, path: string, options: RequestOptions = {}): Promise<T> {
|
||||
const response = await this.rawRequest(method, path, options);
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`AITBC request failed (${response.status}): ${text || response.statusText}`);
|
||||
}
|
||||
return text ? (JSON.parse(text) as T) : ({} as T);
|
||||
}
|
||||
|
||||
private async rawRequest(method: string, path: string, options: RequestOptions = {}): Promise<Response> {
|
||||
const url = this.buildUrl(path, options.query);
|
||||
const headers = this.buildHeaders(options.headers);
|
||||
|
||||
return this.fetchImpl(url, {
|
||||
method,
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
private buildUrl(path: string, query?: RequestOptions["query"]): string {
|
||||
const url = new URL(`${this.baseUrl}${path}`);
|
||||
if (query) {
|
||||
for (const [key, value] of Object.entries(query)) {
|
||||
if (value !== undefined) {
|
||||
url.searchParams.set(key, String(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private buildHeaders(extra?: HeadersInit): HeadersInit {
|
||||
const headers: Record<string, string> = { ...DEFAULT_HEADERS };
|
||||
if (this.apiKey) {
|
||||
headers["X-Api-Key"] = this.apiKey;
|
||||
}
|
||||
if (this.basicAuth) {
|
||||
const token = btoa(`${this.basicAuth.username}:${this.basicAuth.password}`);
|
||||
headers["Authorization"] = `Basic ${token}`;
|
||||
}
|
||||
if (extra) {
|
||||
if (extra instanceof Headers) {
|
||||
extra.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
} else if (Array.isArray(extra)) {
|
||||
for (const [key, value] of extra) {
|
||||
headers[key] = value;
|
||||
}
|
||||
} else {
|
||||
Object.assign(headers, extra as Record<string, string>);
|
||||
}
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
59
packages/js/aitbc-sdk/src/types.ts
Normal file
59
packages/js/aitbc-sdk/src/types.ts
Normal file
@ -0,0 +1,59 @@
|
||||
export interface MatchRequest {
|
||||
jobId: string;
|
||||
requirements?: Record<string, unknown>;
|
||||
hints?: Record<string, unknown>;
|
||||
topK?: number;
|
||||
}
|
||||
|
||||
export interface MatchCandidate {
|
||||
minerId: string;
|
||||
addr: string;
|
||||
proto: string;
|
||||
score: number;
|
||||
explain?: string;
|
||||
etaMs?: number;
|
||||
price?: number;
|
||||
}
|
||||
|
||||
export interface MatchResponse {
|
||||
jobId: string;
|
||||
candidates: MatchCandidate[];
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: "ok" | "degraded";
|
||||
db: boolean;
|
||||
redis: boolean;
|
||||
minersOnline: number;
|
||||
dbError?: string | null;
|
||||
redisError?: string | null;
|
||||
}
|
||||
|
||||
export interface MetricsResponse {
|
||||
raw: string;
|
||||
}
|
||||
|
||||
export interface WalletSignRequest {
|
||||
walletId: string;
|
||||
password: string;
|
||||
messageBase64: string;
|
||||
}
|
||||
|
||||
export interface WalletSignResponse {
|
||||
walletId: string;
|
||||
signatureBase64: string;
|
||||
}
|
||||
|
||||
export interface ClientOptions {
|
||||
baseUrl: string;
|
||||
apiKey?: string;
|
||||
basicAuth?: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface RequestOptions extends RequestInit {
|
||||
query?: Record<string, string | number | boolean | undefined>;
|
||||
}
|
||||
16
packages/js/aitbc-sdk/tsconfig.json
Normal file
16
packages/js/aitbc-sdk/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"declaration": true,
|
||||
"declarationDir": "dist",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user