@elisym/sdk 0.3.2 → 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 +352 -166
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +166 -26
- package/dist/index.d.ts +166 -26
- package/dist/index.js +347 -167
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTransferSolInstruction } from '@solana-program/system';
|
|
2
|
+
import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, getProgramDerivedAddress, assertAccountExists, isAddress, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
|
|
2
3
|
import Decimal2 from 'decimal.js-light';
|
|
3
4
|
import { verifyEvent, finalizeEvent, getPublicKey, nip19, generateSecretKey, SimplePool } from 'nostr-tools';
|
|
4
5
|
import * as nip44 from 'nostr-tools/nip44';
|
|
@@ -35,6 +36,16 @@ var KIND_PONG = 20201;
|
|
|
35
36
|
var LAMPORTS_PER_SOL = 1e9;
|
|
36
37
|
var PROTOCOL_FEE_BPS = 300;
|
|
37
38
|
var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
|
|
39
|
+
var PROTOCOL_PROGRAM_ID_DEVNET = "BrX1CRkSgvcjxBvc2bgc3QqgWjinusofDmeP7ZVxvwrE";
|
|
40
|
+
function getProtocolProgramId(cluster) {
|
|
41
|
+
switch (cluster) {
|
|
42
|
+
case "devnet":
|
|
43
|
+
case "localnet":
|
|
44
|
+
return PROTOCOL_PROGRAM_ID_DEVNET;
|
|
45
|
+
case "mainnet":
|
|
46
|
+
throw new Error("Protocol program is not deployed on mainnet yet");
|
|
47
|
+
}
|
|
48
|
+
}
|
|
38
49
|
var DEFAULTS = {
|
|
39
50
|
SUBSCRIPTION_TIMEOUT_MS: 12e4,
|
|
40
51
|
PING_TIMEOUT_MS: 15e3,
|
|
@@ -61,19 +72,100 @@ var LIMITS = {
|
|
|
61
72
|
MAX_AGENT_NAME_LENGTH: 64,
|
|
62
73
|
MAX_CAPABILITY_LENGTH: 64
|
|
63
74
|
};
|
|
75
|
+
function getConfigDecoder() {
|
|
76
|
+
return getStructDecoder([
|
|
77
|
+
["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
|
|
78
|
+
["version", getU8Decoder()],
|
|
79
|
+
["bump", getU8Decoder()],
|
|
80
|
+
["admin", getAddressDecoder()],
|
|
81
|
+
["pendingAdmin", getOptionDecoder(getAddressDecoder())],
|
|
82
|
+
["treasury", getAddressDecoder()],
|
|
83
|
+
["feeBps", getU16Decoder()],
|
|
84
|
+
["paused", getBooleanDecoder()],
|
|
85
|
+
["lastUpdated", getI64Decoder()],
|
|
86
|
+
["reserved", fixDecoderSize(getBytesDecoder(), 128)]
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
function decodeConfig(encodedAccount) {
|
|
90
|
+
return decodeAccount(
|
|
91
|
+
encodedAccount,
|
|
92
|
+
getConfigDecoder()
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
async function fetchConfig(rpc, address2, config) {
|
|
96
|
+
const maybeAccount = await fetchMaybeConfig(rpc, address2, config);
|
|
97
|
+
assertAccountExists(maybeAccount);
|
|
98
|
+
return maybeAccount;
|
|
99
|
+
}
|
|
100
|
+
async function fetchMaybeConfig(rpc, address2, config) {
|
|
101
|
+
const maybeAccount = await fetchEncodedAccount(rpc, address2, config);
|
|
102
|
+
return decodeConfig(maybeAccount);
|
|
103
|
+
}
|
|
104
|
+
if (process.env.NODE_ENV !== "production") ;
|
|
105
|
+
var CONFIG_SEED = "config";
|
|
106
|
+
async function deriveConfigAddress(programId) {
|
|
107
|
+
const [pda] = await getProgramDerivedAddress({
|
|
108
|
+
programAddress: programId,
|
|
109
|
+
seeds: [new TextEncoder().encode(CONFIG_SEED)]
|
|
110
|
+
});
|
|
111
|
+
return pda;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/config/onchain.ts
|
|
115
|
+
var CACHE_TTL_MS = 6e4;
|
|
116
|
+
var cache = /* @__PURE__ */ new Map();
|
|
117
|
+
function clearProtocolConfigCache() {
|
|
118
|
+
cache.clear();
|
|
119
|
+
}
|
|
120
|
+
async function getProtocolConfig(rpc, programId, options) {
|
|
121
|
+
const key = programId.toString();
|
|
122
|
+
const ttl = options?.ttlMs ?? CACHE_TTL_MS;
|
|
123
|
+
const cached = cache.get(key);
|
|
124
|
+
if (!options?.forceRefresh && cached && Date.now() < cached.expires) {
|
|
125
|
+
return { ...cached.config, source: "cache" };
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const configPda = await deriveConfigAddress(programId);
|
|
129
|
+
const account = await fetchConfig(rpc, configPda);
|
|
130
|
+
const data = account.data;
|
|
131
|
+
const config = {
|
|
132
|
+
programId,
|
|
133
|
+
feeBps: data.feeBps,
|
|
134
|
+
treasury: data.treasury,
|
|
135
|
+
admin: data.admin,
|
|
136
|
+
pendingAdmin: data.pendingAdmin.__option === "Some" ? data.pendingAdmin.value : null,
|
|
137
|
+
paused: data.paused,
|
|
138
|
+
version: data.version,
|
|
139
|
+
source: "onchain"
|
|
140
|
+
};
|
|
141
|
+
cache.set(key, { config, expires: Date.now() + ttl });
|
|
142
|
+
return config;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (cached) {
|
|
145
|
+
return { ...cached.config, source: "cache" };
|
|
146
|
+
}
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Failed to fetch protocol config from on-chain program ${programId} and no cached value exists. Ensure RPC is reachable and the program is initialized. Cause: ${error instanceof Error ? error.message : String(error)}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var BPS_DENOMINATOR = 1e4;
|
|
64
153
|
function assertLamports(value, field) {
|
|
65
154
|
if (!Number.isInteger(value) || value < 0) {
|
|
66
155
|
throw new Error(`Invalid ${field}: ${value}. Must be a non-negative integer.`);
|
|
67
156
|
}
|
|
68
157
|
}
|
|
69
|
-
function calculateProtocolFee(amount) {
|
|
158
|
+
function calculateProtocolFee(amount, feeBps) {
|
|
159
|
+
if (!Number.isInteger(feeBps) || feeBps < 0) {
|
|
160
|
+
throw new Error(`Invalid feeBps: ${feeBps}. Must be a non-negative integer.`);
|
|
161
|
+
}
|
|
70
162
|
if (!Number.isInteger(amount) || amount < 0) {
|
|
71
163
|
throw new Error(`Invalid fee amount: ${amount}. Must be a non-negative integer.`);
|
|
72
164
|
}
|
|
73
|
-
if (amount === 0) {
|
|
165
|
+
if (amount === 0 || feeBps === 0) {
|
|
74
166
|
return 0;
|
|
75
167
|
}
|
|
76
|
-
return new Decimal2(amount).mul(
|
|
168
|
+
return new Decimal2(amount).mul(feeBps).div(BPS_DENOMINATOR).toDecimalPlaces(0, Decimal2.ROUND_CEIL).toNumber();
|
|
77
169
|
}
|
|
78
170
|
function validateExpiry(createdAt, expirySecs) {
|
|
79
171
|
if (!Number.isInteger(createdAt) || createdAt <= 0) {
|
|
@@ -99,47 +191,64 @@ function assertExpiry(createdAt, expirySecs) {
|
|
|
99
191
|
}
|
|
100
192
|
|
|
101
193
|
// src/payment/solana.ts
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
194
|
+
var REFERENCE_BYTE_LENGTH = 32;
|
|
195
|
+
function isValidSolanaAddress(value) {
|
|
196
|
+
return isAddress(value);
|
|
197
|
+
}
|
|
198
|
+
function generateReference() {
|
|
199
|
+
const bytes = new Uint8Array(REFERENCE_BYTE_LENGTH);
|
|
200
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
201
|
+
return getAddressDecoder().decode(bytes);
|
|
202
|
+
}
|
|
203
|
+
function assertReference(reference) {
|
|
204
|
+
if (!isValidSolanaAddress(reference)) {
|
|
205
|
+
throw new Error(`Invalid reference address: ${reference}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function assertExpirySecs(expirySecs) {
|
|
209
|
+
if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
|
|
210
|
+
throw new Error(`Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function assertConfig(config) {
|
|
214
|
+
if (!Number.isInteger(config.feeBps) || config.feeBps < 0) {
|
|
215
|
+
throw new Error(`Invalid feeBps: ${config.feeBps}. Must be a non-negative integer.`);
|
|
216
|
+
}
|
|
217
|
+
if (typeof config.treasury !== "string" || !isValidSolanaAddress(config.treasury)) {
|
|
218
|
+
throw new Error(`Invalid treasury address: ${String(config.treasury)}`);
|
|
108
219
|
}
|
|
109
220
|
}
|
|
110
221
|
var SolanaPaymentStrategy = class {
|
|
111
222
|
chain = "solana";
|
|
112
|
-
calculateFee(amount) {
|
|
113
|
-
|
|
223
|
+
calculateFee(amount, config) {
|
|
224
|
+
assertConfig(config);
|
|
225
|
+
return calculateProtocolFee(amount, config.feeBps);
|
|
114
226
|
}
|
|
115
|
-
createPaymentRequest(recipientAddress, amount,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} catch {
|
|
227
|
+
createPaymentRequest(recipientAddress, amount, config, options) {
|
|
228
|
+
assertConfig(config);
|
|
229
|
+
if (!isValidSolanaAddress(recipientAddress)) {
|
|
119
230
|
throw new Error(`Invalid Solana address: ${recipientAddress}`);
|
|
120
231
|
}
|
|
121
232
|
assertLamports(amount, "payment amount");
|
|
122
233
|
if (amount === 0) {
|
|
123
234
|
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
124
235
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
const feeAmount = calculateProtocolFee(amount);
|
|
131
|
-
const reference = Keypair.generate().publicKey.toBase58();
|
|
236
|
+
const expirySecs = options?.expirySecs ?? DEFAULTS.PAYMENT_EXPIRY_SECS;
|
|
237
|
+
assertExpirySecs(expirySecs);
|
|
238
|
+
const feeAmount = calculateProtocolFee(amount, config.feeBps);
|
|
239
|
+
const reference = generateReference();
|
|
132
240
|
return {
|
|
133
241
|
recipient: recipientAddress,
|
|
134
242
|
amount,
|
|
135
243
|
reference,
|
|
136
|
-
fee_address:
|
|
244
|
+
fee_address: config.treasury,
|
|
137
245
|
fee_amount: feeAmount,
|
|
138
246
|
created_at: Math.floor(Date.now() / 1e3),
|
|
139
247
|
expiry_secs: expirySecs
|
|
140
248
|
};
|
|
141
249
|
}
|
|
142
|
-
validatePaymentRequest(requestJson, expectedRecipient) {
|
|
250
|
+
validatePaymentRequest(requestJson, config, expectedRecipient) {
|
|
251
|
+
assertConfig(config);
|
|
143
252
|
let data;
|
|
144
253
|
try {
|
|
145
254
|
data = JSON.parse(requestJson);
|
|
@@ -181,21 +290,22 @@ var SolanaPaymentStrategy = class {
|
|
|
181
290
|
const code = expiryError.includes("future") ? "future_timestamp" : "expired";
|
|
182
291
|
return { code, message: expiryError };
|
|
183
292
|
}
|
|
184
|
-
const expectedFee = calculateProtocolFee(data.amount);
|
|
293
|
+
const expectedFee = calculateProtocolFee(data.amount, config.feeBps);
|
|
294
|
+
const treasury = config.treasury;
|
|
185
295
|
const { fee_address, fee_amount } = data;
|
|
186
296
|
const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
|
|
187
297
|
const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
|
|
188
298
|
if (hasFeeAddress && hasFeeAmount) {
|
|
189
|
-
if (fee_address !==
|
|
299
|
+
if (fee_address !== treasury) {
|
|
190
300
|
return {
|
|
191
301
|
code: "fee_address_mismatch",
|
|
192
|
-
message: `Fee address mismatch: expected ${
|
|
302
|
+
message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
|
|
193
303
|
};
|
|
194
304
|
}
|
|
195
305
|
if (fee_amount !== expectedFee) {
|
|
196
306
|
return {
|
|
197
307
|
code: "fee_amount_mismatch",
|
|
198
|
-
message: `Fee amount mismatch: expected ${expectedFee} lamports (${
|
|
308
|
+
message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
|
|
199
309
|
};
|
|
200
310
|
}
|
|
201
311
|
return null;
|
|
@@ -203,20 +313,24 @@ var SolanaPaymentStrategy = class {
|
|
|
203
313
|
if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
|
|
204
314
|
return {
|
|
205
315
|
code: "missing_fee",
|
|
206
|
-
message: `Payment request missing protocol fee (${
|
|
316
|
+
message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
207
317
|
};
|
|
208
318
|
}
|
|
209
319
|
return {
|
|
210
320
|
code: "invalid_fee_params",
|
|
211
|
-
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${
|
|
321
|
+
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
212
322
|
};
|
|
213
323
|
}
|
|
214
324
|
/**
|
|
215
|
-
* Build
|
|
216
|
-
* The caller
|
|
217
|
-
*
|
|
325
|
+
* Build, sign, and return a transaction for the supplied payment request.
|
|
326
|
+
* The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
|
|
327
|
+
*
|
|
328
|
+
* The provider transfer instruction includes the payment reference as a
|
|
329
|
+
* read-only, non-signer account so providers can detect the payment via
|
|
330
|
+
* `getSignaturesForAddress(reference)`.
|
|
218
331
|
*/
|
|
219
|
-
async buildTransaction(
|
|
332
|
+
async buildTransaction(paymentRequest, payerSigner, rpc, config) {
|
|
333
|
+
assertConfig(config);
|
|
220
334
|
assertLamports(paymentRequest.amount, "payment amount");
|
|
221
335
|
if (paymentRequest.amount === 0) {
|
|
222
336
|
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
@@ -226,50 +340,32 @@ var SolanaPaymentStrategy = class {
|
|
|
226
340
|
`Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
|
|
227
341
|
);
|
|
228
342
|
}
|
|
343
|
+
assertReference(paymentRequest.reference);
|
|
229
344
|
assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
`Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
const payerPubkey = new PublicKey(payerAddress);
|
|
236
|
-
const recipient = new PublicKey(paymentRequest.recipient);
|
|
237
|
-
const reference = new PublicKey(paymentRequest.reference);
|
|
238
|
-
const feeAddress = paymentRequest.fee_address ? new PublicKey(paymentRequest.fee_address) : null;
|
|
239
|
-
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
240
|
-
const providerAmount = feeAddress && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
|
|
241
|
-
if (providerAmount <= 0) {
|
|
345
|
+
const treasury = config.treasury;
|
|
346
|
+
if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
|
|
242
347
|
throw new Error(
|
|
243
|
-
`
|
|
348
|
+
`Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
244
349
|
);
|
|
245
350
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
tx.add(
|
|
259
|
-
SystemProgram.transfer({
|
|
260
|
-
fromPubkey: payerPubkey,
|
|
261
|
-
toPubkey: feeAddress,
|
|
262
|
-
lamports: feeAmount
|
|
263
|
-
})
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
return tx;
|
|
351
|
+
const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
|
|
352
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
353
|
+
const message = pipe(
|
|
354
|
+
createTransactionMessage({ version: 0 }),
|
|
355
|
+
(m) => setTransactionMessageFeePayerSigner(payerSigner, m),
|
|
356
|
+
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
|
|
357
|
+
(m) => appendTransactionMessageInstructions(
|
|
358
|
+
instructions,
|
|
359
|
+
m
|
|
360
|
+
)
|
|
361
|
+
);
|
|
362
|
+
return signTransactionMessageWithSigners(message);
|
|
267
363
|
}
|
|
268
|
-
async verifyPayment(
|
|
269
|
-
|
|
270
|
-
|
|
364
|
+
async verifyPayment(rpc, paymentRequest, config, options) {
|
|
365
|
+
assertConfig(config);
|
|
366
|
+
if (!rpc || typeof rpc.getTransaction !== "function") {
|
|
367
|
+
return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
|
|
271
368
|
}
|
|
272
|
-
const conn = connection;
|
|
273
369
|
if (!paymentRequest.reference || !paymentRequest.recipient) {
|
|
274
370
|
return { verified: false, error: "Missing required fields in payment request" };
|
|
275
371
|
}
|
|
@@ -285,19 +381,20 @@ var SolanaPaymentStrategy = class {
|
|
|
285
381
|
error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
|
|
286
382
|
};
|
|
287
383
|
}
|
|
288
|
-
const expectedFee = calculateProtocolFee(paymentRequest.amount);
|
|
384
|
+
const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
|
|
289
385
|
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
386
|
+
const treasury = config.treasury;
|
|
290
387
|
if (expectedFee > 0) {
|
|
291
388
|
if (feeAmount < expectedFee) {
|
|
292
389
|
return {
|
|
293
390
|
verified: false,
|
|
294
|
-
error: `Protocol fee ${feeAmount} below required ${expectedFee} (${
|
|
391
|
+
error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
|
|
295
392
|
};
|
|
296
393
|
}
|
|
297
394
|
if (!paymentRequest.fee_address) {
|
|
298
395
|
return { verified: false, error: "Missing fee address in payment request" };
|
|
299
396
|
}
|
|
300
|
-
if (paymentRequest.fee_address !==
|
|
397
|
+
if (paymentRequest.fee_address !== treasury) {
|
|
301
398
|
return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
|
|
302
399
|
}
|
|
303
400
|
}
|
|
@@ -310,10 +407,11 @@ var SolanaPaymentStrategy = class {
|
|
|
310
407
|
}
|
|
311
408
|
if (options?.txSignature) {
|
|
312
409
|
return this._verifyBySignature(
|
|
313
|
-
|
|
410
|
+
rpc,
|
|
314
411
|
options.txSignature,
|
|
315
412
|
paymentRequest.reference,
|
|
316
413
|
paymentRequest.recipient,
|
|
414
|
+
treasury,
|
|
317
415
|
expectedNet,
|
|
318
416
|
feeAmount,
|
|
319
417
|
options?.retries ?? DEFAULTS.VERIFY_RETRIES,
|
|
@@ -321,26 +419,28 @@ var SolanaPaymentStrategy = class {
|
|
|
321
419
|
);
|
|
322
420
|
}
|
|
323
421
|
return this._verifyByReference(
|
|
324
|
-
|
|
422
|
+
rpc,
|
|
325
423
|
paymentRequest.reference,
|
|
326
424
|
paymentRequest.recipient,
|
|
425
|
+
treasury,
|
|
327
426
|
expectedNet,
|
|
328
427
|
feeAmount,
|
|
329
428
|
options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
|
|
330
429
|
options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
|
|
331
430
|
);
|
|
332
431
|
}
|
|
333
|
-
async _verifyBySignature(
|
|
432
|
+
async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
334
433
|
let lastError;
|
|
335
434
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
336
435
|
try {
|
|
337
|
-
const tx = await
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
436
|
+
const tx = await rpc.getTransaction(txSignature, {
|
|
437
|
+
commitment: "confirmed",
|
|
438
|
+
encoding: "json",
|
|
439
|
+
maxSupportedTransactionVersion: 0
|
|
440
|
+
}).send();
|
|
341
441
|
if (!tx?.meta || tx.meta.err) {
|
|
342
442
|
if (attempt < retries - 1) {
|
|
343
|
-
await
|
|
443
|
+
await waitMs(intervalMs);
|
|
344
444
|
continue;
|
|
345
445
|
}
|
|
346
446
|
return {
|
|
@@ -348,50 +448,24 @@ var SolanaPaymentStrategy = class {
|
|
|
348
448
|
error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
|
|
349
449
|
};
|
|
350
450
|
}
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
error: "Reference key not found in transaction - possible replay"
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
const recipientIdx = keyToIdx.get(recipientAddress);
|
|
367
|
-
if (recipientIdx === void 0) {
|
|
368
|
-
return { verified: false, error: "Recipient not found in transaction" };
|
|
369
|
-
}
|
|
370
|
-
const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
|
|
371
|
-
if (recipientDelta < expectedNet) {
|
|
372
|
-
return {
|
|
373
|
-
verified: false,
|
|
374
|
-
error: `Recipient received ${recipientDelta}, expected >= ${expectedNet}`
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
if (expectedFee > 0) {
|
|
378
|
-
const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
|
|
379
|
-
if (treasuryIdx === void 0) {
|
|
380
|
-
return { verified: false, error: "Treasury not found in transaction" };
|
|
381
|
-
}
|
|
382
|
-
const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
|
|
383
|
-
if (treasuryDelta < expectedFee) {
|
|
384
|
-
return {
|
|
385
|
-
verified: false,
|
|
386
|
-
error: `Treasury received ${treasuryDelta}, expected >= ${expectedFee}`
|
|
387
|
-
};
|
|
388
|
-
}
|
|
451
|
+
const verdict = checkBalanceDiff({
|
|
452
|
+
accountKeys: tx.transaction.message.accountKeys,
|
|
453
|
+
preBalances: tx.meta.preBalances,
|
|
454
|
+
postBalances: tx.meta.postBalances,
|
|
455
|
+
referenceKey,
|
|
456
|
+
recipientAddress,
|
|
457
|
+
treasuryAddress,
|
|
458
|
+
expectedNet,
|
|
459
|
+
expectedFee
|
|
460
|
+
});
|
|
461
|
+
if (verdict.ok) {
|
|
462
|
+
return { verified: true, txSignature };
|
|
389
463
|
}
|
|
390
|
-
return { verified:
|
|
464
|
+
return { verified: false, error: verdict.reason };
|
|
391
465
|
} catch (err) {
|
|
392
466
|
lastError = err;
|
|
393
467
|
if (attempt < retries - 1) {
|
|
394
|
-
await
|
|
468
|
+
await waitMs(intervalMs);
|
|
395
469
|
}
|
|
396
470
|
}
|
|
397
471
|
}
|
|
@@ -400,66 +474,50 @@ var SolanaPaymentStrategy = class {
|
|
|
400
474
|
error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
|
|
401
475
|
};
|
|
402
476
|
}
|
|
403
|
-
async _verifyByReference(
|
|
404
|
-
const reference = new PublicKey(referenceKey);
|
|
477
|
+
async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
405
478
|
let lastError;
|
|
479
|
+
const reference = address(referenceKey);
|
|
406
480
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
407
481
|
try {
|
|
408
|
-
const signatures = await
|
|
482
|
+
const signatures = await rpc.getSignaturesForAddress(reference, {
|
|
409
483
|
limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
|
|
410
|
-
});
|
|
411
|
-
const validSigs = signatures.filter((
|
|
484
|
+
}).send();
|
|
485
|
+
const validSigs = signatures.filter((entry) => !entry.err);
|
|
412
486
|
if (validSigs.length > 0) {
|
|
487
|
+
const fetchTransaction = (sig) => rpc.getTransaction(sig, {
|
|
488
|
+
commitment: "confirmed",
|
|
489
|
+
encoding: "json",
|
|
490
|
+
maxSupportedTransactionVersion: 0
|
|
491
|
+
}).send();
|
|
413
492
|
const txResults = await Promise.all(
|
|
414
493
|
validSigs.map(
|
|
415
|
-
(
|
|
416
|
-
maxSupportedTransactionVersion: 0,
|
|
417
|
-
commitment: "confirmed"
|
|
418
|
-
}).then((tx) => ({ sig: s.signature, tx })).catch(() => ({
|
|
419
|
-
sig: s.signature,
|
|
420
|
-
tx: null
|
|
421
|
-
}))
|
|
494
|
+
(entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
|
|
422
495
|
)
|
|
423
496
|
);
|
|
424
497
|
for (const { sig, tx } of txResults) {
|
|
425
498
|
if (!tx?.meta || tx.meta.err) {
|
|
426
499
|
continue;
|
|
427
500
|
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
501
|
+
const verdict = checkBalanceDiff({
|
|
502
|
+
accountKeys: tx.transaction.message.accountKeys,
|
|
503
|
+
preBalances: tx.meta.preBalances,
|
|
504
|
+
postBalances: tx.meta.postBalances,
|
|
505
|
+
referenceKey,
|
|
506
|
+
recipientAddress,
|
|
507
|
+
treasuryAddress,
|
|
508
|
+
expectedNet,
|
|
509
|
+
expectedFee
|
|
510
|
+
});
|
|
511
|
+
if (verdict.ok) {
|
|
512
|
+
return { verified: true, txSignature: sig };
|
|
436
513
|
}
|
|
437
|
-
const recipientIdx = keyToIdx.get(recipientAddress);
|
|
438
|
-
if (recipientIdx === void 0) {
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
|
|
442
|
-
if (recipientDelta < expectedNet) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (expectedFee > 0) {
|
|
446
|
-
const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
|
|
447
|
-
if (treasuryIdx === void 0) {
|
|
448
|
-
continue;
|
|
449
|
-
}
|
|
450
|
-
const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
|
|
451
|
-
if (treasuryDelta < expectedFee) {
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return { verified: true, txSignature: sig };
|
|
456
514
|
}
|
|
457
515
|
}
|
|
458
516
|
} catch (err) {
|
|
459
517
|
lastError = err;
|
|
460
518
|
}
|
|
461
519
|
if (attempt < retries - 1) {
|
|
462
|
-
await
|
|
520
|
+
await waitMs(intervalMs);
|
|
463
521
|
}
|
|
464
522
|
}
|
|
465
523
|
return {
|
|
@@ -468,6 +526,99 @@ var SolanaPaymentStrategy = class {
|
|
|
468
526
|
};
|
|
469
527
|
}
|
|
470
528
|
};
|
|
529
|
+
function checkBalanceDiff(input) {
|
|
530
|
+
const balanceCount = input.preBalances.length;
|
|
531
|
+
const keyToIdx = /* @__PURE__ */ new Map();
|
|
532
|
+
for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
|
|
533
|
+
const key = input.accountKeys[i];
|
|
534
|
+
if (key) {
|
|
535
|
+
keyToIdx.set(String(key), i);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (!keyToIdx.has(input.referenceKey)) {
|
|
539
|
+
return { ok: false, reason: "Reference key not found in transaction - possible replay" };
|
|
540
|
+
}
|
|
541
|
+
const recipientIdx = keyToIdx.get(input.recipientAddress);
|
|
542
|
+
if (recipientIdx === void 0) {
|
|
543
|
+
return { ok: false, reason: "Recipient not found in transaction" };
|
|
544
|
+
}
|
|
545
|
+
const recipientDelta = bigIntDelta(
|
|
546
|
+
input.postBalances[recipientIdx],
|
|
547
|
+
input.preBalances[recipientIdx]
|
|
548
|
+
);
|
|
549
|
+
if (recipientDelta < BigInt(input.expectedNet)) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
if (input.expectedFee > 0) {
|
|
556
|
+
const treasuryIdx = keyToIdx.get(input.treasuryAddress);
|
|
557
|
+
if (treasuryIdx === void 0) {
|
|
558
|
+
return { ok: false, reason: "Treasury not found in transaction" };
|
|
559
|
+
}
|
|
560
|
+
const treasuryDelta = bigIntDelta(
|
|
561
|
+
input.postBalances[treasuryIdx],
|
|
562
|
+
input.preBalances[treasuryIdx]
|
|
563
|
+
);
|
|
564
|
+
if (treasuryDelta < BigInt(input.expectedFee)) {
|
|
565
|
+
return {
|
|
566
|
+
ok: false,
|
|
567
|
+
reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return { ok: true };
|
|
572
|
+
}
|
|
573
|
+
function bigIntDelta(post, pre) {
|
|
574
|
+
const postValue = post === void 0 ? 0n : BigInt(post);
|
|
575
|
+
const preValue = pre === void 0 ? 0n : BigInt(pre);
|
|
576
|
+
return postValue - preValue;
|
|
577
|
+
}
|
|
578
|
+
function waitMs(ms) {
|
|
579
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
580
|
+
}
|
|
581
|
+
function buildPaymentInstructions(paymentRequest, payerSigner) {
|
|
582
|
+
const recipient = address(paymentRequest.recipient);
|
|
583
|
+
const reference = address(paymentRequest.reference);
|
|
584
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
585
|
+
const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
|
|
586
|
+
if (providerAmount <= 0) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const providerTransferIx = getTransferSolInstruction({
|
|
592
|
+
source: payerSigner,
|
|
593
|
+
destination: recipient,
|
|
594
|
+
amount: BigInt(providerAmount)
|
|
595
|
+
});
|
|
596
|
+
const providerTransferIxWithReference = {
|
|
597
|
+
...providerTransferIx,
|
|
598
|
+
accounts: [...providerTransferIx.accounts, { address: reference, role: AccountRole.READONLY }]
|
|
599
|
+
};
|
|
600
|
+
const instructions = [providerTransferIxWithReference];
|
|
601
|
+
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
602
|
+
instructions.push(
|
|
603
|
+
getTransferSolInstruction({
|
|
604
|
+
source: payerSigner,
|
|
605
|
+
destination: address(paymentRequest.fee_address),
|
|
606
|
+
amount: BigInt(feeAmount)
|
|
607
|
+
})
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return instructions;
|
|
611
|
+
}
|
|
612
|
+
async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
|
|
613
|
+
const config = await getProtocolConfig(rpc, programId);
|
|
614
|
+
const strategy = new SolanaPaymentStrategy();
|
|
615
|
+
return strategy.createPaymentRequest(
|
|
616
|
+
recipient,
|
|
617
|
+
amount,
|
|
618
|
+
{ feeBps: config.feeBps, treasury: config.treasury },
|
|
619
|
+
options
|
|
620
|
+
);
|
|
621
|
+
}
|
|
471
622
|
function toDTag(name) {
|
|
472
623
|
const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
|
|
473
624
|
if (!tag) {
|
|
@@ -1571,12 +1722,18 @@ var PingService = class _PingService {
|
|
|
1571
1722
|
constructor(pool) {
|
|
1572
1723
|
this.pool = pool;
|
|
1573
1724
|
this.sessionIdentity = ElisymIdentity.generate();
|
|
1725
|
+
pool.onReset(() => this.clearCache());
|
|
1574
1726
|
}
|
|
1575
1727
|
static PING_CACHE_MAX = 1e3;
|
|
1576
1728
|
sessionIdentity;
|
|
1577
1729
|
pingCache = /* @__PURE__ */ new Map();
|
|
1578
1730
|
// pubkey - timestamp of last online result
|
|
1579
1731
|
pendingPings = /* @__PURE__ */ new Map();
|
|
1732
|
+
/** Drop cached online results. In-flight pings are left alone - they'll
|
|
1733
|
+
* resolve via their own timeouts and remove themselves from `pendingPings`. */
|
|
1734
|
+
clearCache() {
|
|
1735
|
+
this.pingCache.clear();
|
|
1736
|
+
}
|
|
1580
1737
|
/**
|
|
1581
1738
|
* Ping an agent via ephemeral Nostr events (kind 20200/20201).
|
|
1582
1739
|
* Uses a persistent session identity to avoid relay rate-limiting.
|
|
@@ -1775,10 +1932,28 @@ var NostrPool = class {
|
|
|
1775
1932
|
pool;
|
|
1776
1933
|
relays;
|
|
1777
1934
|
activeSubscriptions = /* @__PURE__ */ new Set();
|
|
1935
|
+
resetListeners = /* @__PURE__ */ new Set();
|
|
1778
1936
|
constructor(relays = RELAYS) {
|
|
1779
1937
|
this.pool = new SimplePool();
|
|
1780
1938
|
this.relays = relays;
|
|
1781
1939
|
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Register a callback to run after `reset()` completes (new SimplePool in place).
|
|
1942
|
+
* Services that cache pool-derived state (e.g. ping results) must clear it here,
|
|
1943
|
+
* otherwise stale values survive the reconnect. Returns an unsubscribe function.
|
|
1944
|
+
*
|
|
1945
|
+
* Contract:
|
|
1946
|
+
* - Listeners are invoked **synchronously** at the end of `reset()`, in
|
|
1947
|
+
* registration order. Do not rely on async work inside a listener having
|
|
1948
|
+
* completed before `reset()` returns.
|
|
1949
|
+
* - Listener exceptions are caught and swallowed so that one faulty listener
|
|
1950
|
+
* cannot prevent the others from running (or abort the reset itself).
|
|
1951
|
+
* If a listener needs to surface errors, it must do so out-of-band.
|
|
1952
|
+
*/
|
|
1953
|
+
onReset(listener) {
|
|
1954
|
+
this.resetListeners.add(listener);
|
|
1955
|
+
return () => this.resetListeners.delete(listener);
|
|
1956
|
+
}
|
|
1782
1957
|
/** Query relays synchronously. Returns `[]` on timeout (no error thrown). */
|
|
1783
1958
|
async querySync(filter) {
|
|
1784
1959
|
let timer;
|
|
@@ -1786,13 +1961,12 @@ var NostrPool = class {
|
|
|
1786
1961
|
query.catch(() => {
|
|
1787
1962
|
});
|
|
1788
1963
|
try {
|
|
1789
|
-
|
|
1964
|
+
return await Promise.race([
|
|
1790
1965
|
query,
|
|
1791
1966
|
new Promise((resolve) => {
|
|
1792
1967
|
timer = setTimeout(() => resolve([]), DEFAULTS.QUERY_TIMEOUT_MS);
|
|
1793
1968
|
})
|
|
1794
1969
|
]);
|
|
1795
|
-
return result;
|
|
1796
1970
|
} finally {
|
|
1797
1971
|
clearTimeout(timer);
|
|
1798
1972
|
}
|
|
@@ -1974,6 +2148,12 @@ var NostrPool = class {
|
|
|
1974
2148
|
} catch {
|
|
1975
2149
|
}
|
|
1976
2150
|
this.pool = new SimplePool();
|
|
2151
|
+
for (const listener of this.resetListeners) {
|
|
2152
|
+
try {
|
|
2153
|
+
listener();
|
|
2154
|
+
} catch {
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
1977
2157
|
}
|
|
1978
2158
|
/**
|
|
1979
2159
|
* Lightweight connectivity probe. Returns true if at least one relay responds.
|
|
@@ -2091,6 +2271,6 @@ function serializeConfig(config) {
|
|
|
2091
2271
|
return JSON.stringify(config, null, 2) + "\n";
|
|
2092
2272
|
}
|
|
2093
2273
|
|
|
2094
|
-
export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DiscoveryService, ElisymClient, ElisymIdentity, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_TREASURY, PingService, RELAYS, SolanaPaymentStrategy, assertExpiry, assertLamports, calculateProtocolFee, formatSol, jobRequestKind, jobResultKind, nip44Decrypt, nip44Encrypt, serializeConfig, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
|
|
2274
|
+
export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DiscoveryService, ElisymClient, ElisymIdentity, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PingService, RELAYS, SolanaPaymentStrategy, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, nip44Decrypt, nip44Encrypt, serializeConfig, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
|
|
2095
2275
|
//# sourceMappingURL=index.js.map
|
|
2096
2276
|
//# sourceMappingURL=index.js.map
|