@elisym/sdk 0.3.2 → 0.4.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.js CHANGED
@@ -1,4 +1,5 @@
1
- import { PublicKey, Keypair, SystemProgram, Transaction } from '@solana/web3.js';
1
+ import { getTransferSolInstruction } from '@solana-program/system';
2
+ import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, getProgramDerivedAddress, assertAccountExists, isAddress, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
2
3
  import Decimal2 from 'decimal.js-light';
3
4
  import { verifyEvent, finalizeEvent, getPublicKey, nip19, generateSecretKey, SimplePool } from 'nostr-tools';
4
5
  import * as nip44 from 'nostr-tools/nip44';
@@ -35,6 +36,16 @@ var KIND_PONG = 20201;
35
36
  var LAMPORTS_PER_SOL = 1e9;
36
37
  var PROTOCOL_FEE_BPS = 300;
37
38
  var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
39
+ var PROTOCOL_PROGRAM_ID_DEVNET = "BrX1CRkSgvcjxBvc2bgc3QqgWjinusofDmeP7ZVxvwrE";
40
+ function getProtocolProgramId(cluster) {
41
+ switch (cluster) {
42
+ case "devnet":
43
+ case "localnet":
44
+ return PROTOCOL_PROGRAM_ID_DEVNET;
45
+ case "mainnet":
46
+ throw new Error("Protocol program is not deployed on mainnet yet");
47
+ }
48
+ }
38
49
  var DEFAULTS = {
39
50
  SUBSCRIPTION_TIMEOUT_MS: 12e4,
40
51
  PING_TIMEOUT_MS: 15e3,
@@ -61,19 +72,100 @@ var LIMITS = {
61
72
  MAX_AGENT_NAME_LENGTH: 64,
62
73
  MAX_CAPABILITY_LENGTH: 64
63
74
  };
75
+ function getConfigDecoder() {
76
+ return getStructDecoder([
77
+ ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
78
+ ["version", getU8Decoder()],
79
+ ["bump", getU8Decoder()],
80
+ ["admin", getAddressDecoder()],
81
+ ["pendingAdmin", getOptionDecoder(getAddressDecoder())],
82
+ ["treasury", getAddressDecoder()],
83
+ ["feeBps", getU16Decoder()],
84
+ ["paused", getBooleanDecoder()],
85
+ ["lastUpdated", getI64Decoder()],
86
+ ["reserved", fixDecoderSize(getBytesDecoder(), 128)]
87
+ ]);
88
+ }
89
+ function decodeConfig(encodedAccount) {
90
+ return decodeAccount(
91
+ encodedAccount,
92
+ getConfigDecoder()
93
+ );
94
+ }
95
+ async function fetchConfig(rpc, address2, config) {
96
+ const maybeAccount = await fetchMaybeConfig(rpc, address2, config);
97
+ assertAccountExists(maybeAccount);
98
+ return maybeAccount;
99
+ }
100
+ async function fetchMaybeConfig(rpc, address2, config) {
101
+ const maybeAccount = await fetchEncodedAccount(rpc, address2, config);
102
+ return decodeConfig(maybeAccount);
103
+ }
104
+ if (process.env.NODE_ENV !== "production") ;
105
+ var CONFIG_SEED = "config";
106
+ async function deriveConfigAddress(programId) {
107
+ const [pda] = await getProgramDerivedAddress({
108
+ programAddress: programId,
109
+ seeds: [new TextEncoder().encode(CONFIG_SEED)]
110
+ });
111
+ return pda;
112
+ }
113
+
114
+ // src/config/onchain.ts
115
+ var CACHE_TTL_MS = 6e4;
116
+ var cache = /* @__PURE__ */ new Map();
117
+ function clearProtocolConfigCache() {
118
+ cache.clear();
119
+ }
120
+ async function getProtocolConfig(rpc, programId, options) {
121
+ const key = programId.toString();
122
+ const ttl = options?.ttlMs ?? CACHE_TTL_MS;
123
+ const cached = cache.get(key);
124
+ if (!options?.forceRefresh && cached && Date.now() < cached.expires) {
125
+ return { ...cached.config, source: "cache" };
126
+ }
127
+ try {
128
+ const configPda = await deriveConfigAddress(programId);
129
+ const account = await fetchConfig(rpc, configPda);
130
+ const data = account.data;
131
+ const config = {
132
+ programId,
133
+ feeBps: data.feeBps,
134
+ treasury: data.treasury,
135
+ admin: data.admin,
136
+ pendingAdmin: data.pendingAdmin.__option === "Some" ? data.pendingAdmin.value : null,
137
+ paused: data.paused,
138
+ version: data.version,
139
+ source: "onchain"
140
+ };
141
+ cache.set(key, { config, expires: Date.now() + ttl });
142
+ return config;
143
+ } catch (error) {
144
+ if (cached) {
145
+ return { ...cached.config, source: "cache" };
146
+ }
147
+ throw new Error(
148
+ `Failed to fetch protocol config from on-chain program ${programId} and no cached value exists. Ensure RPC is reachable and the program is initialized. Cause: ${error instanceof Error ? error.message : String(error)}`
149
+ );
150
+ }
151
+ }
152
+ var BPS_DENOMINATOR = 1e4;
64
153
  function assertLamports(value, field) {
65
154
  if (!Number.isInteger(value) || value < 0) {
66
155
  throw new Error(`Invalid ${field}: ${value}. Must be a non-negative integer.`);
67
156
  }
68
157
  }
69
- function calculateProtocolFee(amount) {
158
+ function calculateProtocolFee(amount, feeBps) {
159
+ if (!Number.isInteger(feeBps) || feeBps < 0) {
160
+ throw new Error(`Invalid feeBps: ${feeBps}. Must be a non-negative integer.`);
161
+ }
70
162
  if (!Number.isInteger(amount) || amount < 0) {
71
163
  throw new Error(`Invalid fee amount: ${amount}. Must be a non-negative integer.`);
72
164
  }
73
- if (amount === 0) {
165
+ if (amount === 0 || feeBps === 0) {
74
166
  return 0;
75
167
  }
76
- return new Decimal2(amount).mul(PROTOCOL_FEE_BPS).div(1e4).toDecimalPlaces(0, Decimal2.ROUND_CEIL).toNumber();
168
+ return new Decimal2(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal2.ROUND_CEIL).toNumber();
77
169
  }
78
170
  function validateExpiry(createdAt, expirySecs) {
79
171
  if (!Number.isInteger(createdAt) || createdAt <= 0) {
@@ -99,47 +191,64 @@ function assertExpiry(createdAt, expirySecs) {
99
191
  }
100
192
 
101
193
  // src/payment/solana.ts
102
- function isValidSolanaAddress(address) {
103
- try {
104
- void new PublicKey(address);
105
- return true;
106
- } catch {
107
- return false;
194
+ var REFERENCE_BYTE_LENGTH = 32;
195
+ function isValidSolanaAddress(value) {
196
+ return isAddress(value);
197
+ }
198
+ function generateReference() {
199
+ const bytes = new Uint8Array(REFERENCE_BYTE_LENGTH);
200
+ globalThis.crypto.getRandomValues(bytes);
201
+ return getAddressDecoder().decode(bytes);
202
+ }
203
+ function assertReference(reference) {
204
+ if (!isValidSolanaAddress(reference)) {
205
+ throw new Error(`Invalid reference address: ${reference}`);
206
+ }
207
+ }
208
+ function assertExpirySecs(expirySecs) {
209
+ if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
210
+ throw new Error(`Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`);
211
+ }
212
+ }
213
+ function assertConfig(config) {
214
+ if (!Number.isInteger(config.feeBps) || config.feeBps < 0) {
215
+ throw new Error(`Invalid feeBps: ${config.feeBps}. Must be a non-negative integer.`);
216
+ }
217
+ if (typeof config.treasury !== "string" || !isValidSolanaAddress(config.treasury)) {
218
+ throw new Error(`Invalid treasury address: ${String(config.treasury)}`);
108
219
  }
109
220
  }
110
221
  var SolanaPaymentStrategy = class {
111
222
  chain = "solana";
112
- calculateFee(amount) {
113
- return calculateProtocolFee(amount);
223
+ calculateFee(amount, config) {
224
+ assertConfig(config);
225
+ return calculateProtocolFee(amount, config.feeBps);
114
226
  }
115
- createPaymentRequest(recipientAddress, amount, expirySecs = DEFAULTS.PAYMENT_EXPIRY_SECS) {
116
- try {
117
- void new PublicKey(recipientAddress);
118
- } catch {
227
+ createPaymentRequest(recipientAddress, amount, config, options) {
228
+ assertConfig(config);
229
+ if (!isValidSolanaAddress(recipientAddress)) {
119
230
  throw new Error(`Invalid Solana address: ${recipientAddress}`);
120
231
  }
121
232
  assertLamports(amount, "payment amount");
122
233
  if (amount === 0) {
123
234
  throw new Error("Invalid payment amount: 0. Must be positive.");
124
235
  }
125
- if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
126
- throw new Error(
127
- `Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`
128
- );
129
- }
130
- const feeAmount = calculateProtocolFee(amount);
131
- const reference = Keypair.generate().publicKey.toBase58();
236
+ const expirySecs = options?.expirySecs ?? DEFAULTS.PAYMENT_EXPIRY_SECS;
237
+ assertExpirySecs(expirySecs);
238
+ const feeAmount = calculateProtocolFee(amount, config.feeBps);
239
+ const reference = generateReference();
132
240
  return {
133
241
  recipient: recipientAddress,
134
242
  amount,
135
243
  reference,
136
- fee_address: PROTOCOL_TREASURY,
244
+ fee_address: config.treasury,
137
245
  fee_amount: feeAmount,
138
246
  created_at: Math.floor(Date.now() / 1e3),
139
247
  expiry_secs: expirySecs
140
248
  };
141
249
  }
142
- validatePaymentRequest(requestJson, expectedRecipient) {
250
+ validatePaymentRequest(requestJson, config, expectedRecipient) {
251
+ assertConfig(config);
143
252
  let data;
144
253
  try {
145
254
  data = JSON.parse(requestJson);
@@ -181,21 +290,22 @@ var SolanaPaymentStrategy = class {
181
290
  const code = expiryError.includes("future") ? "future_timestamp" : "expired";
182
291
  return { code, message: expiryError };
183
292
  }
184
- const expectedFee = calculateProtocolFee(data.amount);
293
+ const expectedFee = calculateProtocolFee(data.amount, config.feeBps);
294
+ const treasury = config.treasury;
185
295
  const { fee_address, fee_amount } = data;
186
296
  const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
187
297
  const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
188
298
  if (hasFeeAddress && hasFeeAmount) {
189
- if (fee_address !== PROTOCOL_TREASURY) {
299
+ if (fee_address !== treasury) {
190
300
  return {
191
301
  code: "fee_address_mismatch",
192
- message: `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`
302
+ message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
193
303
  };
194
304
  }
195
305
  if (fee_amount !== expectedFee) {
196
306
  return {
197
307
  code: "fee_amount_mismatch",
198
- message: `Fee amount mismatch: expected ${expectedFee} lamports (${PROTOCOL_FEE_BPS}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
308
+ message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
199
309
  };
200
310
  }
201
311
  return null;
@@ -203,20 +313,24 @@ var SolanaPaymentStrategy = class {
203
313
  if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
204
314
  return {
205
315
  code: "missing_fee",
206
- message: `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
316
+ message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
207
317
  };
208
318
  }
209
319
  return {
210
320
  code: "invalid_fee_params",
211
- message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
321
+ message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
212
322
  };
213
323
  }
214
324
  /**
215
- * Build an unsigned transaction from a payment request.
216
- * The caller must set `recentBlockhash` and `feePayer` on the
217
- * returned Transaction before signing and sending.
325
+ * Build, sign, and return a transaction for the supplied payment request.
326
+ * The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
327
+ *
328
+ * The provider transfer instruction includes the payment reference as a
329
+ * read-only, non-signer account so providers can detect the payment via
330
+ * `getSignaturesForAddress(reference)`.
218
331
  */
219
- async buildTransaction(payerAddress, paymentRequest) {
332
+ async buildTransaction(paymentRequest, payerSigner, rpc, config) {
333
+ assertConfig(config);
220
334
  assertLamports(paymentRequest.amount, "payment amount");
221
335
  if (paymentRequest.amount === 0) {
222
336
  throw new Error("Invalid payment amount: 0. Must be positive.");
@@ -226,50 +340,32 @@ var SolanaPaymentStrategy = class {
226
340
  `Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
227
341
  );
228
342
  }
343
+ assertReference(paymentRequest.reference);
229
344
  assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
230
- if (paymentRequest.fee_address && paymentRequest.fee_address !== PROTOCOL_TREASURY) {
231
- throw new Error(
232
- `Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
233
- );
234
- }
235
- const payerPubkey = new PublicKey(payerAddress);
236
- const recipient = new PublicKey(paymentRequest.recipient);
237
- const reference = new PublicKey(paymentRequest.reference);
238
- const feeAddress = paymentRequest.fee_address ? new PublicKey(paymentRequest.fee_address) : null;
239
- const feeAmount = paymentRequest.fee_amount ?? 0;
240
- const providerAmount = feeAddress && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
241
- if (providerAmount <= 0) {
345
+ const treasury = config.treasury;
346
+ if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
242
347
  throw new Error(
243
- `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
348
+ `Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
244
349
  );
245
350
  }
246
- const transferIx = SystemProgram.transfer({
247
- fromPubkey: payerPubkey,
248
- toPubkey: recipient,
249
- lamports: providerAmount
250
- });
251
- transferIx.keys.push({
252
- pubkey: reference,
253
- isSigner: false,
254
- isWritable: false
255
- });
256
- const tx = new Transaction().add(transferIx);
257
- if (feeAddress && feeAmount > 0) {
258
- tx.add(
259
- SystemProgram.transfer({
260
- fromPubkey: payerPubkey,
261
- toPubkey: feeAddress,
262
- lamports: feeAmount
263
- })
264
- );
265
- }
266
- return tx;
351
+ const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
352
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
353
+ const message = pipe(
354
+ createTransactionMessage({ version: 0 }),
355
+ (m) => setTransactionMessageFeePayerSigner(payerSigner, m),
356
+ (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
357
+ (m) => appendTransactionMessageInstructions(
358
+ instructions,
359
+ m
360
+ )
361
+ );
362
+ return signTransactionMessageWithSigners(message);
267
363
  }
268
- async verifyPayment(connection, paymentRequest, options) {
269
- if (!connection || typeof connection.getTransaction !== "function") {
270
- return { verified: false, error: "Invalid connection: expected Solana Connection instance" };
364
+ async verifyPayment(rpc, paymentRequest, config, options) {
365
+ assertConfig(config);
366
+ if (!rpc || typeof rpc.getTransaction !== "function") {
367
+ return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
271
368
  }
272
- const conn = connection;
273
369
  if (!paymentRequest.reference || !paymentRequest.recipient) {
274
370
  return { verified: false, error: "Missing required fields in payment request" };
275
371
  }
@@ -285,19 +381,20 @@ var SolanaPaymentStrategy = class {
285
381
  error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
286
382
  };
287
383
  }
288
- const expectedFee = calculateProtocolFee(paymentRequest.amount);
384
+ const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
289
385
  const feeAmount = paymentRequest.fee_amount ?? 0;
386
+ const treasury = config.treasury;
290
387
  if (expectedFee > 0) {
291
388
  if (feeAmount < expectedFee) {
292
389
  return {
293
390
  verified: false,
294
- error: `Protocol fee ${feeAmount} below required ${expectedFee} (${PROTOCOL_FEE_BPS}bps of ${paymentRequest.amount})`
391
+ error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
295
392
  };
296
393
  }
297
394
  if (!paymentRequest.fee_address) {
298
395
  return { verified: false, error: "Missing fee address in payment request" };
299
396
  }
300
- if (paymentRequest.fee_address !== PROTOCOL_TREASURY) {
397
+ if (paymentRequest.fee_address !== treasury) {
301
398
  return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
302
399
  }
303
400
  }
@@ -310,10 +407,11 @@ var SolanaPaymentStrategy = class {
310
407
  }
311
408
  if (options?.txSignature) {
312
409
  return this._verifyBySignature(
313
- conn,
410
+ rpc,
314
411
  options.txSignature,
315
412
  paymentRequest.reference,
316
413
  paymentRequest.recipient,
414
+ treasury,
317
415
  expectedNet,
318
416
  feeAmount,
319
417
  options?.retries ?? DEFAULTS.VERIFY_RETRIES,
@@ -321,26 +419,28 @@ var SolanaPaymentStrategy = class {
321
419
  );
322
420
  }
323
421
  return this._verifyByReference(
324
- conn,
422
+ rpc,
325
423
  paymentRequest.reference,
326
424
  paymentRequest.recipient,
425
+ treasury,
327
426
  expectedNet,
328
427
  feeAmount,
329
428
  options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
330
429
  options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
331
430
  );
332
431
  }
333
- async _verifyBySignature(connection, txSignature, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
432
+ async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
334
433
  let lastError;
335
434
  for (let attempt = 0; attempt < retries; attempt++) {
336
435
  try {
337
- const tx = await connection.getTransaction(txSignature, {
338
- maxSupportedTransactionVersion: 0,
339
- commitment: "confirmed"
340
- });
436
+ const tx = await rpc.getTransaction(txSignature, {
437
+ commitment: "confirmed",
438
+ encoding: "json",
439
+ maxSupportedTransactionVersion: 0
440
+ }).send();
341
441
  if (!tx?.meta || tx.meta.err) {
342
442
  if (attempt < retries - 1) {
343
- await new Promise((r) => setTimeout(r, intervalMs));
443
+ await waitMs(intervalMs);
344
444
  continue;
345
445
  }
346
446
  return {
@@ -348,50 +448,24 @@ var SolanaPaymentStrategy = class {
348
448
  error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
349
449
  };
350
450
  }
351
- const accountKeys = tx.transaction.message.getAccountKeys();
352
- const balanceCount = tx.meta.preBalances.length;
353
- const keyToIdx = /* @__PURE__ */ new Map();
354
- for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
355
- const key = accountKeys.get(i);
356
- if (key) {
357
- keyToIdx.set(key.toBase58(), i);
358
- }
359
- }
360
- if (!keyToIdx.has(referenceKey)) {
361
- return {
362
- verified: false,
363
- error: "Reference key not found in transaction - possible replay"
364
- };
365
- }
366
- const recipientIdx = keyToIdx.get(recipientAddress);
367
- if (recipientIdx === void 0) {
368
- return { verified: false, error: "Recipient not found in transaction" };
369
- }
370
- const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
371
- if (recipientDelta < expectedNet) {
372
- return {
373
- verified: false,
374
- error: `Recipient received ${recipientDelta}, expected >= ${expectedNet}`
375
- };
376
- }
377
- if (expectedFee > 0) {
378
- const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
379
- if (treasuryIdx === void 0) {
380
- return { verified: false, error: "Treasury not found in transaction" };
381
- }
382
- const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
383
- if (treasuryDelta < expectedFee) {
384
- return {
385
- verified: false,
386
- error: `Treasury received ${treasuryDelta}, expected >= ${expectedFee}`
387
- };
388
- }
451
+ const verdict = checkBalanceDiff({
452
+ accountKeys: tx.transaction.message.accountKeys,
453
+ preBalances: tx.meta.preBalances,
454
+ postBalances: tx.meta.postBalances,
455
+ referenceKey,
456
+ recipientAddress,
457
+ treasuryAddress,
458
+ expectedNet,
459
+ expectedFee
460
+ });
461
+ if (verdict.ok) {
462
+ return { verified: true, txSignature };
389
463
  }
390
- return { verified: true, txSignature };
464
+ return { verified: false, error: verdict.reason };
391
465
  } catch (err) {
392
466
  lastError = err;
393
467
  if (attempt < retries - 1) {
394
- await new Promise((r) => setTimeout(r, intervalMs));
468
+ await waitMs(intervalMs);
395
469
  }
396
470
  }
397
471
  }
@@ -400,66 +474,50 @@ var SolanaPaymentStrategy = class {
400
474
  error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
401
475
  };
402
476
  }
403
- async _verifyByReference(connection, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
404
- const reference = new PublicKey(referenceKey);
477
+ async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
405
478
  let lastError;
479
+ const reference = address(referenceKey);
406
480
  for (let attempt = 0; attempt < retries; attempt++) {
407
481
  try {
408
- const signatures = await connection.getSignaturesForAddress(reference, {
482
+ const signatures = await rpc.getSignaturesForAddress(reference, {
409
483
  limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
410
- });
411
- const validSigs = signatures.filter((s) => !s.err);
484
+ }).send();
485
+ const validSigs = signatures.filter((entry) => !entry.err);
412
486
  if (validSigs.length > 0) {
487
+ const fetchTransaction = (sig) => rpc.getTransaction(sig, {
488
+ commitment: "confirmed",
489
+ encoding: "json",
490
+ maxSupportedTransactionVersion: 0
491
+ }).send();
413
492
  const txResults = await Promise.all(
414
493
  validSigs.map(
415
- (s) => connection.getTransaction(s.signature, {
416
- maxSupportedTransactionVersion: 0,
417
- commitment: "confirmed"
418
- }).then((tx) => ({ sig: s.signature, tx })).catch(() => ({
419
- sig: s.signature,
420
- tx: null
421
- }))
494
+ (entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
422
495
  )
423
496
  );
424
497
  for (const { sig, tx } of txResults) {
425
498
  if (!tx?.meta || tx.meta.err) {
426
499
  continue;
427
500
  }
428
- const accountKeys = tx.transaction.message.getAccountKeys();
429
- const balanceCount = tx.meta.preBalances.length;
430
- const keyToIdx = /* @__PURE__ */ new Map();
431
- for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
432
- const key = accountKeys.get(i);
433
- if (key) {
434
- keyToIdx.set(key.toBase58(), i);
435
- }
501
+ const verdict = checkBalanceDiff({
502
+ accountKeys: tx.transaction.message.accountKeys,
503
+ preBalances: tx.meta.preBalances,
504
+ postBalances: tx.meta.postBalances,
505
+ referenceKey,
506
+ recipientAddress,
507
+ treasuryAddress,
508
+ expectedNet,
509
+ expectedFee
510
+ });
511
+ if (verdict.ok) {
512
+ return { verified: true, txSignature: sig };
436
513
  }
437
- const recipientIdx = keyToIdx.get(recipientAddress);
438
- if (recipientIdx === void 0) {
439
- continue;
440
- }
441
- const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
442
- if (recipientDelta < expectedNet) {
443
- continue;
444
- }
445
- if (expectedFee > 0) {
446
- const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
447
- if (treasuryIdx === void 0) {
448
- continue;
449
- }
450
- const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
451
- if (treasuryDelta < expectedFee) {
452
- continue;
453
- }
454
- }
455
- return { verified: true, txSignature: sig };
456
514
  }
457
515
  }
458
516
  } catch (err) {
459
517
  lastError = err;
460
518
  }
461
519
  if (attempt < retries - 1) {
462
- await new Promise((r) => setTimeout(r, intervalMs));
520
+ await waitMs(intervalMs);
463
521
  }
464
522
  }
465
523
  return {
@@ -468,6 +526,99 @@ var SolanaPaymentStrategy = class {
468
526
  };
469
527
  }
470
528
  };
529
+ function checkBalanceDiff(input) {
530
+ const balanceCount = input.preBalances.length;
531
+ const keyToIdx = /* @__PURE__ */ new Map();
532
+ for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
533
+ const key = input.accountKeys[i];
534
+ if (key) {
535
+ keyToIdx.set(String(key), i);
536
+ }
537
+ }
538
+ if (!keyToIdx.has(input.referenceKey)) {
539
+ return { ok: false, reason: "Reference key not found in transaction - possible replay" };
540
+ }
541
+ const recipientIdx = keyToIdx.get(input.recipientAddress);
542
+ if (recipientIdx === void 0) {
543
+ return { ok: false, reason: "Recipient not found in transaction" };
544
+ }
545
+ const recipientDelta = bigIntDelta(
546
+ input.postBalances[recipientIdx],
547
+ input.preBalances[recipientIdx]
548
+ );
549
+ if (recipientDelta < BigInt(input.expectedNet)) {
550
+ return {
551
+ ok: false,
552
+ reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
553
+ };
554
+ }
555
+ if (input.expectedFee > 0) {
556
+ const treasuryIdx = keyToIdx.get(input.treasuryAddress);
557
+ if (treasuryIdx === void 0) {
558
+ return { ok: false, reason: "Treasury not found in transaction" };
559
+ }
560
+ const treasuryDelta = bigIntDelta(
561
+ input.postBalances[treasuryIdx],
562
+ input.preBalances[treasuryIdx]
563
+ );
564
+ if (treasuryDelta < BigInt(input.expectedFee)) {
565
+ return {
566
+ ok: false,
567
+ reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
568
+ };
569
+ }
570
+ }
571
+ return { ok: true };
572
+ }
573
+ function bigIntDelta(post, pre) {
574
+ const postValue = post === void 0 ? 0n : BigInt(post);
575
+ const preValue = pre === void 0 ? 0n : BigInt(pre);
576
+ return postValue - preValue;
577
+ }
578
+ function waitMs(ms) {
579
+ return new Promise((resolve) => setTimeout(resolve, ms));
580
+ }
581
+ function buildPaymentInstructions(paymentRequest, payerSigner) {
582
+ const recipient = address(paymentRequest.recipient);
583
+ const reference = address(paymentRequest.reference);
584
+ const feeAmount = paymentRequest.fee_amount ?? 0;
585
+ const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
586
+ if (providerAmount <= 0) {
587
+ throw new Error(
588
+ `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
589
+ );
590
+ }
591
+ const providerTransferIx = getTransferSolInstruction({
592
+ source: payerSigner,
593
+ destination: recipient,
594
+ amount: BigInt(providerAmount)
595
+ });
596
+ const providerTransferIxWithReference = {
597
+ ...providerTransferIx,
598
+ accounts: [...providerTransferIx.accounts, { address: reference, role: AccountRole.READONLY }]
599
+ };
600
+ const instructions = [providerTransferIxWithReference];
601
+ if (paymentRequest.fee_address && feeAmount > 0) {
602
+ instructions.push(
603
+ getTransferSolInstruction({
604
+ source: payerSigner,
605
+ destination: address(paymentRequest.fee_address),
606
+ amount: BigInt(feeAmount)
607
+ })
608
+ );
609
+ }
610
+ return instructions;
611
+ }
612
+ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
613
+ const config = await getProtocolConfig(rpc, programId);
614
+ const strategy = new SolanaPaymentStrategy();
615
+ return strategy.createPaymentRequest(
616
+ recipient,
617
+ amount,
618
+ { feeBps: config.feeBps, treasury: config.treasury },
619
+ options
620
+ );
621
+ }
471
622
  function toDTag(name) {
472
623
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
473
624
  if (!tag) {
@@ -1571,12 +1722,18 @@ var PingService = class _PingService {
1571
1722
  constructor(pool) {
1572
1723
  this.pool = pool;
1573
1724
  this.sessionIdentity = ElisymIdentity.generate();
1725
+ pool.onReset(() => this.clearCache());
1574
1726
  }
1575
1727
  static PING_CACHE_MAX = 1e3;
1576
1728
  sessionIdentity;
1577
1729
  pingCache = /* @__PURE__ */ new Map();
1578
1730
  // pubkey - timestamp of last online result
1579
1731
  pendingPings = /* @__PURE__ */ new Map();
1732
+ /** Drop cached online results. In-flight pings are left alone - they'll
1733
+ * resolve via their own timeouts and remove themselves from `pendingPings`. */
1734
+ clearCache() {
1735
+ this.pingCache.clear();
1736
+ }
1580
1737
  /**
1581
1738
  * Ping an agent via ephemeral Nostr events (kind 20200/20201).
1582
1739
  * Uses a persistent session identity to avoid relay rate-limiting.
@@ -1775,10 +1932,28 @@ var NostrPool = class {
1775
1932
  pool;
1776
1933
  relays;
1777
1934
  activeSubscriptions = /* @__PURE__ */ new Set();
1935
+ resetListeners = /* @__PURE__ */ new Set();
1778
1936
  constructor(relays = RELAYS) {
1779
1937
  this.pool = new SimplePool();
1780
1938
  this.relays = relays;
1781
1939
  }
1940
+ /**
1941
+ * Register a callback to run after `reset()` completes (new SimplePool in place).
1942
+ * Services that cache pool-derived state (e.g. ping results) must clear it here,
1943
+ * otherwise stale values survive the reconnect. Returns an unsubscribe function.
1944
+ *
1945
+ * Contract:
1946
+ * - Listeners are invoked **synchronously** at the end of `reset()`, in
1947
+ * registration order. Do not rely on async work inside a listener having
1948
+ * completed before `reset()` returns.
1949
+ * - Listener exceptions are caught and swallowed so that one faulty listener
1950
+ * cannot prevent the others from running (or abort the reset itself).
1951
+ * If a listener needs to surface errors, it must do so out-of-band.
1952
+ */
1953
+ onReset(listener) {
1954
+ this.resetListeners.add(listener);
1955
+ return () => this.resetListeners.delete(listener);
1956
+ }
1782
1957
  /** Query relays synchronously. Returns `[]` on timeout (no error thrown). */
1783
1958
  async querySync(filter) {
1784
1959
  let timer;
@@ -1786,13 +1961,12 @@ var NostrPool = class {
1786
1961
  query.catch(() => {
1787
1962
  });
1788
1963
  try {
1789
- const result = await Promise.race([
1964
+ return await Promise.race([
1790
1965
  query,
1791
1966
  new Promise((resolve) => {
1792
1967
  timer = setTimeout(() => resolve([]), DEFAULTS.QUERY_TIMEOUT_MS);
1793
1968
  })
1794
1969
  ]);
1795
- return result;
1796
1970
  } finally {
1797
1971
  clearTimeout(timer);
1798
1972
  }
@@ -1974,6 +2148,12 @@ var NostrPool = class {
1974
2148
  } catch {
1975
2149
  }
1976
2150
  this.pool = new SimplePool();
2151
+ for (const listener of this.resetListeners) {
2152
+ try {
2153
+ listener();
2154
+ } catch {
2155
+ }
2156
+ }
1977
2157
  }
1978
2158
  /**
1979
2159
  * Lightweight connectivity probe. Returns true if at least one relay responds.
@@ -2091,6 +2271,6 @@ function serializeConfig(config) {
2091
2271
  return JSON.stringify(config, null, 2) + "\n";
2092
2272
  }
2093
2273
 
2094
- export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DiscoveryService, ElisymClient, ElisymIdentity, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_TREASURY, PingService, RELAYS, SolanaPaymentStrategy, assertExpiry, assertLamports, calculateProtocolFee, formatSol, jobRequestKind, jobResultKind, nip44Decrypt, nip44Encrypt, serializeConfig, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
2274
+ export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DiscoveryService, ElisymClient, ElisymIdentity, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PingService, RELAYS, SolanaPaymentStrategy, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, nip44Decrypt, nip44Encrypt, serializeConfig, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
2095
2275
  //# sourceMappingURL=index.js.map
2096
2276
  //# sourceMappingURL=index.js.map