@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/README.md +2 -2
- package/dist/index.cjs +321 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -26
- package/dist/index.d.ts +134 -26
- package/dist/index.js +316 -165
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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(
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
248
|
+
calculateFee(amount, config) {
|
|
249
|
+
assertConfig(config);
|
|
250
|
+
return calculateProtocolFee(amount, config.feeBps);
|
|
139
251
|
}
|
|
140
|
-
createPaymentRequest(recipientAddress, amount,
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
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 !==
|
|
324
|
+
if (fee_address !== treasury) {
|
|
215
325
|
return {
|
|
216
326
|
code: "fee_address_mismatch",
|
|
217
|
-
message: `Fee address mismatch: expected ${
|
|
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 (${
|
|
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 (${
|
|
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 ${
|
|
346
|
+
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
237
347
|
};
|
|
238
348
|
}
|
|
239
349
|
/**
|
|
240
|
-
* Build
|
|
241
|
-
* The caller
|
|
242
|
-
*
|
|
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(
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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(
|
|
294
|
-
|
|
295
|
-
|
|
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} (${
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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:
|
|
489
|
+
return { verified: false, error: verdict.reason };
|
|
416
490
|
} catch (err) {
|
|
417
491
|
lastError = err;
|
|
418
492
|
if (attempt < retries - 1) {
|
|
419
|
-
await
|
|
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(
|
|
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
|
|
507
|
+
const signatures = await rpc.getSignaturesForAddress(reference, {
|
|
434
508
|
limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
|
|
435
|
-
});
|
|
436
|
-
const validSigs = signatures.filter((
|
|
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
|
-
(
|
|
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
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
if (
|
|
464
|
-
|
|
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
|
|
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;
|