@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.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,25 @@ 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;
|
|
295
|
+
if (expectedFee === 0) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
185
298
|
const { fee_address, fee_amount } = data;
|
|
186
299
|
const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
|
|
187
300
|
const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
|
|
188
301
|
if (hasFeeAddress && hasFeeAmount) {
|
|
189
|
-
if (fee_address !==
|
|
302
|
+
if (fee_address !== treasury) {
|
|
190
303
|
return {
|
|
191
304
|
code: "fee_address_mismatch",
|
|
192
|
-
message: `Fee address mismatch: expected ${
|
|
305
|
+
message: `Fee address mismatch: expected ${treasury}, got ${fee_address}. Provider may be attempting to redirect fees.`
|
|
193
306
|
};
|
|
194
307
|
}
|
|
195
308
|
if (fee_amount !== expectedFee) {
|
|
196
309
|
return {
|
|
197
310
|
code: "fee_amount_mismatch",
|
|
198
|
-
message: `Fee amount mismatch: expected ${expectedFee} lamports (${
|
|
311
|
+
message: `Fee amount mismatch: expected ${expectedFee} lamports (${config.feeBps}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
|
|
199
312
|
};
|
|
200
313
|
}
|
|
201
314
|
return null;
|
|
@@ -203,20 +316,24 @@ var SolanaPaymentStrategy = class {
|
|
|
203
316
|
if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
|
|
204
317
|
return {
|
|
205
318
|
code: "missing_fee",
|
|
206
|
-
message: `Payment request missing protocol fee (${
|
|
319
|
+
message: `Payment request missing protocol fee (${config.feeBps}bps). Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
207
320
|
};
|
|
208
321
|
}
|
|
209
322
|
return {
|
|
210
323
|
code: "invalid_fee_params",
|
|
211
|
-
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${
|
|
324
|
+
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${treasury}.`
|
|
212
325
|
};
|
|
213
326
|
}
|
|
214
327
|
/**
|
|
215
|
-
* Build
|
|
216
|
-
* The caller
|
|
217
|
-
*
|
|
328
|
+
* Build, sign, and return a transaction for the supplied payment request.
|
|
329
|
+
* The caller is responsible for sending it (e.g. via `rpc.sendTransaction`).
|
|
330
|
+
*
|
|
331
|
+
* The provider transfer instruction includes the payment reference as a
|
|
332
|
+
* read-only, non-signer account so providers can detect the payment via
|
|
333
|
+
* `getSignaturesForAddress(reference)`.
|
|
218
334
|
*/
|
|
219
|
-
async buildTransaction(
|
|
335
|
+
async buildTransaction(paymentRequest, payerSigner, rpc, config) {
|
|
336
|
+
assertConfig(config);
|
|
220
337
|
assertLamports(paymentRequest.amount, "payment amount");
|
|
221
338
|
if (paymentRequest.amount === 0) {
|
|
222
339
|
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
@@ -226,50 +343,32 @@ var SolanaPaymentStrategy = class {
|
|
|
226
343
|
`Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
|
|
227
344
|
);
|
|
228
345
|
}
|
|
346
|
+
assertReference(paymentRequest.reference);
|
|
229
347
|
assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
|
|
230
|
-
|
|
348
|
+
const treasury = config.treasury;
|
|
349
|
+
if (paymentRequest.fee_address && paymentRequest.fee_address !== treasury) {
|
|
231
350
|
throw new Error(
|
|
232
|
-
`Invalid fee address: expected ${
|
|
351
|
+
`Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
233
352
|
);
|
|
234
353
|
}
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
fromPubkey: payerPubkey,
|
|
248
|
-
toPubkey: recipient,
|
|
249
|
-
lamports: providerAmount
|
|
250
|
-
});
|
|
251
|
-
transferIx.keys.push({
|
|
252
|
-
pubkey: reference,
|
|
253
|
-
isSigner: false,
|
|
254
|
-
isWritable: false
|
|
255
|
-
});
|
|
256
|
-
const tx = new Transaction().add(transferIx);
|
|
257
|
-
if (feeAddress && feeAmount > 0) {
|
|
258
|
-
tx.add(
|
|
259
|
-
SystemProgram.transfer({
|
|
260
|
-
fromPubkey: payerPubkey,
|
|
261
|
-
toPubkey: feeAddress,
|
|
262
|
-
lamports: feeAmount
|
|
263
|
-
})
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
return tx;
|
|
354
|
+
const instructions = buildPaymentInstructions(paymentRequest, payerSigner);
|
|
355
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
356
|
+
const message = pipe(
|
|
357
|
+
createTransactionMessage({ version: 0 }),
|
|
358
|
+
(m) => setTransactionMessageFeePayerSigner(payerSigner, m),
|
|
359
|
+
(m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m),
|
|
360
|
+
(m) => appendTransactionMessageInstructions(
|
|
361
|
+
instructions,
|
|
362
|
+
m
|
|
363
|
+
)
|
|
364
|
+
);
|
|
365
|
+
return signTransactionMessageWithSigners(message);
|
|
267
366
|
}
|
|
268
|
-
async verifyPayment(
|
|
269
|
-
|
|
270
|
-
|
|
367
|
+
async verifyPayment(rpc, paymentRequest, config, options) {
|
|
368
|
+
assertConfig(config);
|
|
369
|
+
if (!rpc || typeof rpc.getTransaction !== "function") {
|
|
370
|
+
return { verified: false, error: "Invalid rpc: expected Solana Kit Rpc instance" };
|
|
271
371
|
}
|
|
272
|
-
const conn = connection;
|
|
273
372
|
if (!paymentRequest.reference || !paymentRequest.recipient) {
|
|
274
373
|
return { verified: false, error: "Missing required fields in payment request" };
|
|
275
374
|
}
|
|
@@ -285,19 +384,20 @@ var SolanaPaymentStrategy = class {
|
|
|
285
384
|
error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
|
|
286
385
|
};
|
|
287
386
|
}
|
|
288
|
-
const expectedFee = calculateProtocolFee(paymentRequest.amount);
|
|
387
|
+
const expectedFee = calculateProtocolFee(paymentRequest.amount, config.feeBps);
|
|
289
388
|
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
389
|
+
const treasury = config.treasury;
|
|
290
390
|
if (expectedFee > 0) {
|
|
291
391
|
if (feeAmount < expectedFee) {
|
|
292
392
|
return {
|
|
293
393
|
verified: false,
|
|
294
|
-
error: `Protocol fee ${feeAmount} below required ${expectedFee} (${
|
|
394
|
+
error: `Protocol fee ${feeAmount} below required ${expectedFee} (${config.feeBps}bps of ${paymentRequest.amount})`
|
|
295
395
|
};
|
|
296
396
|
}
|
|
297
397
|
if (!paymentRequest.fee_address) {
|
|
298
398
|
return { verified: false, error: "Missing fee address in payment request" };
|
|
299
399
|
}
|
|
300
|
-
if (paymentRequest.fee_address !==
|
|
400
|
+
if (paymentRequest.fee_address !== treasury) {
|
|
301
401
|
return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
|
|
302
402
|
}
|
|
303
403
|
}
|
|
@@ -310,10 +410,11 @@ var SolanaPaymentStrategy = class {
|
|
|
310
410
|
}
|
|
311
411
|
if (options?.txSignature) {
|
|
312
412
|
return this._verifyBySignature(
|
|
313
|
-
|
|
413
|
+
rpc,
|
|
314
414
|
options.txSignature,
|
|
315
415
|
paymentRequest.reference,
|
|
316
416
|
paymentRequest.recipient,
|
|
417
|
+
treasury,
|
|
317
418
|
expectedNet,
|
|
318
419
|
feeAmount,
|
|
319
420
|
options?.retries ?? DEFAULTS.VERIFY_RETRIES,
|
|
@@ -321,26 +422,28 @@ var SolanaPaymentStrategy = class {
|
|
|
321
422
|
);
|
|
322
423
|
}
|
|
323
424
|
return this._verifyByReference(
|
|
324
|
-
|
|
425
|
+
rpc,
|
|
325
426
|
paymentRequest.reference,
|
|
326
427
|
paymentRequest.recipient,
|
|
428
|
+
treasury,
|
|
327
429
|
expectedNet,
|
|
328
430
|
feeAmount,
|
|
329
431
|
options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
|
|
330
432
|
options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
|
|
331
433
|
);
|
|
332
434
|
}
|
|
333
|
-
async _verifyBySignature(
|
|
435
|
+
async _verifyBySignature(rpc, txSignature, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
334
436
|
let lastError;
|
|
335
437
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
336
438
|
try {
|
|
337
|
-
const tx = await
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
439
|
+
const tx = await rpc.getTransaction(txSignature, {
|
|
440
|
+
commitment: "confirmed",
|
|
441
|
+
encoding: "json",
|
|
442
|
+
maxSupportedTransactionVersion: 0
|
|
443
|
+
}).send();
|
|
341
444
|
if (!tx?.meta || tx.meta.err) {
|
|
342
445
|
if (attempt < retries - 1) {
|
|
343
|
-
await
|
|
446
|
+
await waitMs(intervalMs);
|
|
344
447
|
continue;
|
|
345
448
|
}
|
|
346
449
|
return {
|
|
@@ -348,50 +451,24 @@ var SolanaPaymentStrategy = class {
|
|
|
348
451
|
error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
|
|
349
452
|
};
|
|
350
453
|
}
|
|
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
|
-
}
|
|
454
|
+
const verdict = checkBalanceDiff({
|
|
455
|
+
accountKeys: tx.transaction.message.accountKeys,
|
|
456
|
+
preBalances: tx.meta.preBalances,
|
|
457
|
+
postBalances: tx.meta.postBalances,
|
|
458
|
+
referenceKey,
|
|
459
|
+
recipientAddress,
|
|
460
|
+
treasuryAddress,
|
|
461
|
+
expectedNet,
|
|
462
|
+
expectedFee
|
|
463
|
+
});
|
|
464
|
+
if (verdict.ok) {
|
|
465
|
+
return { verified: true, txSignature };
|
|
389
466
|
}
|
|
390
|
-
return { verified:
|
|
467
|
+
return { verified: false, error: verdict.reason };
|
|
391
468
|
} catch (err) {
|
|
392
469
|
lastError = err;
|
|
393
470
|
if (attempt < retries - 1) {
|
|
394
|
-
await
|
|
471
|
+
await waitMs(intervalMs);
|
|
395
472
|
}
|
|
396
473
|
}
|
|
397
474
|
}
|
|
@@ -400,66 +477,50 @@ var SolanaPaymentStrategy = class {
|
|
|
400
477
|
error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
|
|
401
478
|
};
|
|
402
479
|
}
|
|
403
|
-
async _verifyByReference(
|
|
404
|
-
const reference = new PublicKey(referenceKey);
|
|
480
|
+
async _verifyByReference(rpc, referenceKey, recipientAddress, treasuryAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
405
481
|
let lastError;
|
|
482
|
+
const reference = address(referenceKey);
|
|
406
483
|
for (let attempt = 0; attempt < retries; attempt++) {
|
|
407
484
|
try {
|
|
408
|
-
const signatures = await
|
|
485
|
+
const signatures = await rpc.getSignaturesForAddress(reference, {
|
|
409
486
|
limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
|
|
410
|
-
});
|
|
411
|
-
const validSigs = signatures.filter((
|
|
487
|
+
}).send();
|
|
488
|
+
const validSigs = signatures.filter((entry) => !entry.err);
|
|
412
489
|
if (validSigs.length > 0) {
|
|
490
|
+
const fetchTransaction = (sig) => rpc.getTransaction(sig, {
|
|
491
|
+
commitment: "confirmed",
|
|
492
|
+
encoding: "json",
|
|
493
|
+
maxSupportedTransactionVersion: 0
|
|
494
|
+
}).send();
|
|
413
495
|
const txResults = await Promise.all(
|
|
414
496
|
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
|
-
}))
|
|
497
|
+
(entry) => fetchTransaction(entry.signature).then((tx) => ({ sig: entry.signature, tx })).catch(() => ({ sig: entry.signature, tx: null }))
|
|
422
498
|
)
|
|
423
499
|
);
|
|
424
500
|
for (const { sig, tx } of txResults) {
|
|
425
501
|
if (!tx?.meta || tx.meta.err) {
|
|
426
502
|
continue;
|
|
427
503
|
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
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
|
-
}
|
|
504
|
+
const verdict = checkBalanceDiff({
|
|
505
|
+
accountKeys: tx.transaction.message.accountKeys,
|
|
506
|
+
preBalances: tx.meta.preBalances,
|
|
507
|
+
postBalances: tx.meta.postBalances,
|
|
508
|
+
referenceKey,
|
|
509
|
+
recipientAddress,
|
|
510
|
+
treasuryAddress,
|
|
511
|
+
expectedNet,
|
|
512
|
+
expectedFee
|
|
513
|
+
});
|
|
514
|
+
if (verdict.ok) {
|
|
515
|
+
return { verified: true, txSignature: sig };
|
|
454
516
|
}
|
|
455
|
-
return { verified: true, txSignature: sig };
|
|
456
517
|
}
|
|
457
518
|
}
|
|
458
519
|
} catch (err) {
|
|
459
520
|
lastError = err;
|
|
460
521
|
}
|
|
461
522
|
if (attempt < retries - 1) {
|
|
462
|
-
await
|
|
523
|
+
await waitMs(intervalMs);
|
|
463
524
|
}
|
|
464
525
|
}
|
|
465
526
|
return {
|
|
@@ -468,6 +529,99 @@ var SolanaPaymentStrategy = class {
|
|
|
468
529
|
};
|
|
469
530
|
}
|
|
470
531
|
};
|
|
532
|
+
function checkBalanceDiff(input) {
|
|
533
|
+
const balanceCount = input.preBalances.length;
|
|
534
|
+
const keyToIdx = /* @__PURE__ */ new Map();
|
|
535
|
+
for (let i = 0; i < Math.min(input.accountKeys.length, balanceCount); i++) {
|
|
536
|
+
const key = input.accountKeys[i];
|
|
537
|
+
if (key) {
|
|
538
|
+
keyToIdx.set(String(key), i);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!keyToIdx.has(input.referenceKey)) {
|
|
542
|
+
return { ok: false, reason: "Reference key not found in transaction - possible replay" };
|
|
543
|
+
}
|
|
544
|
+
const recipientIdx = keyToIdx.get(input.recipientAddress);
|
|
545
|
+
if (recipientIdx === void 0) {
|
|
546
|
+
return { ok: false, reason: "Recipient not found in transaction" };
|
|
547
|
+
}
|
|
548
|
+
const recipientDelta = bigIntDelta(
|
|
549
|
+
input.postBalances[recipientIdx],
|
|
550
|
+
input.preBalances[recipientIdx]
|
|
551
|
+
);
|
|
552
|
+
if (recipientDelta < BigInt(input.expectedNet)) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
reason: `Recipient received ${recipientDelta.toString()}, expected >= ${input.expectedNet}`
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
if (input.expectedFee > 0) {
|
|
559
|
+
const treasuryIdx = keyToIdx.get(input.treasuryAddress);
|
|
560
|
+
if (treasuryIdx === void 0) {
|
|
561
|
+
return { ok: false, reason: "Treasury not found in transaction" };
|
|
562
|
+
}
|
|
563
|
+
const treasuryDelta = bigIntDelta(
|
|
564
|
+
input.postBalances[treasuryIdx],
|
|
565
|
+
input.preBalances[treasuryIdx]
|
|
566
|
+
);
|
|
567
|
+
if (treasuryDelta < BigInt(input.expectedFee)) {
|
|
568
|
+
return {
|
|
569
|
+
ok: false,
|
|
570
|
+
reason: `Treasury received ${treasuryDelta.toString()}, expected >= ${input.expectedFee}`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return { ok: true };
|
|
575
|
+
}
|
|
576
|
+
function bigIntDelta(post, pre) {
|
|
577
|
+
const postValue = post === void 0 ? 0n : BigInt(post);
|
|
578
|
+
const preValue = pre === void 0 ? 0n : BigInt(pre);
|
|
579
|
+
return postValue - preValue;
|
|
580
|
+
}
|
|
581
|
+
function waitMs(ms) {
|
|
582
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
583
|
+
}
|
|
584
|
+
function buildPaymentInstructions(paymentRequest, payerSigner) {
|
|
585
|
+
const recipient = address(paymentRequest.recipient);
|
|
586
|
+
const reference = address(paymentRequest.reference);
|
|
587
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
588
|
+
const providerAmount = paymentRequest.fee_address && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
|
|
589
|
+
if (providerAmount <= 0) {
|
|
590
|
+
throw new Error(
|
|
591
|
+
`Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
const providerTransferIx = getTransferSolInstruction({
|
|
595
|
+
source: payerSigner,
|
|
596
|
+
destination: recipient,
|
|
597
|
+
amount: BigInt(providerAmount)
|
|
598
|
+
});
|
|
599
|
+
const providerTransferIxWithReference = {
|
|
600
|
+
...providerTransferIx,
|
|
601
|
+
accounts: [...providerTransferIx.accounts, { address: reference, role: AccountRole.READONLY }]
|
|
602
|
+
};
|
|
603
|
+
const instructions = [providerTransferIxWithReference];
|
|
604
|
+
if (paymentRequest.fee_address && feeAmount > 0) {
|
|
605
|
+
instructions.push(
|
|
606
|
+
getTransferSolInstruction({
|
|
607
|
+
source: payerSigner,
|
|
608
|
+
destination: address(paymentRequest.fee_address),
|
|
609
|
+
amount: BigInt(feeAmount)
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
return instructions;
|
|
614
|
+
}
|
|
615
|
+
async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient, amount, options) {
|
|
616
|
+
const config = await getProtocolConfig(rpc, programId);
|
|
617
|
+
const strategy = new SolanaPaymentStrategy();
|
|
618
|
+
return strategy.createPaymentRequest(
|
|
619
|
+
recipient,
|
|
620
|
+
amount,
|
|
621
|
+
{ feeBps: config.feeBps, treasury: config.treasury },
|
|
622
|
+
options
|
|
623
|
+
);
|
|
624
|
+
}
|
|
471
625
|
function toDTag(name) {
|
|
472
626
|
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
627
|
if (!tag) {
|
|
@@ -2120,6 +2274,6 @@ function serializeConfig(config) {
|
|
|
2120
2274
|
return JSON.stringify(config, null, 2) + "\n";
|
|
2121
2275
|
}
|
|
2122
2276
|
|
|
2123
|
-
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 };
|
|
2277
|
+
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 };
|
|
2124
2278
|
//# sourceMappingURL=index.js.map
|
|
2125
2279
|
//# sourceMappingURL=index.js.map
|