@elisym/sdk 0.3.3 → 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.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,22 @@ 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;
210
320
  const { fee_address, fee_amount } = data;
211
321
  const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
212
322
  const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
213
323
  if (hasFeeAddress && hasFeeAmount) {
214
- if (fee_address !== PROTOCOL_TREASURY) {
324
+ if (fee_address !== treasury) {
215
325
  return {
216
326
  code: "fee_address_mismatch",
217
- message: `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`
327
+ message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
218
328
  };
219
329
  }
220
330
  if (fee_amount !== expectedFee) {
221
331
  return {
222
332
  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.`
333
+ message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
224
334
  };
225
335
  }
226
336
  return null;
@@ -228,20 +338,24 @@ var SolanaPaymentStrategy = class {
228
338
  if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
229
339
  return {
230
340
  code: "missing_fee",
231
- message: `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
341
+ message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
232
342
  };
233
343
  }
234
344
  return {
235
345
  code: "invalid_fee_params",
236
- message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
346
+ message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
237
347
  };
238
348
  }
239
349
  /**
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.
350
+ * Build, sign, and return a transaction for the supplied payment request.
351
+ * The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
352
+ *
353
+ * The provider transfer instruction includes the payment reference as a
354
+ * read-only, non-signer account so providers can detect the payment via
355
+ * `getSignaturesForAddress(reference)`.
243
356
  */
244
- async buildTransaction(payerAddress, paymentRequest) {
357
+ async buildTransaction(paymentRequest, payerSigner, rpc, config) {
358
+ assertConfig(config);
245
359
  assertLamports(paymentRequest.amount, "payment amount");
246
360
  if (paymentRequest.amount === 0) {
247
361
  throw new Error("Invalid payment amount: 0. Must be positive.");
@@ -251,50 +365,32 @@ var SolanaPaymentStrategy = class {
251
365
  `Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
252
366
  );
253
367
  }
368
+ assertReference(paymentRequest.reference);
254
369
  assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
255
- if (paymentRequest.fee_address && paymentRequest.fee_address !== PROTOCOL_TREASURY) {
256
- throw new Error(
257
- `Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
258
- );
259
- }
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) {
370
+ const treasury = config.treasury;
371
+ if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
267
372
  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
- })
373
+ `Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
289
374
  );
290
375
  }
291
- return tx;
376
+ const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
377
+ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
378
+ const message = kit.pipe(
379
+ kit.createTransactionMessage({ version: 0 }),
380
+ (m) => kit.setTransactionMessageFeePayerSigner(payerSigner, m),
381
+ (m) => kit.setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
382
+ (m) => kit.appendTransactionMessageInstructions(
383
+ instructions,
384
+ m
385
+ )
386
+ );
387
+ return kit.signTransactionMessageWithSigners(message);
292
388
  }
293
- async verifyPayment(connection, paymentRequest, options) {
294
- if (!connection || typeof connection.getTransaction !== "function") {
295
- return { verified: false, error: "Invalid connection: expected Solana Connection instance" };
389
+ async verifyPayment(rpc, paymentRequest, config, options) {
390
+ assertConfig(config);
391
+ if (!rpc || typeof rpc.getTransaction !== "function") {
392
+ return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
296
393
  }
297
- const conn = connection;
298
394
  if (!paymentRequest.reference || !paymentRequest.recipient) {
299
395
  return { verified: false, error: "Missing required fields in payment request" };
300
396
  }
@@ -310,19 +406,20 @@ var SolanaPaymentStrategy = class {
310
406
  error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
311
407
  };
312
408
  }
313
- const expectedFee = calculateProtocolFee(paymentRequest.amount);
409
+ const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
314
410
  const feeAmount = paymentRequest.fee_amount ?? 0;
411
+ const treasury = config.treasury;
315
412
  if (expectedFee > 0) {
316
413
  if (feeAmount < expectedFee) {
317
414
  return {
318
415
  verified: false,
319
- error: `Protocol fee ${feeAmount} below required ${expectedFee} (${PROTOCOL_FEE_BPS}bps of ${paymentRequest.amount})`
416
+ error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
320
417
  };
321
418
  }
322
419
  if (!paymentRequest.fee_address) {
323
420
  return { verified: false, error: "Missing fee address in payment request" };
324
421
  }
325
- if (paymentRequest.fee_address !== PROTOCOL_TREASURY) {
422
+ if (paymentRequest.fee_address !== treasury) {
326
423
  return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
327
424
  }
328
425
  }
@@ -335,10 +432,11 @@ var SolanaPaymentStrategy = class {
335
432
  }
336
433
  if (options?.txSignature) {
337
434
  return this._verifyBySignature(
338
- conn,
435
+ rpc,
339
436
  options.txSignature,
340
437
  paymentRequest.reference,
341
438
  paymentRequest.recipient,
439
+ treasury,
342
440
  expectedNet,
343
441
  feeAmount,
344
442
  options?.retries ?? DEFAULTS.VERIFY_RETRIES,
@@ -346,26 +444,28 @@ var SolanaPaymentStrategy = class {
346
444
  );
347
445
  }
348
446
  return this._verifyByReference(
349
- conn,
447
+ rpc,
350
448
  paymentRequest.reference,
351
449
  paymentRequest.recipient,
450
+ treasury,
352
451
  expectedNet,
353
452
  feeAmount,
354
453
  options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
355
454
  options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
356
455
  );
357
456
  }
358
- async _verifyBySignature(connection, txSignature, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
457
+ async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
359
458
  let lastError;
360
459
  for (let attempt = 0; attempt < retries; attempt++) {
361
460
  try {
362
- const tx = await connection.getTransaction(txSignature, {
363
- maxSupportedTransactionVersion: 0,
364
- commitment: "confirmed"
365
- });
461
+ const tx = await rpc.getTransaction(txSignature, {
462
+ commitment: "confirmed",
463
+ encoding: "json",
464
+ maxSupportedTransactionVersion: 0
465
+ }).send();
366
466
  if (!tx?.meta || tx.meta.err) {
367
467
  if (attempt < retries - 1) {
368
- await new Promise((r) => setTimeout(r, intervalMs));
468
+ await waitMs(intervalMs);
369
469
  continue;
370
470
  }
371
471
  return {
@@ -373,50 +473,24 @@ var SolanaPaymentStrategy = class {
373
473
  error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
374
474
  };
375
475
  }
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
- }
476
+ const verdict = checkBalanceDiff({
477
+ accountKeys: tx.transaction.message.accountKeys,
478
+ preBalances: tx.meta.preBalances,
479
+ postBalances: tx.meta.postBalances,
480
+ referenceKey,
481
+ recipientAddress,
482
+ treasuryAddress,
483
+ expectedNet,
484
+ expectedFee
485
+ });
486
+ if (verdict.ok) {
487
+ return { verified: true, txSignature };
414
488
  }
415
- return { verified: true, txSignature };
489
+ return { verified: false, error: verdict.reason };
416
490
  } catch (err) {
417
491
  lastError = err;
418
492
  if (attempt < retries - 1) {
419
- await new Promise((r) => setTimeout(r, intervalMs));
493
+ await waitMs(intervalMs);
420
494
  }
421
495
  }
422
496
  }
@@ -425,66 +499,50 @@ var SolanaPaymentStrategy = class {
425
499
  error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
426
500
  };
427
501
  }
428
- async _verifyByReference(connection, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
429
- const reference = new web3_js.PublicKey(referenceKey);
502
+ async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
430
503
  let lastError;
504
+ const reference = kit.address(referenceKey);
431
505
  for (let attempt = 0; attempt < retries; attempt++) {
432
506
  try {
433
- const signatures = await connection.getSignaturesForAddress(reference, {
507
+ const signatures = await rpc.getSignaturesForAddress(reference, {
434
508
  limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
435
- });
436
- const validSigs = signatures.filter((s) => !s.err);
509
+ }).send();
510
+ const validSigs = signatures.filter((entry) => !entry.err);
437
511
  if (validSigs.length > 0) {
512
+ const fetchTransaction = (sig) => rpc.getTransaction(sig, {
513
+ commitment: "confirmed",
514
+ encoding: "json",
515
+ maxSupportedTransactionVersion: 0
516
+ }).send();
438
517
  const txResults = await Promise.all(
439
518
  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
- }))
519
+ (entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
447
520
  )
448
521
  );
449
522
  for (const { sig, tx } of txResults) {
450
523
  if (!tx?.meta || tx.meta.err) {
451
524
  continue;
452
525
  }
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;
526
+ const verdict = checkBalanceDiff({
527
+ accountKeys: tx.transaction.message.accountKeys,
528
+ preBalances: tx.meta.preBalances,
529
+ postBalances: tx.meta.postBalances,
530
+ referenceKey,
531
+ recipientAddress,
532
+ treasuryAddress,
533
+ expectedNet,
534
+ expectedFee
535
+ });
536
+ if (verdict.ok) {
537
+ return { verified: true, txSignature: sig };
469
538
  }
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
- }
479
- }
480
- return { verified: true, txSignature: sig };
481
539
  }
482
540
  }
483
541
  } catch (err) {
484
542
  lastError = err;
485
543
  }
486
544
  if (attempt < retries - 1) {
487
- await new Promise((r) => setTimeout(r, intervalMs));
545
+ await waitMs(intervalMs);
488
546
  }
489
547
  }
490
548
  return {
@@ -493,6 +551,99 @@ var SolanaPaymentStrategy = class {
493
551
  };
494
552
  }
495
553
  };
554
+ function checkBalanceDiff(input) {
555
+ const balanceCount = input.preBalances.length;
556
+ const keyToIdx = /* @__PURE__ */ new Map();
557
+ for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
558
+ const key = input.accountKeys[i];
559
+ if (key) {
560
+ keyToIdx.set(String(key), i);
561
+ }
562
+ }
563
+ if (!keyToIdx.has(input.referenceKey)) {
564
+ return { ok: false, reason: "Reference key not found in transaction - possible replay" };
565
+ }
566
+ const recipientIdx = keyToIdx.get(input.recipientAddress);
567
+ if (recipientIdx === void 0) {
568
+ return { ok: false, reason: "Recipient not found in transaction" };
569
+ }
570
+ const recipientDelta = bigIntDelta(
571
+ input.postBalances[recipientIdx],
572
+ input.preBalances[recipientIdx]
573
+ );
574
+ if (recipientDelta < BigInt(input.expectedNet)) {
575
+ return {
576
+ ok: false,
577
+ reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
578
+ };
579
+ }
580
+ if (input.expectedFee > 0) {
581
+ const treasuryIdx = keyToIdx.get(input.treasuryAddress);
582
+ if (treasuryIdx === void 0) {
583
+ return { ok: false, reason: "Treasury not found in transaction" };
584
+ }
585
+ const treasuryDelta = bigIntDelta(
586
+ input.postBalances[treasuryIdx],
587
+ input.preBalances[treasuryIdx]
588
+ );
589
+ if (treasuryDelta < BigInt(input.expectedFee)) {
590
+ return {
591
+ ok: false,
592
+ reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
593
+ };
594
+ }
595
+ }
596
+ return { ok: true };
597
+ }
598
+ function bigIntDelta(post, pre) {
599
+ const postValue = post === void 0 ? 0n : BigInt(post);
600
+ const preValue = pre === void 0 ? 0n : BigInt(pre);
601
+ return postValue - preValue;
602
+ }
603
+ function waitMs(ms) {
604
+ return new Promise((resolve) => setTimeout(resolve, ms));
605
+ }
606
+ function buildPaymentInstructions(paymentRequest, payerSigner) {
607
+ const recipient = kit.address(paymentRequest.recipient);
608
+ const reference = kit.address(paymentRequest.reference);
609
+ const feeAmount = paymentRequest.fee_amount ?? 0;
610
+ const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
611
+ if (providerAmount <= 0) {
612
+ throw new Error(
613
+ `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
614
+ );
615
+ }
616
+ const providerTransferIx = system.getTransferSolInstruction({
617
+ source: payerSigner,
618
+ destination: recipient,
619
+ amount: BigInt(providerAmount)
620
+ });
621
+ const providerTransferIxWithReference = {
622
+ ...providerTransferIx,
623
+ accounts: [...providerTransferIx.accounts, { address: reference, role: kit.AccountRole.READONLY }]
624
+ };
625
+ const instructions = [providerTransferIxWithReference];
626
+ if (paymentRequest.fee_address && feeAmount > 0) {
627
+ instructions.push(
628
+ system.getTransferSolInstruction({
629
+ source: payerSigner,
630
+ destination: kit.address(paymentRequest.fee_address),
631
+ amount: BigInt(feeAmount)
632
+ })
633
+ );
634
+ }
635
+ return instructions;
636
+ }
637
+ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
638
+ const config = await getProtocolConfig(rpc, programId);
639
+ const strategy = new SolanaPaymentStrategy();
640
+ return strategy.createPaymentRequest(
641
+ recipient,
642
+ amount,
643
+ { feeBps: config.feeBps, treasury: config.treasury },
644
+ options
645
+ );
646
+ }
496
647
  function toDTag(name) {
497
648
  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
649
  if (!tag) {
@@ -2165,14 +2316,20 @@ exports.MarketplaceService = MarketplaceService;
2165
2316
  exports.MediaService = MediaService;
2166
2317
  exports.NostrPool = NostrPool;
2167
2318
  exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
2319
+ exports.PROTOCOL_PROGRAM_ID_DEVNET = PROTOCOL_PROGRAM_ID_DEVNET;
2168
2320
  exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
2169
2321
  exports.PingService = PingService;
2170
2322
  exports.RELAYS = RELAYS;
2171
2323
  exports.SolanaPaymentStrategy = SolanaPaymentStrategy;
2172
2324
  exports.assertExpiry = assertExpiry;
2173
2325
  exports.assertLamports = assertLamports;
2326
+ exports.buildPaymentInstructions = buildPaymentInstructions;
2174
2327
  exports.calculateProtocolFee = calculateProtocolFee;
2328
+ exports.clearProtocolConfigCache = clearProtocolConfigCache;
2329
+ exports.createPaymentRequestWithOnchainConfig = createPaymentRequestWithOnchainConfig;
2175
2330
  exports.formatSol = formatSol;
2331
+ exports.getProtocolConfig = getProtocolConfig;
2332
+ exports.getProtocolProgramId = getProtocolProgramId;
2176
2333
  exports.jobRequestKind = jobRequestKind;
2177
2334
  exports.jobResultKind = jobResultKind;
2178
2335
  exports.nip44Decrypt = nip44Decrypt;