@elisym/sdk 0.3.3 → 0.4.1

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,25 @@ 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;
295
+ if (expectedFee === 0) {
296
+ return null;
297
+ }
185
298
  const { fee_address, fee_amount } = data;
186
299
  const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
187
300
  const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
188
301
  if (hasFeeAddress && hasFeeAmount) {
189
- if (fee_address !== PROTOCOL_TREASURY) {
302
+ if (fee_address !== treasury) {
190
303
  return {
191
304
  code: "fee_address_mismatch",
192
- message: `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`
305
+ message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
193
306
  };
194
307
  }
195
308
  if (fee_amount !== expectedFee) {
196
309
  return {
197
310
  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.`
311
+ message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
199
312
  };
200
313
  }
201
314
  return null;
@@ -203,20 +316,24 @@ var SolanaPaymentStrategy = class {
203
316
  if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
204
317
  return {
205
318
  code: "missing_fee",
206
- message: `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
319
+ message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
207
320
  };
208
321
  }
209
322
  return {
210
323
  code: "invalid_fee_params",
211
- message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
324
+ message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
212
325
  };
213
326
  }
214
327
  /**
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.
328
+ * Build, sign, and return a transaction for the supplied payment request.
329
+ * The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
330
+ *
331
+ * The provider transfer instruction includes the payment reference as a
332
+ * read-only, non-signer account so providers can detect the payment via
333
+ * `getSignaturesForAddress(reference)`.
218
334
  */
219
- async buildTransaction(payerAddress, paymentRequest) {
335
+ async buildTransaction(paymentRequest, payerSigner, rpc, config) {
336
+ assertConfig(config);
220
337
  assertLamports(paymentRequest.amount, "payment amount");
221
338
  if (paymentRequest.amount === 0) {
222
339
  throw new Error("Invalid payment amount: 0. Must be positive.");
@@ -226,50 +343,32 @@ var SolanaPaymentStrategy = class {
226
343
  `Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
227
344
  );
228
345
  }
346
+ assertReference(paymentRequest.reference);
229
347
  assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
230
- if (paymentRequest.fee_address && paymentRequest.fee_address !== PROTOCOL_TREASURY) {
348
+ const treasury = config.treasury;
349
+ if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
231
350
  throw new Error(
232
- `Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
351
+ `Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
233
352
  );
234
353
  }
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) {
242
- throw new Error(
243
- `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
244
- );
245
- }
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;
354
+ const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
355
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
356
+ const message = pipe(
357
+ createTransactionMessage({ version: 0 }),
358
+ (m) => setTransactionMessageFeePayerSigner(payerSigner, m),
359
+ (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
360
+ (m) => appendTransactionMessageInstructions(
361
+ instructions,
362
+ m
363
+ )
364
+ );
365
+ return signTransactionMessageWithSigners(message);
267
366
  }
268
- async verifyPayment(connection, paymentRequest, options) {
269
- if (!connection || typeof connection.getTransaction !== "function") {
270
- return { verified: false, error: "Invalid connection: expected Solana Connection instance" };
367
+ async verifyPayment(rpc, paymentRequest, config, options) {
368
+ assertConfig(config);
369
+ if (!rpc || typeof rpc.getTransaction !== "function") {
370
+ return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
271
371
  }
272
- const conn = connection;
273
372
  if (!paymentRequest.reference || !paymentRequest.recipient) {
274
373
  return { verified: false, error: "Missing required fields in payment request" };
275
374
  }
@@ -285,19 +384,20 @@ var SolanaPaymentStrategy = class {
285
384
  error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
286
385
  };
287
386
  }
288
- const expectedFee = calculateProtocolFee(paymentRequest.amount);
387
+ const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
289
388
  const feeAmount = paymentRequest.fee_amount ?? 0;
389
+ const treasury = config.treasury;
290
390
  if (expectedFee > 0) {
291
391
  if (feeAmount < expectedFee) {
292
392
  return {
293
393
  verified: false,
294
- error: `Protocol fee ${feeAmount} below required ${expectedFee} (${PROTOCOL_FEE_BPS}bps of ${paymentRequest.amount})`
394
+ error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
295
395
  };
296
396
  }
297
397
  if (!paymentRequest.fee_address) {
298
398
  return { verified: false, error: "Missing fee address in payment request" };
299
399
  }
300
- if (paymentRequest.fee_address !== PROTOCOL_TREASURY) {
400
+ if (paymentRequest.fee_address !== treasury) {
301
401
  return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
302
402
  }
303
403
  }
@@ -310,10 +410,11 @@ var SolanaPaymentStrategy = class {
310
410
  }
311
411
  if (options?.txSignature) {
312
412
  return this._verifyBySignature(
313
- conn,
413
+ rpc,
314
414
  options.txSignature,
315
415
  paymentRequest.reference,
316
416
  paymentRequest.recipient,
417
+ treasury,
317
418
  expectedNet,
318
419
  feeAmount,
319
420
  options?.retries ?? DEFAULTS.VERIFY_RETRIES,
@@ -321,26 +422,28 @@ var SolanaPaymentStrategy = class {
321
422
  );
322
423
  }
323
424
  return this._verifyByReference(
324
- conn,
425
+ rpc,
325
426
  paymentRequest.reference,
326
427
  paymentRequest.recipient,
428
+ treasury,
327
429
  expectedNet,
328
430
  feeAmount,
329
431
  options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
330
432
  options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
331
433
  );
332
434
  }
333
- async _verifyBySignature(connection, txSignature, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
435
+ async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
334
436
  let lastError;
335
437
  for (let attempt = 0; attempt < retries; attempt++) {
336
438
  try {
337
- const tx = await connection.getTransaction(txSignature, {
338
- maxSupportedTransactionVersion: 0,
339
- commitment: "confirmed"
340
- });
439
+ const tx = await rpc.getTransaction(txSignature, {
440
+ commitment: "confirmed",
441
+ encoding: "json",
442
+ maxSupportedTransactionVersion: 0
443
+ }).send();
341
444
  if (!tx?.meta || tx.meta.err) {
342
445
  if (attempt < retries - 1) {
343
- await new Promise((r) => setTimeout(r, intervalMs));
446
+ await waitMs(intervalMs);
344
447
  continue;
345
448
  }
346
449
  return {
@@ -348,50 +451,24 @@ var SolanaPaymentStrategy = class {
348
451
  error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
349
452
  };
350
453
  }
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
- }
454
+ const verdict = checkBalanceDiff({
455
+ accountKeys: tx.transaction.message.accountKeys,
456
+ preBalances: tx.meta.preBalances,
457
+ postBalances: tx.meta.postBalances,
458
+ referenceKey,
459
+ recipientAddress,
460
+ treasuryAddress,
461
+ expectedNet,
462
+ expectedFee
463
+ });
464
+ if (verdict.ok) {
465
+ return { verified: true, txSignature };
389
466
  }
390
- return { verified: true, txSignature };
467
+ return { verified: false, error: verdict.reason };
391
468
  } catch (err) {
392
469
  lastError = err;
393
470
  if (attempt < retries - 1) {
394
- await new Promise((r) => setTimeout(r, intervalMs));
471
+ await waitMs(intervalMs);
395
472
  }
396
473
  }
397
474
  }
@@ -400,66 +477,50 @@ var SolanaPaymentStrategy = class {
400
477
  error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
401
478
  };
402
479
  }
403
- async _verifyByReference(connection, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
404
- const reference = new PublicKey(referenceKey);
480
+ async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
405
481
  let lastError;
482
+ const reference = address(referenceKey);
406
483
  for (let attempt = 0; attempt < retries; attempt++) {
407
484
  try {
408
- const signatures = await connection.getSignaturesForAddress(reference, {
485
+ const signatures = await rpc.getSignaturesForAddress(reference, {
409
486
  limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
410
- });
411
- const validSigs = signatures.filter((s) => !s.err);
487
+ }).send();
488
+ const validSigs = signatures.filter((entry) => !entry.err);
412
489
  if (validSigs.length > 0) {
490
+ const fetchTransaction = (sig) => rpc.getTransaction(sig, {
491
+ commitment: "confirmed",
492
+ encoding: "json",
493
+ maxSupportedTransactionVersion: 0
494
+ }).send();
413
495
  const txResults = await Promise.all(
414
496
  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
- }))
497
+ (entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
422
498
  )
423
499
  );
424
500
  for (const { sig, tx } of txResults) {
425
501
  if (!tx?.meta || tx.meta.err) {
426
502
  continue;
427
503
  }
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
- }
436
- }
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
- }
504
+ const verdict = checkBalanceDiff({
505
+ accountKeys: tx.transaction.message.accountKeys,
506
+ preBalances: tx.meta.preBalances,
507
+ postBalances: tx.meta.postBalances,
508
+ referenceKey,
509
+ recipientAddress,
510
+ treasuryAddress,
511
+ expectedNet,
512
+ expectedFee
513
+ });
514
+ if (verdict.ok) {
515
+ return { verified: true, txSignature: sig };
454
516
  }
455
- return { verified: true, txSignature: sig };
456
517
  }
457
518
  }
458
519
  } catch (err) {
459
520
  lastError = err;
460
521
  }
461
522
  if (attempt < retries - 1) {
462
- await new Promise((r) => setTimeout(r, intervalMs));
523
+ await waitMs(intervalMs);
463
524
  }
464
525
  }
465
526
  return {
@@ -468,6 +529,99 @@ var SolanaPaymentStrategy = class {
468
529
  };
469
530
  }
470
531
  };
532
+ function checkBalanceDiff(input) {
533
+ const balanceCount = input.preBalances.length;
534
+ const keyToIdx = /* @__PURE__ */ new Map();
535
+ for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
536
+ const key = input.accountKeys[i];
537
+ if (key) {
538
+ keyToIdx.set(String(key), i);
539
+ }
540
+ }
541
+ if (!keyToIdx.has(input.referenceKey)) {
542
+ return { ok: false, reason: "Reference key not found in transaction - possible replay" };
543
+ }
544
+ const recipientIdx = keyToIdx.get(input.recipientAddress);
545
+ if (recipientIdx === void 0) {
546
+ return { ok: false, reason: "Recipient not found in transaction" };
547
+ }
548
+ const recipientDelta = bigIntDelta(
549
+ input.postBalances[recipientIdx],
550
+ input.preBalances[recipientIdx]
551
+ );
552
+ if (recipientDelta < BigInt(input.expectedNet)) {
553
+ return {
554
+ ok: false,
555
+ reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
556
+ };
557
+ }
558
+ if (input.expectedFee > 0) {
559
+ const treasuryIdx = keyToIdx.get(input.treasuryAddress);
560
+ if (treasuryIdx === void 0) {
561
+ return { ok: false, reason: "Treasury not found in transaction" };
562
+ }
563
+ const treasuryDelta = bigIntDelta(
564
+ input.postBalances[treasuryIdx],
565
+ input.preBalances[treasuryIdx]
566
+ );
567
+ if (treasuryDelta < BigInt(input.expectedFee)) {
568
+ return {
569
+ ok: false,
570
+ reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
571
+ };
572
+ }
573
+ }
574
+ return { ok: true };
575
+ }
576
+ function bigIntDelta(post, pre) {
577
+ const postValue = post === void 0 ? 0n : BigInt(post);
578
+ const preValue = pre === void 0 ? 0n : BigInt(pre);
579
+ return postValue - preValue;
580
+ }
581
+ function waitMs(ms) {
582
+ return new Promise((resolve) => setTimeout(resolve, ms));
583
+ }
584
+ function buildPaymentInstructions(paymentRequest, payerSigner) {
585
+ const recipient = address(paymentRequest.recipient);
586
+ const reference = address(paymentRequest.reference);
587
+ const feeAmount = paymentRequest.fee_amount ?? 0;
588
+ const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
589
+ if (providerAmount <= 0) {
590
+ throw new Error(
591
+ `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
592
+ );
593
+ }
594
+ const providerTransferIx = getTransferSolInstruction({
595
+ source: payerSigner,
596
+ destination: recipient,
597
+ amount: BigInt(providerAmount)
598
+ });
599
+ const providerTransferIxWithReference = {
600
+ ...providerTransferIx,
601
+ accounts: [...providerTransferIx.accounts, { address: reference, role: AccountRole.READONLY }]
602
+ };
603
+ const instructions = [providerTransferIxWithReference];
604
+ if (paymentRequest.fee_address && feeAmount > 0) {
605
+ instructions.push(
606
+ getTransferSolInstruction({
607
+ source: payerSigner,
608
+ destination: address(paymentRequest.fee_address),
609
+ amount: BigInt(feeAmount)
610
+ })
611
+ );
612
+ }
613
+ return instructions;
614
+ }
615
+ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
616
+ const config = await getProtocolConfig(rpc, programId);
617
+ const strategy = new SolanaPaymentStrategy();
618
+ return strategy.createPaymentRequest(
619
+ recipient,
620
+ amount,
621
+ { feeBps: config.feeBps, treasury: config.treasury },
622
+ options
623
+ );
624
+ }
471
625
  function toDTag(name) {
472
626
  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
627
  if (!tag) {
@@ -2120,6 +2274,6 @@ function serializeConfig(config) {
2120
2274
  return JSON.stringify(config, null, 2) + "\n";
2121
2275
  }
2122
2276
 
2123
- 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 };
2277
+ 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 };
2124
2278
  //# sourceMappingURL=index.js.map
2125
2279
  //# sourceMappingURL=index.js.map