@elisym/sdk 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.cjs +321 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +134 -26
- package/dist/index.d.ts +134 -26
- package/dist/index.js +316 -165
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.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
|
-
`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
const transferIx = SystemProgram.transfer({
|
|
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
|
-
})
|
|
348
|
+
`Invalid fee address: expected ${treasury}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
264
349
|
);
|
|
265
350
|
}
|
|
266
|
-
|
|
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
|
-
|
|
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;
|
|
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 };
|
|
444
513
|
}
|
|
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) {
|
|
@@ -2120,6 +2271,6 @@ function serializeConfig(config) {
|
|
|
2120
2271
|
return JSON.stringify(config, null, 2) + "\n";
|
|
2121
2272
|
}
|
|
2122
2273
|
|
|
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 };
|
|
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 };
|
|
2124
2275
|
//# sourceMappingURL=index.js.map
|
|
2125
2276
|
//# sourceMappingURL=index.js.map
|