@chain-lens/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ import type { RankedListing } from "./types.js";
2
+ import { ChainLensResolveError } from "./errors.js";
3
+
4
+ export async function fetchRecommendations(
5
+ gatewayUrl: string,
6
+ task: string,
7
+ maxResults = 5,
8
+ ): Promise<RankedListing[]> {
9
+ const res = await fetch(`${gatewayUrl}/v1/recommend`, {
10
+ method: "POST",
11
+ headers: { "Content-Type": "application/json" },
12
+ body: JSON.stringify({ task, maxResults }),
13
+ });
14
+ if (!res.ok) {
15
+ const body = await res.text().catch(() => "");
16
+ throw new ChainLensResolveError(`recommend failed: ${res.status} ${body}`);
17
+ }
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ const data = (await res.json()) as any;
20
+ return (data.listings ?? []) as RankedListing[];
21
+ }
@@ -0,0 +1,72 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { appendFile, mkdir } from "node:fs/promises";
5
+ import type { FailureMetadata } from "./types.js";
6
+
7
+ export interface TelemetryEntry {
8
+ ts: number;
9
+ listingId: number;
10
+ amountUsdc: number;
11
+ latencyMs: number;
12
+ ok: boolean;
13
+ failure?: FailureMetadata;
14
+ txHash?: string;
15
+ paramsHash?: string;
16
+ }
17
+
18
+ export interface TelemetryConfig {
19
+ enabled: boolean;
20
+ upload: boolean;
21
+ bufferMaxEntries: number;
22
+ gatewayUrl: string;
23
+ walletAddress: string;
24
+ }
25
+
26
+ export class TelemetryRecorder {
27
+ private readonly cfg: TelemetryConfig;
28
+
29
+ constructor(cfg: TelemetryConfig) {
30
+ this.cfg = cfg;
31
+ }
32
+
33
+ async record(entry: TelemetryEntry): Promise<void> {
34
+ if (!this.cfg.enabled) return;
35
+
36
+ const line = JSON.stringify(entry) + "\n";
37
+ const dir = join(homedir(), ".chainlens", "telemetry");
38
+ const filePath = join(dir, `${sanitizeAddress(this.cfg.walletAddress)}.jsonl`);
39
+
40
+ try {
41
+ await mkdir(dir, { recursive: true });
42
+ await appendFile(filePath, line, "utf8");
43
+ } catch {
44
+ // non-blocking — ignore write failures
45
+ }
46
+
47
+ if (this.cfg.upload) {
48
+ void this.uploadAsync(entry);
49
+ }
50
+ }
51
+
52
+ private async uploadAsync(entry: TelemetryEntry): Promise<void> {
53
+ try {
54
+ await fetch(`${this.cfg.gatewayUrl}/v1/telemetry/batch`, {
55
+ method: "POST",
56
+ headers: { "Content-Type": "application/json" },
57
+ body: JSON.stringify({ events: [entry] }),
58
+ });
59
+ } catch {
60
+ // fire-and-forget, ignore errors
61
+ }
62
+ }
63
+ }
64
+
65
+ export function hashParams(params: unknown): string {
66
+ const json = params != null ? JSON.stringify(params) : "";
67
+ return createHash("sha256").update(json, "utf8").digest("hex").slice(0, 16);
68
+ }
69
+
70
+ function sanitizeAddress(addr: string): string {
71
+ return addr.toLowerCase().replace(/[^a-f0-9x]/g, "").slice(0, 42);
72
+ }
package/src/types.ts ADDED
@@ -0,0 +1,136 @@
1
+ export interface ChainLensConfig {
2
+ /** Base URL of the Gateway. Default: https://chainlens.pelicanlab.dev */
3
+ gatewayUrl?: string;
4
+
5
+ /** Wallet adapter — must be able to sign EIP-3009 typed data. */
6
+ wallet: WalletAdapter;
7
+
8
+ /** Off-chain budget enforcement. */
9
+ budget?: {
10
+ perCallMaxUsdc?: number;
11
+ dailyMaxUsdc?: number;
12
+ monthlyMaxUsdc?: number;
13
+ };
14
+
15
+ /** Local telemetry config. */
16
+ telemetry?: {
17
+ /** Default: true */
18
+ enabled?: boolean;
19
+ /** Upload to Gateway. Default: false (explicit opt-in). */
20
+ upload?: boolean;
21
+ /** Max entries in local JSONL buffer. Default: 1000. */
22
+ bufferMaxEntries?: number;
23
+ };
24
+
25
+ /** Auto-fallback on provider failure. */
26
+ fallback?: {
27
+ enabled: boolean;
28
+ /** Total attempts including primary. Default: 2. */
29
+ maxAttempts?: number;
30
+ };
31
+
32
+ /** Chain ID. 84532 = Base Sepolia, 8453 = Base Mainnet. */
33
+ chainId: 84532 | 8453;
34
+ }
35
+
36
+ export interface WalletAdapter {
37
+ address(): Promise<`0x${string}`>;
38
+ signTypedData(typedData: TypedData): Promise<{ v: number; r: `0x${string}`; s: `0x${string}` }>;
39
+ sendTransaction(tx: TxRequest): Promise<`0x${string}`>;
40
+ }
41
+
42
+ export interface TypedData {
43
+ domain: {
44
+ name: string;
45
+ version: string;
46
+ chainId: number;
47
+ verifyingContract: `0x${string}`;
48
+ };
49
+ types: Record<string, Array<{ name: string; type: string }>>;
50
+ primaryType: string;
51
+ message: Record<string, unknown>;
52
+ }
53
+
54
+ export interface TxRequest {
55
+ to: `0x${string}`;
56
+ data?: `0x${string}`;
57
+ value?: bigint;
58
+ }
59
+
60
+ export interface CallOptions {
61
+ fallback?: boolean;
62
+ maxUsdc?: number;
63
+ idempotencyKey?: string;
64
+ signal?: AbortSignal;
65
+ }
66
+
67
+ export interface CallResult<T = unknown> {
68
+ ok: true;
69
+ data: T;
70
+ listingId: number;
71
+ amountUsdc: number;
72
+ feeUsdc: number;
73
+ netUsdc: number;
74
+ settlement: {
75
+ txHash: `0x${string}`;
76
+ blockNumber: number;
77
+ };
78
+ latencyMs: number;
79
+ attemptIndex: number;
80
+ }
81
+
82
+ export interface RankedListing {
83
+ listingId: number | null;
84
+ name: string | null;
85
+ score: number;
86
+ reasons: string[];
87
+ source: "chainlens" | "coinbase_bazaar" | "fixture";
88
+ verifiedByChainLens: boolean;
89
+ resource?: string;
90
+ network?: string;
91
+ asset?: string;
92
+ payTo?: string;
93
+ stats: {
94
+ successRate: number;
95
+ p50LatencyMs: number;
96
+ p95LatencyMs: number;
97
+ avgCostUsdc: number;
98
+ sampleSize: number;
99
+ };
100
+ }
101
+
102
+ export interface FailureMetadata {
103
+ kind:
104
+ | "schema_mismatch"
105
+ | "http_4xx"
106
+ | "http_5xx"
107
+ | "timeout"
108
+ | "auth"
109
+ | "rate_limit"
110
+ | "gateway_error"
111
+ | "budget"
112
+ | "sign"
113
+ | "resolve"
114
+ | "unknown";
115
+ hint: string;
116
+ providerStatus?: number;
117
+ evaluatorLayer?: 1 | 2 | 3 | 4;
118
+ rawProviderError?: string;
119
+ }
120
+
121
+ export interface ListingInfo {
122
+ listingId: number;
123
+ name: string | null;
124
+ priceAtomic: string | null;
125
+ maxLatencyMs: number;
126
+ taskCategory: string;
127
+ outputSchema: unknown | null;
128
+ payout: string;
129
+ active: boolean;
130
+ }
131
+
132
+ export interface BudgetConfig {
133
+ perCallMaxUsdc: number;
134
+ dailyMaxUsdc: number;
135
+ monthlyMaxUsdc: number;
136
+ }
@@ -0,0 +1 @@
1
+ export type { WalletAdapter, TypedData, TxRequest } from "../types.js";
@@ -0,0 +1,57 @@
1
+ import type { WalletClient, Hex, TypedDataDefinition } from "viem";
2
+ import { hexToNumber, numberToHex, parseSignature } from "viem";
3
+ import type { WalletAdapter, TypedData, TxRequest } from "./types.js";
4
+
5
+ /**
6
+ * WalletAdapter backed by a viem WalletClient.
7
+ *
8
+ * Usage:
9
+ * const walletClient = createWalletClient({ account, chain, transport });
10
+ * const wallet = new ViemWallet(walletClient);
11
+ */
12
+ export class ViemWallet implements WalletAdapter {
13
+ constructor(private readonly client: WalletClient) {}
14
+
15
+ async address(): Promise<`0x${string}`> {
16
+ const accounts = await this.client.getAddresses();
17
+ const addr = accounts[0];
18
+ if (!addr) throw new Error("ViemWallet: no accounts available");
19
+ return addr;
20
+ }
21
+
22
+ async signTypedData(
23
+ typedData: TypedData,
24
+ ): Promise<{ v: number; r: `0x${string}`; s: `0x${string}` }> {
25
+ const { domain, types, primaryType, message } = typedData;
26
+
27
+ const sig = await this.client.signTypedData({
28
+ domain: {
29
+ name: domain.name,
30
+ version: domain.version,
31
+ chainId: domain.chainId,
32
+ verifyingContract: domain.verifyingContract,
33
+ },
34
+ types: types as unknown as TypedDataDefinition["types"],
35
+ primaryType,
36
+ message: message as Record<string, unknown>,
37
+ } as Parameters<WalletClient["signTypedData"]>[0]);
38
+
39
+ const parsed = parseSignature(sig as Hex);
40
+ return {
41
+ v: hexToNumber(numberToHex(parsed.v ?? 27n)),
42
+ r: parsed.r as `0x${string}`,
43
+ s: parsed.s as `0x${string}`,
44
+ };
45
+ }
46
+
47
+ async sendTransaction(tx: TxRequest): Promise<`0x${string}`> {
48
+ const from = await this.address();
49
+ return this.client.sendTransaction({
50
+ account: from,
51
+ to: tx.to,
52
+ data: tx.data,
53
+ value: tx.value,
54
+ chain: this.client.chain ?? null,
55
+ });
56
+ }
57
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true
15
+ },
16
+ "include": ["src/**/*.ts"],
17
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
18
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ splitting: false,
8
+ clean: true,
9
+ external: ["viem"],
10
+ });