@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/README.md +2 -2
- package/dist/index.cjs +324 -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 +319 -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,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 !==
|
|
327
|
+
if (fee_address !== treasury) {
|
|
215
328
|
return {
|
|
216
329
|
code: "fee_address_mismatch",
|
|
217
|
-
message: `Fee address mismatch: expected ${
|
|
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 (${
|
|
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 (${
|
|
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 ${
|
|
349
|
+
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
237
350
|
};
|
|
238
351
|
}
|
|
239
352
|
/**
|
|
240
|
-
* Build
|
|
241
|
-
* The caller
|
|
242
|
-
*
|
|
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(
|
|
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
|
-
|
|
373
|
+
const treasury = config.treasury;
|
|
374
|
+
if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
|
|
256
375
|
throw new Error(
|
|
257
|
-
`Invalid fee address: expected ${
|
|
376
|
+
`Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
258
377
|
);
|
|
259
378
|
}
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
|
|
271
|
-
|
|
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(
|
|
294
|
-
|
|
295
|
-
|
|
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} (${
|
|
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 !==
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
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
|
|
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
|
|
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
|
-
}
|
|
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:
|
|
492
|
+
return { verified: false, error: verdict.reason };
|
|
416
493
|
} catch (err) {
|
|
417
494
|
lastError = err;
|
|
418
495
|
if (attempt < retries - 1) {
|
|
419
|
-
await
|
|
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(
|
|
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
|
|
510
|
+
const signatures = await rpc.getSignaturesForAddress(reference, {
|
|
434
511
|
limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
|
|
435
|
-
});
|
|
436
|
-
const validSigs = signatures.filter((
|
|
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
|
-
(
|
|
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
|
|
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;
|
|
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
|
|
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;
|