@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.
- package/dist/index.cjs +660 -0
- package/dist/index.d.cts +284 -0
- package/dist/index.d.ts +284 -0
- package/dist/index.js +609 -0
- package/package.json +36 -0
- package/src/budget.ts +182 -0
- package/src/call.ts +164 -0
- package/src/client.ts +90 -0
- package/src/eip3009.ts +99 -0
- package/src/errors.ts +62 -0
- package/src/index.ts +33 -0
- package/src/provider.ts +75 -0
- package/src/recommend.ts +21 -0
- package/src/telemetry.ts +72 -0
- package/src/types.ts +136 -0
- package/src/wallet/types.ts +1 -0
- package/src/wallet/viem.ts +57 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +10 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BudgetController: () => BudgetController,
|
|
34
|
+
BudgetExceededError: () => BudgetExceededError,
|
|
35
|
+
CHAIN_LENS_MARKET_ADDRESSES: () => CHAIN_LENS_MARKET_ADDRESSES,
|
|
36
|
+
ChainLens: () => ChainLens,
|
|
37
|
+
ChainLensCallError: () => ChainLensCallError,
|
|
38
|
+
ChainLensError: () => ChainLensError,
|
|
39
|
+
ChainLensGatewayError: () => ChainLensGatewayError,
|
|
40
|
+
ChainLensResolveError: () => ChainLensResolveError,
|
|
41
|
+
ChainLensSignError: () => ChainLensSignError,
|
|
42
|
+
ProviderClient: () => ProviderClient,
|
|
43
|
+
USDC_ADDRESSES: () => USDC_ADDRESSES,
|
|
44
|
+
ViemWallet: () => ViemWallet,
|
|
45
|
+
atomicToUsdc: () => atomicToUsdc,
|
|
46
|
+
signReceiveWithAuthorization: () => signReceiveWithAuthorization,
|
|
47
|
+
usdcToAtomic: () => usdcToAtomic
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// src/budget.ts
|
|
52
|
+
var import_node_os = require("os");
|
|
53
|
+
var import_node_path = require("path");
|
|
54
|
+
var DEFAULTS = {
|
|
55
|
+
perCallMaxUsdc: 1,
|
|
56
|
+
dailyMaxUsdc: 50,
|
|
57
|
+
monthlyMaxUsdc: 500
|
|
58
|
+
};
|
|
59
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
60
|
+
var MS_PER_MONTH = 30 * MS_PER_DAY;
|
|
61
|
+
var BudgetController = class {
|
|
62
|
+
cfg;
|
|
63
|
+
db = null;
|
|
64
|
+
walletAddress;
|
|
65
|
+
initPromise = null;
|
|
66
|
+
constructor(walletAddress, cfg = {}) {
|
|
67
|
+
this.walletAddress = walletAddress;
|
|
68
|
+
this.cfg = { ...DEFAULTS, ...cfg };
|
|
69
|
+
}
|
|
70
|
+
async getDb() {
|
|
71
|
+
if (this.db) return this.db;
|
|
72
|
+
if (!this.initPromise) {
|
|
73
|
+
this.initPromise = this.openDb();
|
|
74
|
+
}
|
|
75
|
+
await this.initPromise;
|
|
76
|
+
return this.db;
|
|
77
|
+
}
|
|
78
|
+
async openDb() {
|
|
79
|
+
try {
|
|
80
|
+
const { Level } = await import("level");
|
|
81
|
+
const dbPath = (0, import_node_path.join)(
|
|
82
|
+
(0, import_node_os.homedir)(),
|
|
83
|
+
".chainlens",
|
|
84
|
+
"budget",
|
|
85
|
+
sanitizeAddress(this.walletAddress)
|
|
86
|
+
);
|
|
87
|
+
const level = new Level(dbPath, { valueEncoding: "json" });
|
|
88
|
+
await level.open();
|
|
89
|
+
this.db = new LevelDB(level);
|
|
90
|
+
} catch {
|
|
91
|
+
process.stderr.write(
|
|
92
|
+
"chain-lens SDK: LevelDB unavailable, using in-memory budget storage.\n"
|
|
93
|
+
);
|
|
94
|
+
this.db = new InMemoryDB();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async canSpend(amount) {
|
|
98
|
+
if (amount > this.cfg.perCallMaxUsdc) {
|
|
99
|
+
return { ok: false, reason: `per-call cap: $${amount} > $${this.cfg.perCallMaxUsdc}` };
|
|
100
|
+
}
|
|
101
|
+
const db = await this.getDb();
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const records = await db.readAll();
|
|
104
|
+
const alive = records.filter((r) => now - r.ts < MS_PER_MONTH);
|
|
105
|
+
const dailySpend = alive.filter((r) => now - r.ts < MS_PER_DAY).reduce((s, r) => s + r.amount, 0);
|
|
106
|
+
const monthlySpend = alive.reduce((s, r) => s + r.amount, 0);
|
|
107
|
+
if (dailySpend + amount > this.cfg.dailyMaxUsdc) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
reason: `daily cap: $${(dailySpend + amount).toFixed(4)} > $${this.cfg.dailyMaxUsdc}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (monthlySpend + amount > this.cfg.monthlyMaxUsdc) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reason: `monthly cap: $${(monthlySpend + amount).toFixed(4)} > $${this.cfg.monthlyMaxUsdc}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
async debit(amount, idempotencyKey) {
|
|
122
|
+
const db = await this.getDb();
|
|
123
|
+
if (idempotencyKey) {
|
|
124
|
+
const existing = await db.readAll();
|
|
125
|
+
if (existing.some((r) => r.idempotencyKey === idempotencyKey)) return;
|
|
126
|
+
}
|
|
127
|
+
await db.append({ ts: Date.now(), amount, idempotencyKey });
|
|
128
|
+
await db.evictBefore(Date.now() - MS_PER_MONTH);
|
|
129
|
+
}
|
|
130
|
+
async currentSpend() {
|
|
131
|
+
const db = await this.getDb();
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const records = await db.readAll();
|
|
134
|
+
const dailyUsdc = records.filter((r) => now - r.ts < MS_PER_DAY).reduce((s, r) => s + r.amount, 0);
|
|
135
|
+
const monthlyUsdc = records.filter((r) => now - r.ts < MS_PER_MONTH).reduce((s, r) => s + r.amount, 0);
|
|
136
|
+
return { dailyUsdc, monthlyUsdc };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
var InMemoryDB = class {
|
|
140
|
+
records = [];
|
|
141
|
+
async readAll() {
|
|
142
|
+
return [...this.records];
|
|
143
|
+
}
|
|
144
|
+
async append(record) {
|
|
145
|
+
this.records.push(record);
|
|
146
|
+
}
|
|
147
|
+
async evictBefore(ts) {
|
|
148
|
+
this.records = this.records.filter((r) => r.ts >= ts);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
var LevelDB = class {
|
|
152
|
+
constructor(level) {
|
|
153
|
+
this.level = level;
|
|
154
|
+
}
|
|
155
|
+
level;
|
|
156
|
+
async readAll() {
|
|
157
|
+
const records = [];
|
|
158
|
+
for await (const value of this.level.values()) {
|
|
159
|
+
try {
|
|
160
|
+
records.push(JSON.parse(value));
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return records;
|
|
165
|
+
}
|
|
166
|
+
async append(record) {
|
|
167
|
+
const key = `${record.ts}-${Math.random().toString(36).slice(2)}`;
|
|
168
|
+
await this.level.put(key, JSON.stringify(record));
|
|
169
|
+
}
|
|
170
|
+
async evictBefore(ts) {
|
|
171
|
+
const batch = this.level.batch();
|
|
172
|
+
for await (const [key, value] of this.level.iterator()) {
|
|
173
|
+
try {
|
|
174
|
+
const record = JSON.parse(value);
|
|
175
|
+
if (record.ts < ts) batch.del(key);
|
|
176
|
+
} catch {
|
|
177
|
+
batch.del(key);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
await batch.write();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
function sanitizeAddress(addr) {
|
|
184
|
+
return addr.toLowerCase().replace(/[^a-f0-9x]/g, "").slice(0, 42);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/telemetry.ts
|
|
188
|
+
var import_node_os2 = require("os");
|
|
189
|
+
var import_node_path2 = require("path");
|
|
190
|
+
var import_node_crypto = require("crypto");
|
|
191
|
+
var import_promises = require("fs/promises");
|
|
192
|
+
var TelemetryRecorder = class {
|
|
193
|
+
cfg;
|
|
194
|
+
constructor(cfg) {
|
|
195
|
+
this.cfg = cfg;
|
|
196
|
+
}
|
|
197
|
+
async record(entry) {
|
|
198
|
+
if (!this.cfg.enabled) return;
|
|
199
|
+
const line = JSON.stringify(entry) + "\n";
|
|
200
|
+
const dir = (0, import_node_path2.join)((0, import_node_os2.homedir)(), ".chainlens", "telemetry");
|
|
201
|
+
const filePath = (0, import_node_path2.join)(dir, `${sanitizeAddress2(this.cfg.walletAddress)}.jsonl`);
|
|
202
|
+
try {
|
|
203
|
+
await (0, import_promises.mkdir)(dir, { recursive: true });
|
|
204
|
+
await (0, import_promises.appendFile)(filePath, line, "utf8");
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
if (this.cfg.upload) {
|
|
208
|
+
void this.uploadAsync(entry);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async uploadAsync(entry) {
|
|
212
|
+
try {
|
|
213
|
+
await fetch(`${this.cfg.gatewayUrl}/v1/telemetry/batch`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: { "Content-Type": "application/json" },
|
|
216
|
+
body: JSON.stringify({ events: [entry] })
|
|
217
|
+
});
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
function hashParams(params) {
|
|
223
|
+
const json = params != null ? JSON.stringify(params) : "";
|
|
224
|
+
return (0, import_node_crypto.createHash)("sha256").update(json, "utf8").digest("hex").slice(0, 16);
|
|
225
|
+
}
|
|
226
|
+
function sanitizeAddress2(addr) {
|
|
227
|
+
return addr.toLowerCase().replace(/[^a-f0-9x]/g, "").slice(0, 42);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/eip3009.ts
|
|
231
|
+
var import_node_crypto2 = require("crypto");
|
|
232
|
+
var USDC_ADDRESSES = {
|
|
233
|
+
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
234
|
+
// Base Sepolia
|
|
235
|
+
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
|
|
236
|
+
// Base Mainnet
|
|
237
|
+
};
|
|
238
|
+
var CHAIN_LENS_MARKET_ADDRESSES = {
|
|
239
|
+
84532: "0x45bB56fDB0E6bb14d178E417b67Ed7B3323ffFf7",
|
|
240
|
+
// Base Sepolia
|
|
241
|
+
8453: "0x0000000000000000000000000000000000000000"
|
|
242
|
+
// placeholder — not deployed yet
|
|
243
|
+
};
|
|
244
|
+
var RECEIVE_WITH_AUTHORIZATION_TYPES = {
|
|
245
|
+
ReceiveWithAuthorization: [
|
|
246
|
+
{ name: "from", type: "address" },
|
|
247
|
+
{ name: "to", type: "address" },
|
|
248
|
+
{ name: "value", type: "uint256" },
|
|
249
|
+
{ name: "validAfter", type: "uint256" },
|
|
250
|
+
{ name: "validBefore", type: "uint256" },
|
|
251
|
+
{ name: "nonce", type: "bytes32" }
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
async function signReceiveWithAuthorization(opts) {
|
|
255
|
+
const { wallet, chainId, amount, to } = opts;
|
|
256
|
+
const usdcAddress = USDC_ADDRESSES[chainId];
|
|
257
|
+
if (!usdcAddress) throw new Error(`No USDC address for chainId=${chainId}`);
|
|
258
|
+
const from = await wallet.address();
|
|
259
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
260
|
+
const validAfter = BigInt(now - 60);
|
|
261
|
+
const validBefore = BigInt(now + 300);
|
|
262
|
+
const nonce = "0x" + (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
263
|
+
const typedData = {
|
|
264
|
+
domain: {
|
|
265
|
+
name: "USD Coin",
|
|
266
|
+
version: "2",
|
|
267
|
+
chainId,
|
|
268
|
+
verifyingContract: usdcAddress
|
|
269
|
+
},
|
|
270
|
+
types: RECEIVE_WITH_AUTHORIZATION_TYPES,
|
|
271
|
+
primaryType: "ReceiveWithAuthorization",
|
|
272
|
+
message: {
|
|
273
|
+
from,
|
|
274
|
+
to,
|
|
275
|
+
value: amount,
|
|
276
|
+
validAfter,
|
|
277
|
+
validBefore,
|
|
278
|
+
nonce
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
const sig = await wallet.signTypedData(typedData);
|
|
282
|
+
return {
|
|
283
|
+
from,
|
|
284
|
+
to,
|
|
285
|
+
amount: amount.toString(),
|
|
286
|
+
validAfter: validAfter.toString(),
|
|
287
|
+
validBefore: validBefore.toString(),
|
|
288
|
+
nonce,
|
|
289
|
+
v: sig.v,
|
|
290
|
+
r: sig.r,
|
|
291
|
+
s: sig.s
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function usdcToAtomic(usdc) {
|
|
295
|
+
return BigInt(Math.round(usdc * 1e6));
|
|
296
|
+
}
|
|
297
|
+
function atomicToUsdc(atomic) {
|
|
298
|
+
return Number(BigInt(atomic)) / 1e6;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/errors.ts
|
|
302
|
+
var ChainLensError = class extends Error {
|
|
303
|
+
code;
|
|
304
|
+
cause;
|
|
305
|
+
constructor(message, code, cause) {
|
|
306
|
+
super(message);
|
|
307
|
+
this.name = "ChainLensError";
|
|
308
|
+
this.code = code;
|
|
309
|
+
this.cause = cause;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
var ChainLensResolveError = class extends ChainLensError {
|
|
313
|
+
constructor(message, cause) {
|
|
314
|
+
super(message, "RESOLVE", cause);
|
|
315
|
+
this.name = "ChainLensResolveError";
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
var BudgetExceededError = class extends ChainLensError {
|
|
319
|
+
reason;
|
|
320
|
+
constructor(reason) {
|
|
321
|
+
super(`Budget exceeded: ${reason}`, "BUDGET");
|
|
322
|
+
this.name = "BudgetExceededError";
|
|
323
|
+
this.reason = reason;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
var ChainLensSignError = class extends ChainLensError {
|
|
327
|
+
constructor(message, cause) {
|
|
328
|
+
super(message, "SIGN", cause);
|
|
329
|
+
this.name = "ChainLensSignError";
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
var ChainLensGatewayError = class extends ChainLensError {
|
|
333
|
+
status;
|
|
334
|
+
constructor(message, status, cause) {
|
|
335
|
+
super(message, "GATEWAY", cause);
|
|
336
|
+
this.name = "ChainLensGatewayError";
|
|
337
|
+
this.status = status;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
var ChainLensCallError = class extends ChainLensError {
|
|
341
|
+
failure;
|
|
342
|
+
constructor(failure) {
|
|
343
|
+
super(`Call failed: ${failure.kind} \u2014 ${failure.hint}`, "CALL");
|
|
344
|
+
this.name = "ChainLensCallError";
|
|
345
|
+
this.failure = failure;
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// src/call.ts
|
|
350
|
+
async function fetchListingInfo(gatewayUrl, listingId) {
|
|
351
|
+
const res = await fetch(`${gatewayUrl}/v1/listings/${listingId}`);
|
|
352
|
+
if (!res.ok) {
|
|
353
|
+
const body = await res.text().catch(() => "");
|
|
354
|
+
throw new ChainLensResolveError(
|
|
355
|
+
`Failed to fetch listing ${listingId}: ${res.status} ${body}`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
return res.json();
|
|
359
|
+
}
|
|
360
|
+
async function executeCall(cfg, budget, telemetry, listingId, params, options = {}) {
|
|
361
|
+
const t0 = Date.now();
|
|
362
|
+
let listing;
|
|
363
|
+
try {
|
|
364
|
+
listing = await fetchListingInfo(cfg.gatewayUrl, listingId);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
throw err instanceof ChainLensResolveError ? err : new ChainLensResolveError(String(err));
|
|
367
|
+
}
|
|
368
|
+
const priceUsdc = listing.priceAtomic ? atomicToUsdc(listing.priceAtomic) : 0;
|
|
369
|
+
const effectiveMaxUsdc = options.maxUsdc ?? priceUsdc;
|
|
370
|
+
const budgetCheck = await budget.canSpend(effectiveMaxUsdc);
|
|
371
|
+
if (!budgetCheck.ok) {
|
|
372
|
+
throw new BudgetExceededError(budgetCheck.reason);
|
|
373
|
+
}
|
|
374
|
+
const marketAddress = CHAIN_LENS_MARKET_ADDRESSES[cfg.chainId];
|
|
375
|
+
if (!marketAddress) throw new ChainLensResolveError(`No market address for chainId=${cfg.chainId}`);
|
|
376
|
+
const amountAtomic = usdcToAtomic(effectiveMaxUsdc);
|
|
377
|
+
let auth;
|
|
378
|
+
try {
|
|
379
|
+
auth = await signReceiveWithAuthorization({
|
|
380
|
+
wallet: cfg.wallet,
|
|
381
|
+
chainId: cfg.chainId,
|
|
382
|
+
amount: amountAtomic,
|
|
383
|
+
to: marketAddress,
|
|
384
|
+
signal: options.signal
|
|
385
|
+
});
|
|
386
|
+
} catch (err) {
|
|
387
|
+
throw new ChainLensSignError(String(err), err);
|
|
388
|
+
}
|
|
389
|
+
let res;
|
|
390
|
+
try {
|
|
391
|
+
res = await fetch(`${cfg.gatewayUrl}/v1/call`, {
|
|
392
|
+
method: "POST",
|
|
393
|
+
headers: { "Content-Type": "application/json" },
|
|
394
|
+
signal: options.signal,
|
|
395
|
+
body: JSON.stringify({
|
|
396
|
+
listingId,
|
|
397
|
+
params,
|
|
398
|
+
auth: {
|
|
399
|
+
from: auth.from,
|
|
400
|
+
to: auth.to,
|
|
401
|
+
amount: auth.amount,
|
|
402
|
+
validAfter: auth.validAfter,
|
|
403
|
+
validBefore: auth.validBefore,
|
|
404
|
+
nonce: auth.nonce,
|
|
405
|
+
v: auth.v,
|
|
406
|
+
r: auth.r,
|
|
407
|
+
s: auth.s
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
});
|
|
411
|
+
} catch (err) {
|
|
412
|
+
const latencyMs2 = Date.now() - t0;
|
|
413
|
+
await telemetry.record({
|
|
414
|
+
ts: t0,
|
|
415
|
+
listingId,
|
|
416
|
+
amountUsdc: effectiveMaxUsdc,
|
|
417
|
+
latencyMs: latencyMs2,
|
|
418
|
+
ok: false,
|
|
419
|
+
failure: { kind: "unknown", hint: String(err) },
|
|
420
|
+
paramsHash: hashParams(params)
|
|
421
|
+
});
|
|
422
|
+
throw new ChainLensGatewayError(`Network error: ${String(err)}`, 0, err);
|
|
423
|
+
}
|
|
424
|
+
const latencyMs = Date.now() - t0;
|
|
425
|
+
const body = await res.json().catch(() => null);
|
|
426
|
+
if (!res.ok || !body?.ok) {
|
|
427
|
+
const failure = body?.failure ?? { kind: "unknown", hint: `HTTP ${res.status}` };
|
|
428
|
+
await telemetry.record({
|
|
429
|
+
ts: t0,
|
|
430
|
+
listingId,
|
|
431
|
+
amountUsdc: effectiveMaxUsdc,
|
|
432
|
+
latencyMs,
|
|
433
|
+
ok: false,
|
|
434
|
+
failure,
|
|
435
|
+
paramsHash: hashParams(params)
|
|
436
|
+
});
|
|
437
|
+
throw new ChainLensCallError(failure);
|
|
438
|
+
}
|
|
439
|
+
const amountUsdc = body.amount ? atomicToUsdc(body.amount) : effectiveMaxUsdc;
|
|
440
|
+
const feeUsdc = body.fee ? atomicToUsdc(body.fee) : 0;
|
|
441
|
+
const netUsdc = body.net ? atomicToUsdc(body.net) : amountUsdc - feeUsdc;
|
|
442
|
+
await budget.debit(amountUsdc, options.idempotencyKey);
|
|
443
|
+
await telemetry.record({
|
|
444
|
+
ts: t0,
|
|
445
|
+
listingId,
|
|
446
|
+
amountUsdc,
|
|
447
|
+
latencyMs,
|
|
448
|
+
ok: true,
|
|
449
|
+
txHash: body.settlement?.txHash,
|
|
450
|
+
paramsHash: hashParams(params)
|
|
451
|
+
});
|
|
452
|
+
return {
|
|
453
|
+
ok: true,
|
|
454
|
+
data: body.response,
|
|
455
|
+
listingId,
|
|
456
|
+
amountUsdc,
|
|
457
|
+
feeUsdc,
|
|
458
|
+
netUsdc,
|
|
459
|
+
settlement: body.settlement,
|
|
460
|
+
latencyMs,
|
|
461
|
+
attemptIndex: 0
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/recommend.ts
|
|
466
|
+
async function fetchRecommendations(gatewayUrl, task, maxResults = 5) {
|
|
467
|
+
const res = await fetch(`${gatewayUrl}/v1/recommend`, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
headers: { "Content-Type": "application/json" },
|
|
470
|
+
body: JSON.stringify({ task, maxResults })
|
|
471
|
+
});
|
|
472
|
+
if (!res.ok) {
|
|
473
|
+
const body = await res.text().catch(() => "");
|
|
474
|
+
throw new ChainLensResolveError(`recommend failed: ${res.status} ${body}`);
|
|
475
|
+
}
|
|
476
|
+
const data = await res.json();
|
|
477
|
+
return data.listings ?? [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/provider.ts
|
|
481
|
+
var ProviderClient = class {
|
|
482
|
+
constructor(gatewayUrl, wallet) {
|
|
483
|
+
this.gatewayUrl = gatewayUrl;
|
|
484
|
+
this.wallet = wallet;
|
|
485
|
+
}
|
|
486
|
+
gatewayUrl;
|
|
487
|
+
wallet;
|
|
488
|
+
async claimable() {
|
|
489
|
+
const address = await this.wallet.address();
|
|
490
|
+
const res = await fetch(
|
|
491
|
+
`${this.gatewayUrl}/v1/provider/claimable?address=${address}`
|
|
492
|
+
);
|
|
493
|
+
if (!res.ok) {
|
|
494
|
+
throw new ChainLensGatewayError(
|
|
495
|
+
`claimable fetch failed: ${res.status}`,
|
|
496
|
+
res.status
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return res.json();
|
|
500
|
+
}
|
|
501
|
+
async claim() {
|
|
502
|
+
const claimable = await this.claimable();
|
|
503
|
+
if (BigInt(claimable.atomicBalance) === 0n) {
|
|
504
|
+
return { skipped: true };
|
|
505
|
+
}
|
|
506
|
+
const address = await this.wallet.address();
|
|
507
|
+
const res = await fetch(`${this.gatewayUrl}/v1/provider/claim`, {
|
|
508
|
+
method: "POST",
|
|
509
|
+
headers: { "Content-Type": "application/json" },
|
|
510
|
+
body: JSON.stringify({ address })
|
|
511
|
+
});
|
|
512
|
+
if (!res.ok) {
|
|
513
|
+
const body = await res.text().catch(() => "");
|
|
514
|
+
throw new ChainLensGatewayError(`claim failed: ${res.status} ${body}`, res.status);
|
|
515
|
+
}
|
|
516
|
+
const data = await res.json();
|
|
517
|
+
return { txHash: data.txHash };
|
|
518
|
+
}
|
|
519
|
+
async listingDashboard(listingId) {
|
|
520
|
+
const address = await this.wallet.address();
|
|
521
|
+
const res = await fetch(
|
|
522
|
+
`${this.gatewayUrl}/v1/provider/listing/${listingId}?address=${address}`
|
|
523
|
+
);
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
if (res.status === 403) {
|
|
526
|
+
throw new ChainLensResolveError(`Not authorized to view listing ${listingId}`);
|
|
527
|
+
}
|
|
528
|
+
throw new ChainLensGatewayError(
|
|
529
|
+
`dashboard fetch failed: ${res.status}`,
|
|
530
|
+
res.status
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
return res.json();
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
// src/client.ts
|
|
538
|
+
var DEFAULT_GATEWAY = "https://chainlens.pelicanlab.dev";
|
|
539
|
+
var RETRYABLE_KINDS = /* @__PURE__ */ new Set([
|
|
540
|
+
"http_5xx",
|
|
541
|
+
"timeout",
|
|
542
|
+
"schema_mismatch"
|
|
543
|
+
]);
|
|
544
|
+
var ChainLens = class {
|
|
545
|
+
cfg;
|
|
546
|
+
budget = null;
|
|
547
|
+
telemetry = null;
|
|
548
|
+
walletAddress = null;
|
|
549
|
+
provider;
|
|
550
|
+
constructor(cfg) {
|
|
551
|
+
this.cfg = { gatewayUrl: DEFAULT_GATEWAY, ...cfg };
|
|
552
|
+
this.provider = new ProviderClient(this.cfg.gatewayUrl, cfg.wallet);
|
|
553
|
+
}
|
|
554
|
+
async init() {
|
|
555
|
+
if (!this.walletAddress) {
|
|
556
|
+
this.walletAddress = await this.cfg.wallet.address();
|
|
557
|
+
}
|
|
558
|
+
if (!this.budget) {
|
|
559
|
+
this.budget = new BudgetController(this.walletAddress, this.cfg.budget);
|
|
560
|
+
}
|
|
561
|
+
if (!this.telemetry) {
|
|
562
|
+
this.telemetry = new TelemetryRecorder({
|
|
563
|
+
enabled: this.cfg.telemetry?.enabled ?? true,
|
|
564
|
+
upload: this.cfg.telemetry?.upload ?? false,
|
|
565
|
+
bufferMaxEntries: this.cfg.telemetry?.bufferMaxEntries ?? 1e3,
|
|
566
|
+
gatewayUrl: this.cfg.gatewayUrl,
|
|
567
|
+
walletAddress: this.walletAddress
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return { budget: this.budget, telemetry: this.telemetry };
|
|
571
|
+
}
|
|
572
|
+
async call(listingId, params, options = {}) {
|
|
573
|
+
const { budget, telemetry } = await this.init();
|
|
574
|
+
const maxAttempts = options.fallback !== false && this.cfg.fallback?.enabled ? this.cfg.fallback.maxAttempts ?? 2 : 1;
|
|
575
|
+
let lastErr;
|
|
576
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
577
|
+
try {
|
|
578
|
+
const result = await executeCall(this.cfg, budget, telemetry, listingId, params, options);
|
|
579
|
+
return { ...result, attemptIndex: attempt };
|
|
580
|
+
} catch (err) {
|
|
581
|
+
lastErr = err;
|
|
582
|
+
if (attempt < maxAttempts - 1 && err instanceof ChainLensCallError && RETRYABLE_KINDS.has(err.failure.kind)) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
throw lastErr;
|
|
589
|
+
}
|
|
590
|
+
async recommend(task, maxResults = 5) {
|
|
591
|
+
return fetchRecommendations(this.cfg.gatewayUrl, task, maxResults);
|
|
592
|
+
}
|
|
593
|
+
async currentSpend() {
|
|
594
|
+
const { budget } = await this.init();
|
|
595
|
+
return budget.currentSpend();
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// src/wallet/viem.ts
|
|
600
|
+
var import_viem = require("viem");
|
|
601
|
+
var ViemWallet = class {
|
|
602
|
+
constructor(client) {
|
|
603
|
+
this.client = client;
|
|
604
|
+
}
|
|
605
|
+
client;
|
|
606
|
+
async address() {
|
|
607
|
+
const accounts = await this.client.getAddresses();
|
|
608
|
+
const addr = accounts[0];
|
|
609
|
+
if (!addr) throw new Error("ViemWallet: no accounts available");
|
|
610
|
+
return addr;
|
|
611
|
+
}
|
|
612
|
+
async signTypedData(typedData) {
|
|
613
|
+
const { domain, types, primaryType, message } = typedData;
|
|
614
|
+
const sig = await this.client.signTypedData({
|
|
615
|
+
domain: {
|
|
616
|
+
name: domain.name,
|
|
617
|
+
version: domain.version,
|
|
618
|
+
chainId: domain.chainId,
|
|
619
|
+
verifyingContract: domain.verifyingContract
|
|
620
|
+
},
|
|
621
|
+
types,
|
|
622
|
+
primaryType,
|
|
623
|
+
message
|
|
624
|
+
});
|
|
625
|
+
const parsed = (0, import_viem.parseSignature)(sig);
|
|
626
|
+
return {
|
|
627
|
+
v: (0, import_viem.hexToNumber)((0, import_viem.numberToHex)(parsed.v ?? 27n)),
|
|
628
|
+
r: parsed.r,
|
|
629
|
+
s: parsed.s
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async sendTransaction(tx) {
|
|
633
|
+
const from = await this.address();
|
|
634
|
+
return this.client.sendTransaction({
|
|
635
|
+
account: from,
|
|
636
|
+
to: tx.to,
|
|
637
|
+
data: tx.data,
|
|
638
|
+
value: tx.value,
|
|
639
|
+
chain: this.client.chain ?? null
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
644
|
+
0 && (module.exports = {
|
|
645
|
+
BudgetController,
|
|
646
|
+
BudgetExceededError,
|
|
647
|
+
CHAIN_LENS_MARKET_ADDRESSES,
|
|
648
|
+
ChainLens,
|
|
649
|
+
ChainLensCallError,
|
|
650
|
+
ChainLensError,
|
|
651
|
+
ChainLensGatewayError,
|
|
652
|
+
ChainLensResolveError,
|
|
653
|
+
ChainLensSignError,
|
|
654
|
+
ProviderClient,
|
|
655
|
+
USDC_ADDRESSES,
|
|
656
|
+
ViemWallet,
|
|
657
|
+
atomicToUsdc,
|
|
658
|
+
signReceiveWithAuthorization,
|
|
659
|
+
usdcToAtomic
|
|
660
|
+
});
|