@elisym/sdk 0.5.0 → 0.7.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 CHANGED
@@ -3,6 +3,7 @@
3
3
  var system = require('@solana-program/system');
4
4
  var kit = require('@solana/kit');
5
5
  var Decimal2 = require('decimal.js-light');
6
+ var zod = require('zod');
6
7
  var nostrTools = require('nostr-tools');
7
8
  var nip44 = require('nostr-tools/nip44');
8
9
 
@@ -215,7 +216,118 @@ function assertExpiry(createdAt, expirySecs) {
215
216
  }
216
217
  }
217
218
 
219
+ // src/payment/priorityFee.ts
220
+ var PRIORITY_FEE_FLOOR_MICROLAMPORTS = 1000n;
221
+ var DEFAULT_PERCENTILE = 75;
222
+ var DEFAULT_CACHE_TTL_MS = 1e4;
223
+ var cache2 = /* @__PURE__ */ new Map();
224
+ async function estimatePriorityFeeMicroLamports(rpc, options) {
225
+ const percentile = clampPercentile(options?.percentile ?? DEFAULT_PERCENTILE);
226
+ const ttl = options?.ttlMs ?? DEFAULT_CACHE_TTL_MS;
227
+ const accounts = options?.accounts ?? [];
228
+ const key = cacheKey(percentile, accounts);
229
+ const now = Date.now();
230
+ const cached = cache2.get(key);
231
+ if (cached && now < cached.expires) {
232
+ return cached.microLamports;
233
+ }
234
+ const samples = await rpc.getRecentPrioritizationFees(accounts).send();
235
+ const fee = pickPercentileFee(samples, percentile);
236
+ cache2.set(key, { microLamports: fee, expires: now + ttl });
237
+ return fee;
238
+ }
239
+ function clearPriorityFeeCache() {
240
+ cache2.clear();
241
+ }
242
+ function pickPercentileFee(samples, percentile) {
243
+ if (samples.length === 0) {
244
+ return PRIORITY_FEE_FLOOR_MICROLAMPORTS;
245
+ }
246
+ const sorted = samples.map((sample) => BigInt(sample.prioritizationFee)).sort(compareBigInt);
247
+ const clamped = clampPercentile(percentile);
248
+ const indexFloat = clamped / 100 * (sorted.length - 1) | 0;
249
+ const value = sorted[indexFloat];
250
+ return value > PRIORITY_FEE_FLOOR_MICROLAMPORTS ? value : PRIORITY_FEE_FLOOR_MICROLAMPORTS;
251
+ }
252
+ function clampPercentile(value) {
253
+ if (!Number.isFinite(value)) {
254
+ return DEFAULT_PERCENTILE;
255
+ }
256
+ if (value < 0) {
257
+ return 0;
258
+ }
259
+ if (value > 100) {
260
+ return 100;
261
+ }
262
+ return value;
263
+ }
264
+ function compareBigInt(left, right) {
265
+ if (left < right) {
266
+ return -1;
267
+ }
268
+ if (left > right) {
269
+ return 1;
270
+ }
271
+ return 0;
272
+ }
273
+ function cacheKey(percentile, accounts) {
274
+ if (accounts.length === 0) {
275
+ return `p:${percentile}`;
276
+ }
277
+ return `p:${percentile}:${[...accounts].sort().join(",")}`;
278
+ }
279
+ var MAX_DESCRIPTION_LENGTH = LIMITS.MAX_DESCRIPTION_LENGTH;
280
+ var MAX_SAFE_LAMPORTS = Number.MAX_SAFE_INTEGER;
281
+ var MAX_EXPIRY_SECS_SCHEMA = 86400;
282
+ var BASE58_RE = /^[1-9A-HJ-NP-Za-km-z]+$/;
283
+ var SOLANA_ADDRESS_LENGTH_RE = /^.{32,44}$/;
284
+ var lamportsSchema = zod.z.number().int().positive().max(MAX_SAFE_LAMPORTS, `amount must be <= ${MAX_SAFE_LAMPORTS}`);
285
+ var feeAmountSchema = zod.z.number().int().nonnegative().max(MAX_SAFE_LAMPORTS, `fee_amount must be <= ${MAX_SAFE_LAMPORTS}`);
286
+ var solanaAddressSchema = zod.z.string().regex(BASE58_RE, "must be base58").regex(SOLANA_ADDRESS_LENGTH_RE, "must be 32-44 base58 chars");
287
+ var PaymentRequestSchema = zod.z.object({
288
+ recipient: solanaAddressSchema,
289
+ amount: lamportsSchema,
290
+ reference: solanaAddressSchema,
291
+ description: zod.z.string().max(MAX_DESCRIPTION_LENGTH).optional(),
292
+ fee_address: solanaAddressSchema.optional(),
293
+ fee_amount: feeAmountSchema.optional(),
294
+ created_at: zod.z.number().int().positive(),
295
+ expiry_secs: zod.z.number().int().positive().max(MAX_EXPIRY_SECS_SCHEMA, `expiry_secs must be <= ${MAX_EXPIRY_SECS_SCHEMA}`)
296
+ });
297
+ function parsePaymentRequest(input, options) {
298
+ let parsed;
299
+ try {
300
+ parsed = JSON.parse(input);
301
+ } catch (e) {
302
+ return {
303
+ ok: false,
304
+ error: { code: "invalid_json", message: `Invalid payment request JSON: ${e}` }
305
+ };
306
+ }
307
+ const result = PaymentRequestSchema.safeParse(parsed);
308
+ if (!result.success) {
309
+ return {
310
+ ok: false,
311
+ error: { code: "schema", message: result.error.message }
312
+ };
313
+ }
314
+ if (options?.maxAmountLamports !== void 0) {
315
+ if (BigInt(result.data.amount) > options.maxAmountLamports) {
316
+ return {
317
+ ok: false,
318
+ error: {
319
+ code: "amount_exceeds_max",
320
+ message: `Payment amount ${result.data.amount} lamports exceeds approved max ${options.maxAmountLamports}.`
321
+ }
322
+ };
323
+ }
324
+ }
325
+ return { ok: true, data: result.data };
326
+ }
327
+
218
328
  // src/payment/solana.ts
329
+ var DEFAULT_COMPUTE_UNIT_LIMIT = 2e5;
330
+ var DEFAULT_PRIORITY_FEE_PERCENTILE = 75;
219
331
  var REFERENCE_BYTE_LENGTH = 32;
220
332
  function isValidSolanaAddress(value) {
221
333
  return kit.isAddress(value);
@@ -272,32 +384,27 @@ var SolanaPaymentStrategy = class {
272
384
  expiry_secs: expirySecs
273
385
  };
274
386
  }
275
- validatePaymentRequest(requestJson, config, expectedRecipient) {
387
+ validatePaymentRequest(requestJson, config, expectedRecipient, options) {
276
388
  assertConfig(config);
277
- let data;
278
- try {
279
- data = JSON.parse(requestJson);
280
- } catch (e) {
281
- return { code: "invalid_json", message: `Invalid payment request JSON: ${e}` };
282
- }
283
- if (typeof data.amount !== "number" || !Number.isInteger(data.amount) || data.amount <= 0) {
284
- return {
285
- code: "invalid_amount",
286
- message: `Invalid amount in payment request: ${data.amount}`
287
- };
288
- }
289
- if (typeof data.recipient !== "string" || !data.recipient) {
290
- return { code: "missing_recipient", message: "Missing recipient in payment request" };
389
+ const parsed = parsePaymentRequest(requestJson, {
390
+ maxAmountLamports: options?.maxAmountLamports
391
+ });
392
+ if (!parsed.ok) {
393
+ if (parsed.error.code === "invalid_json") {
394
+ return { code: "invalid_json", message: parsed.error.message };
395
+ }
396
+ if (parsed.error.code === "amount_exceeds_max") {
397
+ return { code: "invalid_amount", message: parsed.error.message };
398
+ }
399
+ return { code: "invalid_amount", message: parsed.error.message };
291
400
  }
401
+ const data = parsed.data;
292
402
  if (!isValidSolanaAddress(data.recipient)) {
293
403
  return {
294
404
  code: "invalid_recipient_address",
295
405
  message: `Invalid Solana address for recipient: ${data.recipient}`
296
406
  };
297
407
  }
298
- if (typeof data.reference !== "string" || !data.reference) {
299
- return { code: "missing_reference", message: "Missing reference in payment request" };
300
- }
301
408
  if (!isValidSolanaAddress(data.reference)) {
302
409
  return {
303
410
  code: "invalid_reference_address",
@@ -357,7 +464,7 @@ var SolanaPaymentStrategy = class {
357
464
  * read-only, non-signer account so providers can detect the payment via
358
465
  * `getSignaturesForAddress(reference)`.
359
466
  */
360
- async buildTransaction(paymentRequest, payerSigner, rpc, config) {
467
+ async buildTransaction(paymentRequest, payerSigner, rpc, config, options) {
361
468
  assertConfig(config);
362
469
  assertLamports(paymentRequest.amount, "payment amount");
363
470
  if (paymentRequest.amount === 0) {
@@ -376,14 +483,23 @@ var SolanaPaymentStrategy = class {
376
483
  `Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
377
484
  );
378
485
  }
379
- const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
486
+ const computeUnitLimit = options?.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT;
487
+ if (!Number.isInteger(computeUnitLimit) || computeUnitLimit <= 0) {
488
+ throw new Error(`Invalid computeUnitLimit: ${computeUnitLimit}. Must be a positive integer.`);
489
+ }
490
+ const paymentInstructions = buildPaymentInstructions(paymentRequest, payerSigner);
491
+ const priorityFeeMicroLamports = options?.priorityFeeMicroLamports ?? await estimatePriorityFeeMicroLamports(rpc, {
492
+ percentile: options?.priorityFeePercentile ?? DEFAULT_PRIORITY_FEE_PERCENTILE
493
+ });
380
494
  const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
381
495
  const message = kit.pipe(
382
496
  kit.createTransactionMessage({ version: 0 }),
383
497
  (m) => kit.setTransactionMessageFeePayerSigner(payerSigner, m),
384
498
  (m) => kit.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
499
+ (m) => kit.setTransactionMessageComputeUnitLimit(computeUnitLimit, m),
500
+ (m) => kit.setTransactionMessageComputeUnitPrice(priorityFeeMicroLamports, m),
385
501
  (m) => kit.appendTransactionMessageInstructions(
386
- instructions,
502
+ paymentInstructions,
387
503
  m
388
504
  )
389
505
  );
@@ -2296,12 +2412,149 @@ function validateAgentName(name) {
2296
2412
  }
2297
2413
  }
2298
2414
 
2415
+ // src/primitives/rateLimiter.ts
2416
+ function createSlidingWindowLimiter(options) {
2417
+ const { windowMs, maxPerWindow, maxKeys } = options;
2418
+ if (windowMs <= 0) {
2419
+ throw new RangeError("windowMs must be > 0");
2420
+ }
2421
+ if (maxPerWindow <= 0) {
2422
+ throw new RangeError("maxPerWindow must be > 0");
2423
+ }
2424
+ if (maxKeys <= 0) {
2425
+ throw new RangeError("maxKeys must be > 0");
2426
+ }
2427
+ const entries = /* @__PURE__ */ new Map();
2428
+ function evictIfNeeded() {
2429
+ while (entries.size > maxKeys) {
2430
+ const oldestKey = entries.keys().next().value;
2431
+ if (oldestKey === void 0) {
2432
+ return;
2433
+ }
2434
+ entries.delete(oldestKey);
2435
+ }
2436
+ }
2437
+ return {
2438
+ peek(key, now = Date.now()) {
2439
+ const entry = entries.get(key);
2440
+ if (!entry) {
2441
+ return { allowed: true, resetAt: now + windowMs, count: 0 };
2442
+ }
2443
+ const cutoff = now - windowMs;
2444
+ const fresh = entry.hits.filter((timestamp) => timestamp > cutoff);
2445
+ return {
2446
+ allowed: fresh.length < maxPerWindow,
2447
+ resetAt: (fresh[0] ?? now) + windowMs,
2448
+ count: fresh.length
2449
+ };
2450
+ },
2451
+ check(key, now = Date.now()) {
2452
+ const entry = entries.get(key) ?? { hits: [] };
2453
+ const cutoff = now - windowMs;
2454
+ const fresh = entry.hits.filter((timestamp) => timestamp > cutoff);
2455
+ if (fresh.length >= maxPerWindow) {
2456
+ entries.delete(key);
2457
+ entries.set(key, { hits: fresh });
2458
+ return {
2459
+ allowed: false,
2460
+ resetAt: (fresh[0] ?? now) + windowMs,
2461
+ count: fresh.length
2462
+ };
2463
+ }
2464
+ fresh.push(now);
2465
+ entries.delete(key);
2466
+ entries.set(key, { hits: fresh });
2467
+ evictIfNeeded();
2468
+ return {
2469
+ allowed: true,
2470
+ resetAt: (fresh[0] ?? now) + windowMs,
2471
+ count: fresh.length
2472
+ };
2473
+ },
2474
+ prune(now = Date.now()) {
2475
+ const cutoff = now - windowMs;
2476
+ for (const [key, entry] of entries) {
2477
+ const fresh = entry.hits.filter((timestamp) => timestamp > cutoff);
2478
+ if (fresh.length === 0) {
2479
+ entries.delete(key);
2480
+ } else if (fresh.length !== entry.hits.length) {
2481
+ entry.hits = fresh;
2482
+ }
2483
+ }
2484
+ },
2485
+ size() {
2486
+ return entries.size;
2487
+ },
2488
+ reset() {
2489
+ entries.clear();
2490
+ }
2491
+ };
2492
+ }
2493
+
2494
+ // src/primitives/logRedact.ts
2495
+ var SECRET_REDACT_PATHS = [
2496
+ "*.ELISYM_NOSTR_PRIVATE_KEY",
2497
+ "*.ELISYM_SOLANA_PRIVATE_KEY",
2498
+ "*.nostrPrivateKeyHex",
2499
+ "*.solanaPrivateKeyBase58",
2500
+ "*.secretKey",
2501
+ "*.secret",
2502
+ "ELISYM_NOSTR_PRIVATE_KEY",
2503
+ "ELISYM_SOLANA_PRIVATE_KEY",
2504
+ // Canonical on-disk `.secrets.json` field names. Logging the whole
2505
+ // `secrets` object, or any single field directly, must not leak.
2506
+ "llm_api_key",
2507
+ "nostr_secret_key",
2508
+ "solana_secret_key",
2509
+ "*.llm_api_key",
2510
+ "*.nostr_secret_key",
2511
+ "*.solana_secret_key",
2512
+ "secrets",
2513
+ "*.secrets"
2514
+ ];
2515
+ var INPUT_REDACT_PATHS = [
2516
+ "content",
2517
+ "input",
2518
+ "prompt",
2519
+ "*.content",
2520
+ "*.input",
2521
+ "*.prompt",
2522
+ "event.content",
2523
+ "*.event.content",
2524
+ // JobLedger entries carry the raw Nostr event JSON (which embeds
2525
+ // `event.content`) and the full LLM `resultContent` - both are
2526
+ // customer-confidential and must never land in a structured log.
2527
+ "rawEventJson",
2528
+ "resultContent",
2529
+ "*.rawEventJson",
2530
+ "*.resultContent"
2531
+ ];
2532
+ var DEFAULT_REDACT_PATHS = [...SECRET_REDACT_PATHS, ...INPUT_REDACT_PATHS];
2533
+ var INPUT_REDACT_LEAVES = /* @__PURE__ */ new Set([
2534
+ "content",
2535
+ "input",
2536
+ "prompt",
2537
+ "rawEventJson",
2538
+ "resultContent"
2539
+ ]);
2540
+ function makeCensor() {
2541
+ return (_value, path) => {
2542
+ const last = path[path.length - 1];
2543
+ if (last !== void 0 && INPUT_REDACT_LEAVES.has(last)) {
2544
+ return "[INPUT REDACTED]";
2545
+ }
2546
+ return "[REDACTED]";
2547
+ };
2548
+ }
2549
+
2299
2550
  exports.BoundedSet = BoundedSet;
2300
2551
  exports.DEFAULTS = DEFAULTS;
2301
2552
  exports.DEFAULT_KIND_OFFSET = DEFAULT_KIND_OFFSET;
2553
+ exports.DEFAULT_REDACT_PATHS = DEFAULT_REDACT_PATHS;
2302
2554
  exports.DiscoveryService = DiscoveryService;
2303
2555
  exports.ElisymClient = ElisymClient;
2304
2556
  exports.ElisymIdentity = ElisymIdentity;
2557
+ exports.INPUT_REDACT_PATHS = INPUT_REDACT_PATHS;
2305
2558
  exports.KIND_APP_HANDLER = KIND_APP_HANDLER;
2306
2559
  exports.KIND_JOB_FEEDBACK = KIND_JOB_FEEDBACK;
2307
2560
  exports.KIND_JOB_REQUEST = KIND_JOB_REQUEST;
@@ -2318,22 +2571,30 @@ exports.NostrPool = NostrPool;
2318
2571
  exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
2319
2572
  exports.PROTOCOL_PROGRAM_ID_DEVNET = PROTOCOL_PROGRAM_ID_DEVNET;
2320
2573
  exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
2574
+ exports.PaymentRequestSchema = PaymentRequestSchema;
2321
2575
  exports.PingService = PingService;
2322
2576
  exports.RELAYS = RELAYS;
2577
+ exports.SECRET_REDACT_PATHS = SECRET_REDACT_PATHS;
2323
2578
  exports.SolanaPaymentStrategy = SolanaPaymentStrategy;
2324
2579
  exports.assertExpiry = assertExpiry;
2325
2580
  exports.assertLamports = assertLamports;
2326
2581
  exports.buildPaymentInstructions = buildPaymentInstructions;
2327
2582
  exports.calculateProtocolFee = calculateProtocolFee;
2583
+ exports.clearPriorityFeeCache = clearPriorityFeeCache;
2328
2584
  exports.clearProtocolConfigCache = clearProtocolConfigCache;
2329
2585
  exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
2586
+ exports.createSlidingWindowLimiter = createSlidingWindowLimiter;
2587
+ exports.estimatePriorityFeeMicroLamports = estimatePriorityFeeMicroLamports;
2330
2588
  exports.formatSol = formatSol;
2331
2589
  exports.getProtocolConfig = getProtocolConfig;
2332
2590
  exports.getProtocolProgramId = getProtocolProgramId;
2333
2591
  exports.jobRequestKind = jobRequestKind;
2334
2592
  exports.jobResultKind = jobResultKind;
2593
+ exports.makeCensor = makeCensor;
2335
2594
  exports.nip44Decrypt = nip44Decrypt;
2336
2595
  exports.nip44Encrypt = nip44Encrypt;
2596
+ exports.parsePaymentRequest = parsePaymentRequest;
2597
+ exports.pickPercentileFee = pickPercentileFee;
2337
2598
  exports.timeAgo = timeAgo;
2338
2599
  exports.toDTag = toDTag;
2339
2600
  exports.truncateKey = truncateKey;