@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.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- var web3_js = require('@solana/web3.js');
3
+ var system = require('@solana-program/system');
4
+ var kit = require('@solana/kit');
4
5
  var Decimal2 = require('decimal.js-light');
5
6
  var nostrTools = require('nostr-tools');
6
7
  var nip44 = require('nostr-tools/nip44');
@@ -60,6 +61,16 @@ var KIND_PONG = 20201;
60
61
  var LAMPORTS_PER_SOL = 1e9;
61
62
  var PROTOCOL_FEE_BPS = 300;
62
63
  var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
64
+ var PROTOCOL_PROGRAM_ID_DEVNET = "BrX1CRkSgvcjxBvc2bgc3QqgWjinusofDmeP7ZVxvwrE";
65
+ function getProtocolProgramId(cluster) {
66
+ switch (cluster) {
67
+ case "devnet":
68
+ case "localnet":
69
+ return PROTOCOL_PROGRAM_ID_DEVNET;
70
+ case "mainnet":
71
+ throw new Error("Protocol program is not deployed on mainnet yet");
72
+ }
73
+ }
63
74
  var DEFAULTS = {
64
75
  SUBSCRIPTION_TIMEOUT_MS: 12e4,
65
76
  PING_TIMEOUT_MS: 15e3,
@@ -86,19 +97,100 @@ var LIMITS = {
86
97
  MAX_AGENT_NAME_LENGTH: 64,
87
98
  MAX_CAPABILITY_LENGTH: 64
88
99
  };
100
+ function getConfigDecoder() {
101
+ return kit.getStructDecoder([
102
+ ["discriminator", kit.fixDecoderSize(kit.getBytesDecoder(), 8)],
103
+ ["version", kit.getU8Decoder()],
104
+ ["bump", kit.getU8Decoder()],
105
+ ["admin", kit.getAddressDecoder()],
106
+ ["pendingAdmin", kit.getOptionDecoder(kit.getAddressDecoder())],
107
+ ["treasury", kit.getAddressDecoder()],
108
+ ["feeBps", kit.getU16Decoder()],
109
+ ["paused", kit.getBooleanDecoder()],
110
+ ["lastUpdated", kit.getI64Decoder()],
111
+ ["reserved", kit.fixDecoderSize(kit.getBytesDecoder(), 128)]
112
+ ]);
113
+ }
114
+ function decodeConfig(encodedAccount) {
115
+ return kit.decodeAccount(
116
+ encodedAccount,
117
+ getConfigDecoder()
118
+ );
119
+ }
120
+ async function fetchConfig(rpc, address2, config) {
121
+ const maybeAccount = await fetchMaybeConfig(rpc, address2, config);
122
+ kit.assertAccountExists(maybeAccount);
123
+ return maybeAccount;
124
+ }
125
+ async function fetchMaybeConfig(rpc, address2, config) {
126
+ const maybeAccount = await kit.fetchEncodedAccount(rpc, address2, config);
127
+ return decodeConfig(maybeAccount);
128
+ }
129
+ if (process.env.NODE_ENV !== "production") ;
130
+ var CONFIG_SEED = "config";
131
+ async function deriveConfigAddress(programId) {
132
+ const [pda] = await kit.getProgramDerivedAddress({
133
+ programAddress: programId,
134
+ seeds: [new TextEncoder().encode(CONFIG_SEED)]
135
+ });
136
+ return pda;
137
+ }
138
+
139
+ // src/config/onchain.ts
140
+ var CACHE_TTL_MS = 6e4;
141
+ var cache = /* @__PURE__ */ new Map();
142
+ function clearProtocolConfigCache() {
143
+ cache.clear();
144
+ }
145
+ async function getProtocolConfig(rpc, programId, options) {
146
+ const key = programId.toString();
147
+ const ttl = options?.ttlMs ?? CACHE_TTL_MS;
148
+ const cached = cache.get(key);
149
+ if (!options?.forceRefresh && cached && Date.now() < cached.expires) {
150
+ return { ...cached.config, source: "cache" };
151
+ }
152
+ try {
153
+ const configPda = await deriveConfigAddress(programId);
154
+ const account = await fetchConfig(rpc, configPda);
155
+ const data = account.data;
156
+ const config = {
157
+ programId,
158
+ feeBps: data.feeBps,
159
+ treasury: data.treasury,
160
+ admin: data.admin,
161
+ pendingAdmin: data.pendingAdmin.__option === "Some" ? data.pendingAdmin.value : null,
162
+ paused: data.paused,
163
+ version: data.version,
164
+ source: "onchain"
165
+ };
166
+ cache.set(key, { config, expires: Date.now() + ttl });
167
+ return config;
168
+ } catch (error) {
169
+ if (cached) {
170
+ return { ...cached.config, source: "cache" };
171
+ }
172
+ throw new Error(
173
+ `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)}`
174
+ );
175
+ }
176
+ }
177
+ var BPS_DENOMINATOR = 1e4;
89
178
  function assertLamports(value, field) {
90
179
  if (!Number.isInteger(value) || value < 0) {
91
180
  throw new Error(`Invalid ${field}: ${value}. Must be a non-negative integer.`);
92
181
  }
93
182
  }
94
- function calculateProtocolFee(amount) {
183
+ function calculateProtocolFee(amount, feeBps) {
184
+ if (!Number.isInteger(feeBps) || feeBps < 0) {
185
+ throw new Error(`Invalid feeBps: ${feeBps}. Must be a non-negative integer.`);
186
+ }
95
187
  if (!Number.isInteger(amount) || amount < 0) {
96
188
  throw new Error(`Invalid fee amount: ${amount}. Must be a non-negative integer.`);
97
189
  }
98
- if (amount === 0) {
190
+ if (amount === 0 || feeBps === 0) {
99
191
  return 0;
100
192
  }
101
- return new Decimal2__default.default(amount).mul(PROTOCOL_FEE_BPS).div(1e4).toDecimalPlaces(0, Decimal2__default.default.ROUND_CEIL).toNumber();
193
+ return new Decimal2__default.default(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal2__default.default.ROUND_CEIL).toNumber();
102
194
  }
103
195
  function validateExpiry(createdAt, expirySecs) {
104
196
  if (!Number.isInteger(createdAt) || createdAt <= 0) {
@@ -124,47 +216,64 @@ function assertExpiry(createdAt, expirySecs) {
124
216
  }
125
217
 
126
218
  // src/payment/solana.ts
127
- function isValidSolanaAddress(address) {
128
- try {
129
- void new web3_js.PublicKey(address);
130
- return true;
131
- } catch {
132
- return false;
219
+ var REFERENCE_BYTE_LENGTH = 32;
220
+ function isValidSolanaAddress(value) {
221
+ return kit.isAddress(value);
222
+ }
223
+ function generateReference() {
224
+ const bytes = new Uint8Array(REFERENCE_BYTE_LENGTH);
225
+ globalThis.crypto.getRandomValues(bytes);
226
+ return kit.getAddressDecoder().decode(bytes);
227
+ }
228
+ function assertReference(reference) {
229
+ if (!isValidSolanaAddress(reference)) {
230
+ throw new Error(`Invalid reference address: ${reference}`);
231
+ }
232
+ }
233
+ function assertExpirySecs(expirySecs) {
234
+ if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
235
+ throw new Error(`Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`);
236
+ }
237
+ }
238
+ function assertConfig(config) {
239
+ if (!Number.isInteger(config.feeBps) || config.feeBps < 0) {
240
+ throw new Error(`Invalid feeBps: ${config.feeBps}. Must be a non-negative integer.`);
241
+ }
242
+ if (typeof config.treasury !== "string" || !isValidSolanaAddress(config.treasury)) {
243
+ throw new Error(`Invalid treasury address: ${String(config.treasury)}`);
133
244
  }
134
245
  }
135
246
  var SolanaPaymentStrategy = class {
136
247
  chain = "solana";
137
- calculateFee(amount) {
138
- return calculateProtocolFee(amount);
248
+ calculateFee(amount, config) {
249
+ assertConfig(config);
250
+ return calculateProtocolFee(amount, config.feeBps);
139
251
  }
140
- createPaymentRequest(recipientAddress, amount, expirySecs = DEFAULTS.PAYMENT_EXPIRY_SECS) {
141
- try {
142
- void new web3_js.PublicKey(recipientAddress);
143
- } catch {
252
+ createPaymentRequest(recipientAddress, amount, config, options) {
253
+ assertConfig(config);
254
+ if (!isValidSolanaAddress(recipientAddress)) {
144
255
  throw new Error(`Invalid Solana address: ${recipientAddress}`);
145
256
  }
146
257
  assertLamports(amount, "payment amount");
147
258
  if (amount === 0) {
148
259
  throw new Error("Invalid payment amount: 0. Must be positive.");
149
260
  }
150
- if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
151
- throw new Error(
152
- `Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`
153
- );
154
- }
155
- const feeAmount = calculateProtocolFee(amount);
156
- const reference = web3_js.Keypair.generate().publicKey.toBase58();
261
+ const expirySecs = options?.expirySecs ?? DEFAULTS.PAYMENT_EXPIRY_SECS;
262
+ assertExpirySecs(expirySecs);
263
+ const feeAmount = calculateProtocolFee(amount, config.feeBps);
264
+ const reference = generateReference();
157
265
  return {
158
266
  recipient: recipientAddress,
159
267
  amount,
160
268
  reference,
161
- fee_address: PROTOCOL_TREASURY,
269
+ fee_address: config.treasury,
162
270
  fee_amount: feeAmount,
163
271
  created_at: Math.floor(Date.now() / 1e3),
164
272
  expiry_secs: expirySecs
165
273
  };
166
274
  }
167
- validatePaymentRequest(requestJson, expectedRecipient) {
275
+ validatePaymentRequest(requestJson, config, expectedRecipient) {
276
+ assertConfig(config);
168
277
  let data;
169
278
  try {
170
279
  data = JSON.parse(requestJson);
@@ -206,21 +315,25 @@ var SolanaPaymentStrategy = class {
206
315
  const code = expiryError.includes("future") ? "future_timestamp" : "expired";
207
316
  return { code, message: expiryError };
208
317
  }
209
- const expectedFee = calculateProtocolFee(data.amount);
318
+ const expectedFee = calculateProtocolFee(data.amount, config.feeBps);
319
+ const treasury = config.treasury;
320
+ if (expectedFee === 0) {
321
+ return null;
322
+ }
210
323
  const { fee_address, fee_amount } = data;
211
324
  const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
212
325
  const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
213
326
  if (hasFeeAddress && hasFeeAmount) {
214
- if (fee_address !== PROTOCOL_TREASURY) {
327
+ if (fee_address !== treasury) {
215
328
  return {
216
329
  code: "fee_address_mismatch",
217
- message: `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`
330
+ message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
218
331
  };
219
332
  }
220
333
  if (fee_amount !== expectedFee) {
221
334
  return {
222
335
  code: "fee_amount_mismatch",
223
- message: `Fee amount mismatch: expected ${expectedFee} lamports (${PROTOCOL_FEE_BPS}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
336
+ message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
224
337
  };
225
338
  }
226
339
  return null;
@@ -228,20 +341,24 @@ var SolanaPaymentStrategy = class {
228
341
  if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
229
342
  return {
230
343
  code: "missing_fee",
231
- message: `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
344
+ message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
232
345
  };
233
346
  }
234
347
  return {
235
348
  code: "invalid_fee_params",
236
- message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
349
+ message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
237
350
  };
238
351
  }
239
352
  /**
240
- * Build an unsigned transaction from a payment request.
241
- * The caller must set `recentBlockhash` and `feePayer` on the
242
- * returned Transaction before signing and sending.
353
+ * Build, sign, and return a transaction for the supplied payment request.
354
+ * The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
355
+ *
356
+ * The provider transfer instruction includes the payment reference as a
357
+ * read-only, non-signer account so providers can detect the payment via
358
+ * `getSignaturesForAddress(reference)`.
243
359
  */
244
- async buildTransaction(payerAddress, paymentRequest) {
360
+ async buildTransaction(paymentRequest, payerSigner, rpc, config) {
361
+ assertConfig(config);
245
362
  assertLamports(paymentRequest.amount, "payment amount");
246
363
  if (paymentRequest.amount === 0) {
247
364
  throw new Error("Invalid payment amount: 0. Must be positive.");
@@ -251,50 +368,32 @@ var SolanaPaymentStrategy = class {
251
368
  `Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
252
369
  );
253
370
  }
371
+ assertReference(paymentRequest.reference);
254
372
  assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
255
- if (paymentRequest.fee_address && paymentRequest.fee_address !== PROTOCOL_TREASURY) {
373
+ const treasury = config.treasury;
374
+ if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
256
375
  throw new Error(
257
- `Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
376
+ `Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
258
377
  );
259
378
  }
260
- const payerPubkey = new web3_js.PublicKey(payerAddress);
261
- const recipient = new web3_js.PublicKey(paymentRequest.recipient);
262
- const reference = new web3_js.PublicKey(paymentRequest.reference);
263
- const feeAddress = paymentRequest.fee_address ? new web3_js.PublicKey(paymentRequest.fee_address) : null;
264
- const feeAmount = paymentRequest.fee_amount ?? 0;
265
- const providerAmount = feeAddress && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
266
- if (providerAmount <= 0) {
267
- throw new Error(
268
- `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
269
- );
270
- }
271
- const transferIx = web3_js.SystemProgram.transfer({
272
- fromPubkey: payerPubkey,
273
- toPubkey: recipient,
274
- lamports: providerAmount
275
- });
276
- transferIx.keys.push({
277
- pubkey: reference,
278
- isSigner: false,
279
- isWritable: false
280
- });
281
- const tx = new web3_js.Transaction().add(transferIx);
282
- if (feeAddress && feeAmount > 0) {
283
- tx.add(
284
- web3_js.SystemProgram.transfer({
285
- fromPubkey: payerPubkey,
286
- toPubkey: feeAddress,
287
- lamports: feeAmount
288
- })
289
- );
290
- }
291
- return tx;
379
+ const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
380
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
381
+ const message = kit.pipe(
382
+ kit.createTransactionMessage({ version: 0 }),
383
+ (m) => kit.setTransactionMessageFeePayerSigner(payerSigner, m),
384
+ (m) => kit.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
385
+ (m) => kit.appendTransactionMessageInstructions(
386
+ instructions,
387
+ m
388
+ )
389
+ );
390
+ return kit.signTransactionMessageWithSigners(message);
292
391
  }
293
- async verifyPayment(connection, paymentRequest, options) {
294
- if (!connection || typeof connection.getTransaction !== "function") {
295
- return { verified: false, error: "Invalid connection: expected Solana Connection instance" };
392
+ async verifyPayment(rpc, paymentRequest, config, options) {
393
+ assertConfig(config);
394
+ if (!rpc || typeof rpc.getTransaction !== "function") {
395
+ return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
296
396
  }
297
- const conn = connection;
298
397
  if (!paymentRequest.reference || !paymentRequest.recipient) {
299
398
  return { verified: false, error: "Missing required fields in payment request" };
300
399
  }
@@ -310,19 +409,20 @@ var SolanaPaymentStrategy = class {
310
409
  error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
311
410
  };
312
411
  }
313
- const expectedFee = calculateProtocolFee(paymentRequest.amount);
412
+ const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
314
413
  const feeAmount = paymentRequest.fee_amount ?? 0;
414
+ const treasury = config.treasury;
315
415
  if (expectedFee > 0) {
316
416
  if (feeAmount < expectedFee) {
317
417
  return {
318
418
  verified: false,
319
- error: `Protocol fee ${feeAmount} below required ${expectedFee} (${PROTOCOL_FEE_BPS}bps of ${paymentRequest.amount})`
419
+ error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
320
420
  };
321
421
  }
322
422
  if (!paymentRequest.fee_address) {
323
423
  return { verified: false, error: "Missing fee address in payment request" };
324
424
  }
325
- if (paymentRequest.fee_address !== PROTOCOL_TREASURY) {
425
+ if (paymentRequest.fee_address !== treasury) {
326
426
  return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
327
427
  }
328
428
  }
@@ -335,10 +435,11 @@ var SolanaPaymentStrategy = class {
335
435
  }
336
436
  if (options?.txSignature) {
337
437
  return this._verifyBySignature(
338
- conn,
438
+ rpc,
339
439
  options.txSignature,
340
440
  paymentRequest.reference,
341
441
  paymentRequest.recipient,
442
+ treasury,
342
443
  expectedNet,
343
444
  feeAmount,
344
445
  options?.retries ?? DEFAULTS.VERIFY_RETRIES,
@@ -346,26 +447,28 @@ var SolanaPaymentStrategy = class {
346
447
  );
347
448
  }
348
449
  return this._verifyByReference(
349
- conn,
450
+ rpc,
350
451
  paymentRequest.reference,
351
452
  paymentRequest.recipient,
453
+ treasury,
352
454
  expectedNet,
353
455
  feeAmount,
354
456
  options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
355
457
  options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
356
458
  );
357
459
  }
358
- async _verifyBySignature(connection, txSignature, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
460
+ async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
359
461
  let lastError;
360
462
  for (let attempt = 0; attempt < retries; attempt++) {
361
463
  try {
362
- const tx = await connection.getTransaction(txSignature, {
363
- maxSupportedTransactionVersion: 0,
364
- commitment: "confirmed"
365
- });
464
+ const tx = await rpc.getTransaction(txSignature, {
465
+ commitment: "confirmed",
466
+ encoding: "json",
467
+ maxSupportedTransactionVersion: 0
468
+ }).send();
366
469
  if (!tx?.meta || tx.meta.err) {
367
470
  if (attempt < retries - 1) {
368
- await new Promise((r) => setTimeout(r, intervalMs));
471
+ await waitMs(intervalMs);
369
472
  continue;
370
473
  }
371
474
  return {
@@ -373,50 +476,24 @@ var SolanaPaymentStrategy = class {
373
476
  error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
374
477
  };
375
478
  }
376
- const accountKeys = tx.transaction.message.getAccountKeys();
377
- const balanceCount = tx.meta.preBalances.length;
378
- const keyToIdx = /* @__PURE__ */ new Map();
379
- for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
380
- const key = accountKeys.get(i);
381
- if (key) {
382
- keyToIdx.set(key.toBase58(), i);
383
- }
384
- }
385
- if (!keyToIdx.has(referenceKey)) {
386
- return {
387
- verified: false,
388
- error: "Reference key not found in transaction - possible replay"
389
- };
390
- }
391
- const recipientIdx = keyToIdx.get(recipientAddress);
392
- if (recipientIdx === void 0) {
393
- return { verified: false, error: "Recipient not found in transaction" };
394
- }
395
- const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
396
- if (recipientDelta < expectedNet) {
397
- return {
398
- verified: false,
399
- error: `Recipient received ${recipientDelta}, expected >= ${expectedNet}`
400
- };
401
- }
402
- if (expectedFee > 0) {
403
- const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
404
- if (treasuryIdx === void 0) {
405
- return { verified: false, error: "Treasury not found in transaction" };
406
- }
407
- const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
408
- if (treasuryDelta < expectedFee) {
409
- return {
410
- verified: false,
411
- error: `Treasury received ${treasuryDelta}, expected >= ${expectedFee}`
412
- };
413
- }
479
+ const verdict = checkBalanceDiff({
480
+ accountKeys: tx.transaction.message.accountKeys,
481
+ preBalances: tx.meta.preBalances,
482
+ postBalances: tx.meta.postBalances,
483
+ referenceKey,
484
+ recipientAddress,
485
+ treasuryAddress,
486
+ expectedNet,
487
+ expectedFee
488
+ });
489
+ if (verdict.ok) {
490
+ return { verified: true, txSignature };
414
491
  }
415
- return { verified: true, txSignature };
492
+ return { verified: false, error: verdict.reason };
416
493
  } catch (err) {
417
494
  lastError = err;
418
495
  if (attempt < retries - 1) {
419
- await new Promise((r) => setTimeout(r, intervalMs));
496
+ await waitMs(intervalMs);
420
497
  }
421
498
  }
422
499
  }
@@ -425,66 +502,50 @@ var SolanaPaymentStrategy = class {
425
502
  error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
426
503
  };
427
504
  }
428
- async _verifyByReference(connection, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
429
- const reference = new web3_js.PublicKey(referenceKey);
505
+ async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
430
506
  let lastError;
507
+ const reference = kit.address(referenceKey);
431
508
  for (let attempt = 0; attempt < retries; attempt++) {
432
509
  try {
433
- const signatures = await connection.getSignaturesForAddress(reference, {
510
+ const signatures = await rpc.getSignaturesForAddress(reference, {
434
511
  limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
435
- });
436
- const validSigs = signatures.filter((s) => !s.err);
512
+ }).send();
513
+ const validSigs = signatures.filter((entry) => !entry.err);
437
514
  if (validSigs.length > 0) {
515
+ const fetchTransaction = (sig) => rpc.getTransaction(sig, {
516
+ commitment: "confirmed",
517
+ encoding: "json",
518
+ maxSupportedTransactionVersion: 0
519
+ }).send();
438
520
  const txResults = await Promise.all(
439
521
  validSigs.map(
440
- (s) => connection.getTransaction(s.signature, {
441
- maxSupportedTransactionVersion: 0,
442
- commitment: "confirmed"
443
- }).then((tx) => ({ sig: s.signature, tx })).catch(() => ({
444
- sig: s.signature,
445
- tx: null
446
- }))
522
+ (entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
447
523
  )
448
524
  );
449
525
  for (const { sig, tx } of txResults) {
450
526
  if (!tx?.meta || tx.meta.err) {
451
527
  continue;
452
528
  }
453
- const accountKeys = tx.transaction.message.getAccountKeys();
454
- const balanceCount = tx.meta.preBalances.length;
455
- const keyToIdx = /* @__PURE__ */ new Map();
456
- for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
457
- const key = accountKeys.get(i);
458
- if (key) {
459
- keyToIdx.set(key.toBase58(), i);
460
- }
461
- }
462
- const recipientIdx = keyToIdx.get(recipientAddress);
463
- if (recipientIdx === void 0) {
464
- continue;
465
- }
466
- const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
467
- if (recipientDelta < expectedNet) {
468
- continue;
469
- }
470
- if (expectedFee > 0) {
471
- const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
472
- if (treasuryIdx === void 0) {
473
- continue;
474
- }
475
- const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
476
- if (treasuryDelta < expectedFee) {
477
- continue;
478
- }
529
+ const verdict = checkBalanceDiff({
530
+ accountKeys: tx.transaction.message.accountKeys,
531
+ preBalances: tx.meta.preBalances,
532
+ postBalances: tx.meta.postBalances,
533
+ referenceKey,
534
+ recipientAddress,
535
+ treasuryAddress,
536
+ expectedNet,
537
+ expectedFee
538
+ });
539
+ if (verdict.ok) {
540
+ return { verified: true, txSignature: sig };
479
541
  }
480
- return { verified: true, txSignature: sig };
481
542
  }
482
543
  }
483
544
  } catch (err) {
484
545
  lastError = err;
485
546
  }
486
547
  if (attempt < retries - 1) {
487
- await new Promise((r) => setTimeout(r, intervalMs));
548
+ await waitMs(intervalMs);
488
549
  }
489
550
  }
490
551
  return {
@@ -493,6 +554,99 @@ var SolanaPaymentStrategy = class {
493
554
  };
494
555
  }
495
556
  };
557
+ function checkBalanceDiff(input) {
558
+ const balanceCount = input.preBalances.length;
559
+ const keyToIdx = /* @__PURE__ */ new Map();
560
+ for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
561
+ const key = input.accountKeys[i];
562
+ if (key) {
563
+ keyToIdx.set(String(key), i);
564
+ }
565
+ }
566
+ if (!keyToIdx.has(input.referenceKey)) {
567
+ return { ok: false, reason: "Reference key not found in transaction - possible replay" };
568
+ }
569
+ const recipientIdx = keyToIdx.get(input.recipientAddress);
570
+ if (recipientIdx === void 0) {
571
+ return { ok: false, reason: "Recipient not found in transaction" };
572
+ }
573
+ const recipientDelta = bigIntDelta(
574
+ input.postBalances[recipientIdx],
575
+ input.preBalances[recipientIdx]
576
+ );
577
+ if (recipientDelta < BigInt(input.expectedNet)) {
578
+ return {
579
+ ok: false,
580
+ reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
581
+ };
582
+ }
583
+ if (input.expectedFee > 0) {
584
+ const treasuryIdx = keyToIdx.get(input.treasuryAddress);
585
+ if (treasuryIdx === void 0) {
586
+ return { ok: false, reason: "Treasury not found in transaction" };
587
+ }
588
+ const treasuryDelta = bigIntDelta(
589
+ input.postBalances[treasuryIdx],
590
+ input.preBalances[treasuryIdx]
591
+ );
592
+ if (treasuryDelta < BigInt(input.expectedFee)) {
593
+ return {
594
+ ok: false,
595
+ reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
596
+ };
597
+ }
598
+ }
599
+ return { ok: true };
600
+ }
601
+ function bigIntDelta(post, pre) {
602
+ const postValue = post === void 0 ? 0n : BigInt(post);
603
+ const preValue = pre === void 0 ? 0n : BigInt(pre);
604
+ return postValue - preValue;
605
+ }
606
+ function waitMs(ms) {
607
+ return new Promise((resolve) => setTimeout(resolve, ms));
608
+ }
609
+ function buildPaymentInstructions(paymentRequest, payerSigner) {
610
+ const recipient = kit.address(paymentRequest.recipient);
611
+ const reference = kit.address(paymentRequest.reference);
612
+ const feeAmount = paymentRequest.fee_amount ?? 0;
613
+ const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
614
+ if (providerAmount <= 0) {
615
+ throw new Error(
616
+ `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
617
+ );
618
+ }
619
+ const providerTransferIx = system.getTransferSolInstruction({
620
+ source: payerSigner,
621
+ destination: recipient,
622
+ amount: BigInt(providerAmount)
623
+ });
624
+ const providerTransferIxWithReference = {
625
+ ...providerTransferIx,
626
+ accounts: [...providerTransferIx.accounts, { address: reference, role: kit.AccountRole.READONLY }]
627
+ };
628
+ const instructions = [providerTransferIxWithReference];
629
+ if (paymentRequest.fee_address && feeAmount > 0) {
630
+ instructions.push(
631
+ system.getTransferSolInstruction({
632
+ source: payerSigner,
633
+ destination: kit.address(paymentRequest.fee_address),
634
+ amount: BigInt(feeAmount)
635
+ })
636
+ );
637
+ }
638
+ return instructions;
639
+ }
640
+ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
641
+ const config = await getProtocolConfig(rpc, programId);
642
+ const strategy = new SolanaPaymentStrategy();
643
+ return strategy.createPaymentRequest(
644
+ recipient,
645
+ amount,
646
+ { feeBps: config.feeBps, treasury: config.treasury },
647
+ options
648
+ );
649
+ }
496
650
  function toDTag(name) {
497
651
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
498
652
  if (!tag) {
@@ -2165,14 +2319,20 @@ exports.MarketplaceService = MarketplaceService;
2165
2319
  exports.MediaService = MediaService;
2166
2320
  exports.NostrPool = NostrPool;
2167
2321
  exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
2322
+ exports.PROTOCOL_PROGRAM_ID_DEVNET = PROTOCOL_PROGRAM_ID_DEVNET;
2168
2323
  exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
2169
2324
  exports.PingService = PingService;
2170
2325
  exports.RELAYS = RELAYS;
2171
2326
  exports.SolanaPaymentStrategy = SolanaPaymentStrategy;
2172
2327
  exports.assertExpiry = assertExpiry;
2173
2328
  exports.assertLamports = assertLamports;
2329
+ exports.buildPaymentInstructions = buildPaymentInstructions;
2174
2330
  exports.calculateProtocolFee = calculateProtocolFee;
2331
+ exports.clearProtocolConfigCache = clearProtocolConfigCache;
2332
+ exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
2175
2333
  exports.formatSol = formatSol;
2334
+ exports.getProtocolConfig = getProtocolConfig;
2335
+ exports.getProtocolProgramId = getProtocolProgramId;
2176
2336
  exports.jobRequestKind = jobRequestKind;
2177
2337
  exports.jobResultKind = jobResultKind;
2178
2338
  exports.nip44Decrypt = nip44Decrypt;