@elisym/sdk 0.1.2 → 0.2.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 +46 -288
- package/dist/index.cjs +1536 -504
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +207 -139
- package/dist/index.d.ts +207 -139
- package/dist/index.js +1525 -504
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +172 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +32 -0
- package/dist/node.d.ts +32 -0
- package/dist/node.js +167 -0
- package/dist/node.js.map +1 -0
- package/dist/types-CII4k_8d.d.cts +181 -0
- package/dist/types-CII4k_8d.d.ts +181 -0
- package/package.json +56 -23
package/dist/index.cjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var web3_js = require('@solana/web3.js');
|
|
4
|
+
var Decimal2 = require('decimal.js-light');
|
|
3
5
|
var nostrTools = require('nostr-tools');
|
|
4
6
|
var nip44 = require('nostr-tools/nip44');
|
|
5
7
|
var nip17 = require('nostr-tools/nip17');
|
|
6
8
|
var nip59 = require('nostr-tools/nip59');
|
|
7
|
-
var web3_js = require('@solana/web3.js');
|
|
8
|
-
var Decimal = require('decimal.js-light');
|
|
9
9
|
|
|
10
10
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
11
|
|
|
@@ -27,12 +27,10 @@ function _interopNamespace(e) {
|
|
|
27
27
|
return Object.freeze(n);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
var Decimal2__default = /*#__PURE__*/_interopDefault(Decimal2);
|
|
30
31
|
var nip44__namespace = /*#__PURE__*/_interopNamespace(nip44);
|
|
31
32
|
var nip17__namespace = /*#__PURE__*/_interopNamespace(nip17);
|
|
32
33
|
var nip59__namespace = /*#__PURE__*/_interopNamespace(nip59);
|
|
33
|
-
var Decimal__default = /*#__PURE__*/_interopDefault(Decimal);
|
|
34
|
-
|
|
35
|
-
// src/core/pool.ts
|
|
36
34
|
|
|
37
35
|
// src/constants.ts
|
|
38
36
|
var RELAYS = [
|
|
@@ -51,9 +49,15 @@ var KIND_GIFT_WRAP = 1059;
|
|
|
51
49
|
var KIND_JOB_REQUEST = KIND_JOB_REQUEST_BASE + DEFAULT_KIND_OFFSET;
|
|
52
50
|
var KIND_JOB_RESULT = KIND_JOB_RESULT_BASE + DEFAULT_KIND_OFFSET;
|
|
53
51
|
function jobRequestKind(offset) {
|
|
52
|
+
if (!Number.isInteger(offset) || offset < 0 || offset >= 1e3) {
|
|
53
|
+
throw new Error(`Invalid kind offset: ${offset}. Must be integer 0-999.`);
|
|
54
|
+
}
|
|
54
55
|
return KIND_JOB_REQUEST_BASE + offset;
|
|
55
56
|
}
|
|
56
57
|
function jobResultKind(offset) {
|
|
58
|
+
if (!Number.isInteger(offset) || offset < 0 || offset >= 1e3) {
|
|
59
|
+
throw new Error(`Invalid kind offset: ${offset}. Must be integer 0-999.`);
|
|
60
|
+
}
|
|
57
61
|
return KIND_JOB_RESULT_BASE + offset;
|
|
58
62
|
}
|
|
59
63
|
var KIND_PING = 20200;
|
|
@@ -61,192 +65,453 @@ var KIND_PONG = 20201;
|
|
|
61
65
|
var LAMPORTS_PER_SOL = 1e9;
|
|
62
66
|
var PROTOCOL_FEE_BPS = 300;
|
|
63
67
|
var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
var DEFAULTS = {
|
|
69
|
+
SUBSCRIPTION_TIMEOUT_MS: 12e4,
|
|
70
|
+
PING_TIMEOUT_MS: 15e3,
|
|
71
|
+
PING_RETRIES: 2,
|
|
72
|
+
PING_CACHE_TTL_MS: 3e4,
|
|
73
|
+
PAYMENT_EXPIRY_SECS: 600,
|
|
74
|
+
BATCH_SIZE: 250,
|
|
75
|
+
QUERY_TIMEOUT_MS: 15e3,
|
|
76
|
+
EOSE_TIMEOUT_MS: 3e3,
|
|
77
|
+
VERIFY_RETRIES: 10,
|
|
78
|
+
VERIFY_INTERVAL_MS: 3e3,
|
|
79
|
+
VERIFY_BY_REF_RETRIES: 15,
|
|
80
|
+
VERIFY_BY_REF_INTERVAL_MS: 2e3,
|
|
81
|
+
RESULT_RETRY_COUNT: 3,
|
|
82
|
+
RESULT_RETRY_BASE_MS: 1e3,
|
|
83
|
+
QUERY_MAX_CONCURRENCY: 6,
|
|
84
|
+
VERIFY_SIGNATURE_LIMIT: 25
|
|
85
|
+
};
|
|
86
|
+
var LIMITS = {
|
|
87
|
+
MAX_INPUT_LENGTH: 1e5,
|
|
88
|
+
MAX_TIMEOUT_SECS: 600,
|
|
89
|
+
MAX_CAPABILITIES: 20,
|
|
90
|
+
MAX_DESCRIPTION_LENGTH: 500,
|
|
91
|
+
MAX_AGENT_NAME_LENGTH: 64,
|
|
92
|
+
MAX_MESSAGE_LENGTH: 1e4,
|
|
93
|
+
MAX_CAPABILITY_LENGTH: 64
|
|
94
|
+
};
|
|
95
|
+
function assertLamports(value, field) {
|
|
96
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
97
|
+
throw new Error(`Invalid ${field}: ${value}. Must be a non-negative integer.`);
|
|
72
98
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
78
|
-
)
|
|
79
|
-
]);
|
|
80
|
-
}
|
|
81
|
-
async queryBatched(filter, keys, batchSize = 250) {
|
|
82
|
-
const batches = [];
|
|
83
|
-
for (let i = 0; i < keys.length; i += batchSize) {
|
|
84
|
-
const batch = keys.slice(i, i + batchSize);
|
|
85
|
-
batches.push(
|
|
86
|
-
Promise.race([
|
|
87
|
-
this.pool.querySync(this.relays, {
|
|
88
|
-
...filter,
|
|
89
|
-
authors: batch
|
|
90
|
-
}),
|
|
91
|
-
new Promise(
|
|
92
|
-
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
93
|
-
)
|
|
94
|
-
])
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
return (await Promise.all(batches)).flat();
|
|
99
|
+
}
|
|
100
|
+
function calculateProtocolFee(amount) {
|
|
101
|
+
if (!Number.isInteger(amount) || amount < 0) {
|
|
102
|
+
throw new Error(`Invalid fee amount: ${amount}. Must be a non-negative integer.`);
|
|
98
103
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for (let i = 0; i < values.length; i += batchSize) {
|
|
102
|
-
const batch = values.slice(i, i + batchSize);
|
|
103
|
-
batches.push(
|
|
104
|
-
Promise.race([
|
|
105
|
-
this.pool.querySync(this.relays, {
|
|
106
|
-
...filter,
|
|
107
|
-
[`#${tagName}`]: batch
|
|
108
|
-
}),
|
|
109
|
-
new Promise(
|
|
110
|
-
(resolve) => setTimeout(() => resolve([]), 15e3)
|
|
111
|
-
)
|
|
112
|
-
])
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
return (await Promise.all(batches)).flat();
|
|
104
|
+
if (amount === 0) {
|
|
105
|
+
return 0;
|
|
116
106
|
}
|
|
117
|
-
|
|
107
|
+
return new Decimal2__default.default(amount).mul(PROTOCOL_FEE_BPS).div(1e4).toDecimalPlaces(0, Decimal2__default.default.ROUND_CEIL).toNumber();
|
|
108
|
+
}
|
|
109
|
+
function validateExpiry(createdAt, expirySecs) {
|
|
110
|
+
if (!Number.isInteger(createdAt) || createdAt <= 0) {
|
|
111
|
+
return "Invalid or missing created_at in payment request.";
|
|
112
|
+
}
|
|
113
|
+
if (!Number.isInteger(expirySecs) || expirySecs <= 0) {
|
|
114
|
+
return "Invalid or missing expiry_secs in payment request.";
|
|
115
|
+
}
|
|
116
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
117
|
+
if (createdAt > now + 120) {
|
|
118
|
+
return `Payment request created_at is in the future (${createdAt} vs now ${now}). Possible manipulation.`;
|
|
119
|
+
}
|
|
120
|
+
if (now - createdAt > expirySecs) {
|
|
121
|
+
return `Payment request expired (created ${createdAt}, expiry ${expirySecs}s).`;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
function assertExpiry(createdAt, expirySecs) {
|
|
126
|
+
const error = validateExpiry(createdAt, expirySecs);
|
|
127
|
+
if (error) {
|
|
128
|
+
throw new Error(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/payment/solana.ts
|
|
133
|
+
function isValidSolanaAddress(address) {
|
|
134
|
+
try {
|
|
135
|
+
void new web3_js.PublicKey(address);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
var SolanaPaymentStrategy = class {
|
|
142
|
+
chain = "solana";
|
|
143
|
+
calculateFee(amount) {
|
|
144
|
+
return calculateProtocolFee(amount);
|
|
145
|
+
}
|
|
146
|
+
createPaymentRequest(recipientAddress, amount, expirySecs = DEFAULTS.PAYMENT_EXPIRY_SECS) {
|
|
118
147
|
try {
|
|
119
|
-
|
|
120
|
-
} catch
|
|
121
|
-
|
|
122
|
-
throw new Error(
|
|
123
|
-
`Failed to publish to all ${this.relays.length} relays: ${err.errors.map((e) => e.message).join(", ")}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
throw err;
|
|
148
|
+
void new web3_js.PublicKey(recipientAddress);
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error(`Invalid Solana address: ${recipientAddress}`);
|
|
127
151
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!anyOk) {
|
|
152
|
+
assertLamports(amount, "payment amount");
|
|
153
|
+
if (amount === 0) {
|
|
154
|
+
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
155
|
+
}
|
|
156
|
+
if (!Number.isInteger(expirySecs) || expirySecs <= 0 || expirySecs > LIMITS.MAX_TIMEOUT_SECS) {
|
|
134
157
|
throw new Error(
|
|
135
|
-
`
|
|
158
|
+
`Invalid expiry: ${expirySecs}. Must be integer 1-${LIMITS.MAX_TIMEOUT_SECS}.`
|
|
136
159
|
);
|
|
137
160
|
}
|
|
161
|
+
const feeAmount = calculateProtocolFee(amount);
|
|
162
|
+
const reference = web3_js.Keypair.generate().publicKey.toBase58();
|
|
163
|
+
return {
|
|
164
|
+
recipient: recipientAddress,
|
|
165
|
+
amount,
|
|
166
|
+
reference,
|
|
167
|
+
fee_address: PROTOCOL_TREASURY,
|
|
168
|
+
fee_amount: feeAmount,
|
|
169
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
170
|
+
expiry_secs: expirySecs
|
|
171
|
+
};
|
|
138
172
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* before publishing.
|
|
151
|
-
*/
|
|
152
|
-
subscribeAndWait(filter, onEvent, timeoutMs = 3e3) {
|
|
153
|
-
return new Promise((resolve) => {
|
|
154
|
-
let resolved = false;
|
|
155
|
-
const done = () => {
|
|
156
|
-
if (resolved) return;
|
|
157
|
-
resolved = true;
|
|
158
|
-
resolve(combinedSub);
|
|
173
|
+
validatePaymentRequest(requestJson, expectedRecipient) {
|
|
174
|
+
let data;
|
|
175
|
+
try {
|
|
176
|
+
data = JSON.parse(requestJson);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return { code: "invalid_json", message: `Invalid payment request JSON: ${e}` };
|
|
179
|
+
}
|
|
180
|
+
if (typeof data.amount !== "number" || !Number.isInteger(data.amount) || data.amount <= 0) {
|
|
181
|
+
return {
|
|
182
|
+
code: "invalid_amount",
|
|
183
|
+
message: `Invalid amount in payment request: ${data.amount}`
|
|
159
184
|
};
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
185
|
+
}
|
|
186
|
+
if (typeof data.recipient !== "string" || !data.recipient) {
|
|
187
|
+
return { code: "missing_recipient", message: "Missing recipient in payment request" };
|
|
188
|
+
}
|
|
189
|
+
if (!isValidSolanaAddress(data.recipient)) {
|
|
190
|
+
return {
|
|
191
|
+
code: "invalid_recipient_address",
|
|
192
|
+
message: `Invalid Solana address for recipient: ${data.recipient}`
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (typeof data.reference !== "string" || !data.reference) {
|
|
196
|
+
return { code: "missing_reference", message: "Missing reference in payment request" };
|
|
197
|
+
}
|
|
198
|
+
if (!isValidSolanaAddress(data.reference)) {
|
|
199
|
+
return {
|
|
200
|
+
code: "invalid_reference_address",
|
|
201
|
+
message: `Invalid Solana address for reference: ${data.reference}`
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (expectedRecipient && data.recipient !== expectedRecipient) {
|
|
205
|
+
return {
|
|
206
|
+
code: "recipient_mismatch",
|
|
207
|
+
message: `Recipient mismatch: expected ${expectedRecipient}, got ${data.recipient}. Provider may be attempting to redirect payment.`
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const expiryError = validateExpiry(data.created_at, data.expiry_secs);
|
|
211
|
+
if (expiryError) {
|
|
212
|
+
const code = expiryError.includes("future") ? "future_timestamp" : "expired";
|
|
213
|
+
return { code, message: expiryError };
|
|
214
|
+
}
|
|
215
|
+
const expectedFee = calculateProtocolFee(data.amount);
|
|
216
|
+
const { fee_address, fee_amount } = data;
|
|
217
|
+
const hasFeeAddress = typeof fee_address === "string" && fee_address.length > 0;
|
|
218
|
+
const hasFeeAmount = typeof fee_amount === "number" && fee_amount > 0;
|
|
219
|
+
if (hasFeeAddress && hasFeeAmount) {
|
|
220
|
+
if (fee_address !== PROTOCOL_TREASURY) {
|
|
221
|
+
return {
|
|
222
|
+
code: "fee_address_mismatch",
|
|
223
|
+
message: `Fee address mismatch: expected ${PROTOCOL_TREASURY}, got ${fee_address}. Provider may be attempting to redirect fees.`
|
|
224
|
+
};
|
|
171
225
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
226
|
+
if (fee_amount !== expectedFee) {
|
|
227
|
+
return {
|
|
228
|
+
code: "fee_amount_mismatch",
|
|
229
|
+
message: `Fee amount mismatch: expected ${expectedFee} lamports (${PROTOCOL_FEE_BPS}bps of ${data.amount}), got ${fee_amount}. Provider may be tampering with fee.`
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (!hasFeeAddress && (fee_amount === null || fee_amount === void 0 || fee_amount === 0)) {
|
|
235
|
+
return {
|
|
236
|
+
code: "missing_fee",
|
|
237
|
+
message: `Payment request missing protocol fee (${PROTOCOL_FEE_BPS}bps). Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
|
|
176
238
|
};
|
|
177
|
-
setTimeout(done, timeoutMs);
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* Tear down pool and create a fresh one.
|
|
182
|
-
* Works around nostr-tools `onerror → skipReconnection = true` bug
|
|
183
|
-
* that permanently kills subscriptions. Callers must re-subscribe.
|
|
184
|
-
*/
|
|
185
|
-
reset() {
|
|
186
|
-
try {
|
|
187
|
-
this.pool.close(this.relays);
|
|
188
|
-
} catch {
|
|
189
239
|
}
|
|
190
|
-
|
|
240
|
+
return {
|
|
241
|
+
code: "invalid_fee_params",
|
|
242
|
+
message: `Invalid fee params in payment request. Expected fee: ${expectedFee} lamports to ${PROTOCOL_TREASURY}.`
|
|
243
|
+
};
|
|
191
244
|
}
|
|
192
245
|
/**
|
|
193
|
-
*
|
|
246
|
+
* Build an unsigned transaction from a payment request.
|
|
247
|
+
* The caller must set `recentBlockhash` and `feePayer` on the
|
|
248
|
+
* returned Transaction before signing and sending.
|
|
194
249
|
*/
|
|
195
|
-
async
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
250
|
+
async buildTransaction(payerAddress, paymentRequest) {
|
|
251
|
+
assertLamports(paymentRequest.amount, "payment amount");
|
|
252
|
+
if (paymentRequest.amount === 0) {
|
|
253
|
+
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
254
|
+
}
|
|
255
|
+
if (paymentRequest.fee_amount !== null && paymentRequest.fee_amount !== void 0 && (!Number.isInteger(paymentRequest.fee_amount) || paymentRequest.fee_amount < 0)) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`Invalid fee amount: ${paymentRequest.fee_amount}. Must be a non-negative integer (lamports).`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
assertExpiry(paymentRequest.created_at, paymentRequest.expiry_secs);
|
|
261
|
+
if (paymentRequest.fee_address && paymentRequest.fee_address !== PROTOCOL_TREASURY) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Invalid fee address: expected ${PROTOCOL_TREASURY}, got ${paymentRequest.fee_address}. Cannot build transaction with redirected fees.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const payerPubkey = new web3_js.PublicKey(payerAddress);
|
|
267
|
+
const recipient = new web3_js.PublicKey(paymentRequest.recipient);
|
|
268
|
+
const reference = new web3_js.PublicKey(paymentRequest.reference);
|
|
269
|
+
const feeAddress = paymentRequest.fee_address ? new web3_js.PublicKey(paymentRequest.fee_address) : null;
|
|
270
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
271
|
+
const providerAmount = feeAddress && feeAmount > 0 ? paymentRequest.amount - feeAmount : paymentRequest.amount;
|
|
272
|
+
if (providerAmount <= 0) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount}). Cannot create transaction with non-positive provider amount.`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const transferIx = web3_js.SystemProgram.transfer({
|
|
278
|
+
fromPubkey: payerPubkey,
|
|
279
|
+
toPubkey: recipient,
|
|
280
|
+
lamports: providerAmount
|
|
281
|
+
});
|
|
282
|
+
transferIx.keys.push({
|
|
283
|
+
pubkey: reference,
|
|
284
|
+
isSigner: false,
|
|
285
|
+
isWritable: false
|
|
286
|
+
});
|
|
287
|
+
const tx = new web3_js.Transaction().add(transferIx);
|
|
288
|
+
if (feeAddress && feeAmount > 0) {
|
|
289
|
+
tx.add(
|
|
290
|
+
web3_js.SystemProgram.transfer({
|
|
291
|
+
fromPubkey: payerPubkey,
|
|
292
|
+
toPubkey: feeAddress,
|
|
293
|
+
lamports: feeAmount
|
|
202
294
|
})
|
|
203
|
-
|
|
204
|
-
return true;
|
|
205
|
-
} catch {
|
|
206
|
-
return false;
|
|
207
|
-
} finally {
|
|
208
|
-
clearTimeout(timer);
|
|
295
|
+
);
|
|
209
296
|
}
|
|
297
|
+
return tx;
|
|
210
298
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
299
|
+
async verifyPayment(connection, paymentRequest, options) {
|
|
300
|
+
if (!connection || typeof connection.getTransaction !== "function") {
|
|
301
|
+
return { verified: false, error: "Invalid connection: expected Solana Connection instance" };
|
|
302
|
+
}
|
|
303
|
+
const conn = connection;
|
|
304
|
+
if (!paymentRequest.reference || !paymentRequest.recipient) {
|
|
305
|
+
return { verified: false, error: "Missing required fields in payment request" };
|
|
306
|
+
}
|
|
307
|
+
if (!Number.isInteger(paymentRequest.amount) || paymentRequest.amount <= 0) {
|
|
308
|
+
return {
|
|
309
|
+
verified: false,
|
|
310
|
+
error: `Invalid payment amount: ${paymentRequest.amount}. Must be a positive integer.`
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (paymentRequest.fee_amount !== null && paymentRequest.fee_amount !== void 0 && (!Number.isInteger(paymentRequest.fee_amount) || paymentRequest.fee_amount < 0)) {
|
|
314
|
+
return {
|
|
315
|
+
verified: false,
|
|
316
|
+
error: `Invalid fee_amount: ${paymentRequest.fee_amount}. Must be a non-negative integer.`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const expectedFee = calculateProtocolFee(paymentRequest.amount);
|
|
320
|
+
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
321
|
+
if (expectedFee > 0) {
|
|
322
|
+
if (feeAmount < expectedFee) {
|
|
323
|
+
return {
|
|
324
|
+
verified: false,
|
|
325
|
+
error: `Protocol fee ${feeAmount} below required ${expectedFee} (${PROTOCOL_FEE_BPS}bps of ${paymentRequest.amount})`
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if (!paymentRequest.fee_address) {
|
|
329
|
+
return { verified: false, error: "Missing fee address in payment request" };
|
|
330
|
+
}
|
|
331
|
+
if (paymentRequest.fee_address !== PROTOCOL_TREASURY) {
|
|
332
|
+
return { verified: false, error: `Invalid fee address: ${paymentRequest.fee_address}` };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const expectedNet = paymentRequest.amount - feeAmount;
|
|
336
|
+
if (expectedNet <= 0) {
|
|
337
|
+
return {
|
|
338
|
+
verified: false,
|
|
339
|
+
error: `Fee amount (${feeAmount}) exceeds or equals total amount (${paymentRequest.amount})`
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
if (options?.txSignature) {
|
|
343
|
+
return this._verifyBySignature(
|
|
344
|
+
conn,
|
|
345
|
+
options.txSignature,
|
|
346
|
+
paymentRequest.reference,
|
|
347
|
+
paymentRequest.recipient,
|
|
348
|
+
expectedNet,
|
|
349
|
+
feeAmount,
|
|
350
|
+
options?.retries ?? DEFAULTS.VERIFY_RETRIES,
|
|
351
|
+
options?.intervalMs ?? DEFAULTS.VERIFY_INTERVAL_MS
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
return this._verifyByReference(
|
|
355
|
+
conn,
|
|
356
|
+
paymentRequest.reference,
|
|
357
|
+
paymentRequest.recipient,
|
|
358
|
+
expectedNet,
|
|
359
|
+
feeAmount,
|
|
360
|
+
options?.retries ?? DEFAULTS.VERIFY_BY_REF_RETRIES,
|
|
361
|
+
options?.intervalMs ?? DEFAULTS.VERIFY_BY_REF_INTERVAL_MS
|
|
362
|
+
);
|
|
232
363
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
364
|
+
async _verifyBySignature(connection, txSignature, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
365
|
+
let lastError;
|
|
366
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
367
|
+
try {
|
|
368
|
+
const tx = await connection.getTransaction(txSignature, {
|
|
369
|
+
maxSupportedTransactionVersion: 0,
|
|
370
|
+
commitment: "confirmed"
|
|
371
|
+
});
|
|
372
|
+
if (!tx?.meta || tx.meta.err) {
|
|
373
|
+
if (attempt < retries - 1) {
|
|
374
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
verified: false,
|
|
379
|
+
error: tx?.meta?.err ? "Transaction failed on-chain" : "Transaction not found"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const accountKeys = tx.transaction.message.getAccountKeys();
|
|
383
|
+
const balanceCount = tx.meta.preBalances.length;
|
|
384
|
+
const keyToIdx = /* @__PURE__ */ new Map();
|
|
385
|
+
for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
|
|
386
|
+
const key = accountKeys.get(i);
|
|
387
|
+
if (key) {
|
|
388
|
+
keyToIdx.set(key.toBase58(), i);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (!keyToIdx.has(referenceKey)) {
|
|
392
|
+
return {
|
|
393
|
+
verified: false,
|
|
394
|
+
error: "Reference key not found in transaction - possible replay"
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const recipientIdx = keyToIdx.get(recipientAddress);
|
|
398
|
+
if (recipientIdx === void 0) {
|
|
399
|
+
return { verified: false, error: "Recipient not found in transaction" };
|
|
400
|
+
}
|
|
401
|
+
const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
|
|
402
|
+
if (recipientDelta < expectedNet) {
|
|
403
|
+
return {
|
|
404
|
+
verified: false,
|
|
405
|
+
error: `Recipient received ${recipientDelta}, expected >= ${expectedNet}`
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
if (expectedFee > 0) {
|
|
409
|
+
const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
|
|
410
|
+
if (treasuryIdx === void 0) {
|
|
411
|
+
return { verified: false, error: "Treasury not found in transaction" };
|
|
412
|
+
}
|
|
413
|
+
const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
|
|
414
|
+
if (treasuryDelta < expectedFee) {
|
|
415
|
+
return {
|
|
416
|
+
verified: false,
|
|
417
|
+
error: `Treasury received ${treasuryDelta}, expected >= ${expectedFee}`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { verified: true, txSignature };
|
|
422
|
+
} catch (err) {
|
|
423
|
+
lastError = err;
|
|
424
|
+
if (attempt < retries - 1) {
|
|
425
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
236
428
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
429
|
+
return {
|
|
430
|
+
verified: false,
|
|
431
|
+
error: `Verification failed after ${retries} retries: ${lastError instanceof Error ? lastError.message : "unknown error"}`
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async _verifyByReference(connection, referenceKey, recipientAddress, expectedNet, expectedFee, retries, intervalMs) {
|
|
435
|
+
const reference = new web3_js.PublicKey(referenceKey);
|
|
436
|
+
let lastError;
|
|
437
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
438
|
+
try {
|
|
439
|
+
const signatures = await connection.getSignaturesForAddress(reference, {
|
|
440
|
+
limit: DEFAULTS.VERIFY_SIGNATURE_LIMIT
|
|
441
|
+
});
|
|
442
|
+
const validSigs = signatures.filter((s) => !s.err);
|
|
443
|
+
if (validSigs.length > 0) {
|
|
444
|
+
const txResults = await Promise.all(
|
|
445
|
+
validSigs.map(
|
|
446
|
+
(s) => connection.getTransaction(s.signature, {
|
|
447
|
+
maxSupportedTransactionVersion: 0,
|
|
448
|
+
commitment: "confirmed"
|
|
449
|
+
}).then((tx) => ({ sig: s.signature, tx })).catch(() => ({
|
|
450
|
+
sig: s.signature,
|
|
451
|
+
tx: null
|
|
452
|
+
}))
|
|
453
|
+
)
|
|
454
|
+
);
|
|
455
|
+
for (const { sig, tx } of txResults) {
|
|
456
|
+
if (!tx?.meta || tx.meta.err) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const accountKeys = tx.transaction.message.getAccountKeys();
|
|
460
|
+
const balanceCount = tx.meta.preBalances.length;
|
|
461
|
+
const keyToIdx = /* @__PURE__ */ new Map();
|
|
462
|
+
for (let i = 0; i < Math.min(accountKeys.length, balanceCount); i++) {
|
|
463
|
+
const key = accountKeys.get(i);
|
|
464
|
+
if (key) {
|
|
465
|
+
keyToIdx.set(key.toBase58(), i);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const recipientIdx = keyToIdx.get(recipientAddress);
|
|
469
|
+
if (recipientIdx === void 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const recipientDelta = (tx.meta.postBalances[recipientIdx] ?? 0) - (tx.meta.preBalances[recipientIdx] ?? 0);
|
|
473
|
+
if (recipientDelta < expectedNet) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (expectedFee > 0) {
|
|
477
|
+
const treasuryIdx = keyToIdx.get(PROTOCOL_TREASURY);
|
|
478
|
+
if (treasuryIdx === void 0) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
const treasuryDelta = (tx.meta.postBalances[treasuryIdx] ?? 0) - (tx.meta.preBalances[treasuryIdx] ?? 0);
|
|
482
|
+
if (treasuryDelta < expectedFee) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return { verified: true, txSignature: sig };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch (err) {
|
|
490
|
+
lastError = err;
|
|
491
|
+
}
|
|
492
|
+
if (attempt < retries - 1) {
|
|
493
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
494
|
+
}
|
|
240
495
|
}
|
|
241
|
-
return
|
|
496
|
+
return {
|
|
497
|
+
verified: false,
|
|
498
|
+
error: lastError ? `Verification failed: ${lastError instanceof Error ? lastError.message : "unknown error"}` : "No matching transaction found for reference key"
|
|
499
|
+
};
|
|
242
500
|
}
|
|
243
501
|
};
|
|
244
502
|
function toDTag(name) {
|
|
245
|
-
|
|
503
|
+
const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
|
|
504
|
+
if (!tag) {
|
|
505
|
+
throw new Error("Capability name must contain at least one ASCII alphanumeric character.");
|
|
506
|
+
}
|
|
507
|
+
return tag;
|
|
246
508
|
}
|
|
247
509
|
function buildAgentsFromEvents(events, network) {
|
|
248
510
|
const latestByDTag = /* @__PURE__ */ new Map();
|
|
249
511
|
for (const event of events) {
|
|
512
|
+
if (!nostrTools.verifyEvent(event)) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
250
515
|
const dTag = event.tags.find((t) => t[0] === "d")?.[1] ?? "";
|
|
251
516
|
const key = `${event.pubkey}:${dTag}`;
|
|
252
517
|
const prev = latestByDTag.get(key);
|
|
@@ -257,17 +522,45 @@ function buildAgentsFromEvents(events, network) {
|
|
|
257
522
|
const accumMap = /* @__PURE__ */ new Map();
|
|
258
523
|
for (const event of latestByDTag.values()) {
|
|
259
524
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
525
|
+
if (!event.content) {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
const raw = JSON.parse(event.content);
|
|
529
|
+
if (!raw || typeof raw !== "object") {
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (typeof raw.name !== "string" || !raw.name) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (typeof raw.description !== "string") {
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
if (!Array.isArray(raw.capabilities) || !raw.capabilities.every((c) => typeof c === "string")) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (raw.deleted) {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const card = raw;
|
|
545
|
+
if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
262
551
|
const agentNetwork = card.payment?.network ?? "devnet";
|
|
263
|
-
if (agentNetwork !== network)
|
|
552
|
+
if (agentNetwork !== network) {
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
264
555
|
const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
|
|
265
556
|
const entry = { card, kTags, createdAt: event.created_at };
|
|
266
557
|
const existing = accumMap.get(event.pubkey);
|
|
267
558
|
if (existing) {
|
|
268
559
|
const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
|
|
269
560
|
if (dupIndex >= 0) {
|
|
270
|
-
existing.entries[dupIndex]
|
|
561
|
+
if (entry.createdAt >= existing.entries[dupIndex].createdAt) {
|
|
562
|
+
existing.entries[dupIndex] = entry;
|
|
563
|
+
}
|
|
271
564
|
} else {
|
|
272
565
|
existing.entries.push(entry);
|
|
273
566
|
}
|
|
@@ -289,12 +582,13 @@ function buildAgentsFromEvents(events, network) {
|
|
|
289
582
|
}
|
|
290
583
|
const agentMap = /* @__PURE__ */ new Map();
|
|
291
584
|
for (const [pubkey, acc] of accumMap) {
|
|
292
|
-
const
|
|
585
|
+
const kindsSet = /* @__PURE__ */ new Set();
|
|
293
586
|
for (const e of acc.entries) {
|
|
294
587
|
for (const k of e.kTags) {
|
|
295
|
-
|
|
588
|
+
kindsSet.add(k);
|
|
296
589
|
}
|
|
297
590
|
}
|
|
591
|
+
const supportedKinds = [...kindsSet];
|
|
298
592
|
agentMap.set(pubkey, {
|
|
299
593
|
pubkey: acc.pubkey,
|
|
300
594
|
npub: acc.npub,
|
|
@@ -310,23 +604,28 @@ var DiscoveryService = class {
|
|
|
310
604
|
constructor(pool) {
|
|
311
605
|
this.pool = pool;
|
|
312
606
|
}
|
|
313
|
-
// Instance-level set — avoids module-level state leak across clients
|
|
314
|
-
allSeenAgents = /* @__PURE__ */ new Set();
|
|
315
607
|
/** Count elisym agents (kind:31990 with "elisym" tag). */
|
|
316
608
|
async fetchAllAgentCount() {
|
|
317
609
|
const events = await this.pool.querySync({
|
|
318
610
|
kinds: [KIND_APP_HANDLER],
|
|
319
611
|
"#t": ["elisym"]
|
|
320
612
|
});
|
|
613
|
+
const uniquePubkeys = /* @__PURE__ */ new Set();
|
|
321
614
|
for (const event of events) {
|
|
322
|
-
|
|
615
|
+
if (!nostrTools.verifyEvent(event)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
uniquePubkeys.add(event.pubkey);
|
|
323
619
|
}
|
|
324
|
-
return
|
|
620
|
+
return uniquePubkeys.size;
|
|
325
621
|
}
|
|
326
622
|
/**
|
|
327
623
|
* Fetch a single page of elisym agents with relay-side pagination.
|
|
328
624
|
* Uses `until` cursor for Nostr cursor-based pagination.
|
|
329
|
-
*
|
|
625
|
+
*
|
|
626
|
+
* Unlike `fetchAgents`, this method does NOT enrich agents with
|
|
627
|
+
* kind:0 metadata (name, picture, about) or update `lastSeen` from
|
|
628
|
+
* recent job activity. Call `enrichWithMetadata()` separately if needed.
|
|
330
629
|
*/
|
|
331
630
|
async fetchAgentsPage(network = "devnet", limit = 20, until) {
|
|
332
631
|
const filter = {
|
|
@@ -346,21 +645,24 @@ var DiscoveryService = class {
|
|
|
346
645
|
}
|
|
347
646
|
}
|
|
348
647
|
const agentMap = buildAgentsFromEvents(events, network);
|
|
349
|
-
const agents = Array.from(agentMap.values()).sort(
|
|
350
|
-
(a, b) => b.lastSeen - a.lastSeen
|
|
351
|
-
);
|
|
648
|
+
const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
352
649
|
return { agents, oldestCreatedAt, rawEventCount };
|
|
353
650
|
}
|
|
354
651
|
/** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
|
|
355
652
|
async enrichWithMetadata(agents) {
|
|
356
653
|
const pubkeys = agents.map((a) => a.pubkey);
|
|
357
|
-
if (pubkeys.length === 0)
|
|
654
|
+
if (pubkeys.length === 0) {
|
|
655
|
+
return agents;
|
|
656
|
+
}
|
|
358
657
|
const metaEvents = await this.pool.queryBatched(
|
|
359
658
|
{ kinds: [0] },
|
|
360
659
|
pubkeys
|
|
361
660
|
);
|
|
362
661
|
const latestMeta = /* @__PURE__ */ new Map();
|
|
363
662
|
for (const ev of metaEvents) {
|
|
663
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
364
666
|
const prev = latestMeta.get(ev.pubkey);
|
|
365
667
|
if (!prev || ev.created_at > prev.created_at) {
|
|
366
668
|
latestMeta.set(ev.pubkey, ev);
|
|
@@ -369,14 +671,22 @@ var DiscoveryService = class {
|
|
|
369
671
|
const agentLookup = new Map(agents.map((a) => [a.pubkey, a]));
|
|
370
672
|
for (const [pubkey, ev] of latestMeta) {
|
|
371
673
|
const agent = agentLookup.get(pubkey);
|
|
372
|
-
if (!agent)
|
|
674
|
+
if (!agent) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
373
677
|
try {
|
|
374
678
|
const meta = JSON.parse(ev.content);
|
|
375
|
-
if (meta.picture
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
679
|
+
if (typeof meta.picture === "string") {
|
|
680
|
+
agent.picture = meta.picture;
|
|
681
|
+
}
|
|
682
|
+
if (typeof meta.name === "string") {
|
|
683
|
+
agent.name = meta.name;
|
|
684
|
+
}
|
|
685
|
+
if (typeof meta.about === "string") {
|
|
686
|
+
agent.about = meta.about;
|
|
687
|
+
}
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
380
690
|
}
|
|
381
691
|
return agents;
|
|
382
692
|
}
|
|
@@ -386,9 +696,12 @@ var DiscoveryService = class {
|
|
|
386
696
|
kinds: [KIND_APP_HANDLER],
|
|
387
697
|
"#t": ["elisym"]
|
|
388
698
|
};
|
|
389
|
-
if (limit !== void 0)
|
|
699
|
+
if (limit !== void 0) {
|
|
700
|
+
filter.limit = limit;
|
|
701
|
+
}
|
|
390
702
|
const events = await this.pool.querySync(filter);
|
|
391
703
|
const agentMap = buildAgentsFromEvents(events, network);
|
|
704
|
+
const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
|
|
392
705
|
const agentPubkeys = Array.from(agentMap.keys());
|
|
393
706
|
if (agentPubkeys.length > 0) {
|
|
394
707
|
const activitySince = Math.floor(Date.now() / 1e3) - 24 * 60 * 60;
|
|
@@ -401,33 +714,65 @@ var DiscoveryService = class {
|
|
|
401
714
|
}
|
|
402
715
|
}
|
|
403
716
|
resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
|
|
404
|
-
const activityEvents = await
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
717
|
+
const [activityEvents] = await Promise.all([
|
|
718
|
+
this.pool.queryBatched(
|
|
719
|
+
{
|
|
720
|
+
kinds: [...resultKinds, KIND_JOB_FEEDBACK],
|
|
721
|
+
since: activitySince
|
|
722
|
+
},
|
|
723
|
+
agentPubkeys
|
|
724
|
+
),
|
|
725
|
+
this.enrichWithMetadata(agents)
|
|
726
|
+
]);
|
|
411
727
|
for (const ev of activityEvents) {
|
|
728
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
412
731
|
const agent = agentMap.get(ev.pubkey);
|
|
413
732
|
if (agent && ev.created_at > agent.lastSeen) {
|
|
414
733
|
agent.lastSeen = ev.created_at;
|
|
415
734
|
}
|
|
416
735
|
}
|
|
736
|
+
agents.sort((a, b) => b.lastSeen - a.lastSeen);
|
|
417
737
|
}
|
|
418
|
-
const agents = Array.from(agentMap.values()).sort(
|
|
419
|
-
(a, b) => b.lastSeen - a.lastSeen
|
|
420
|
-
);
|
|
421
|
-
await this.enrichWithMetadata(agents);
|
|
422
738
|
return agents;
|
|
423
739
|
}
|
|
424
|
-
/**
|
|
740
|
+
/**
|
|
741
|
+
* Publish a capability card (kind:31990) as a provider.
|
|
742
|
+
* Solana address is validated for Base58 format only - full decode
|
|
743
|
+
* validation (32-byte public key) happens at payment time.
|
|
744
|
+
*/
|
|
425
745
|
async publishCapability(identity, card, kinds = [KIND_JOB_REQUEST]) {
|
|
426
746
|
if (!card.payment?.address) {
|
|
427
747
|
throw new Error(
|
|
428
748
|
"Cannot publish capability without a payment address. Connect a wallet before publishing."
|
|
429
749
|
);
|
|
430
750
|
}
|
|
751
|
+
if (card.payment.chain === "solana" && !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(card.payment.address)) {
|
|
752
|
+
throw new Error(`Invalid Solana address format: ${card.payment.address}`);
|
|
753
|
+
}
|
|
754
|
+
if (card.name.length > LIMITS.MAX_AGENT_NAME_LENGTH) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`Agent name too long: ${card.name.length} chars (max ${LIMITS.MAX_AGENT_NAME_LENGTH}).`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
if (card.description.length > LIMITS.MAX_DESCRIPTION_LENGTH) {
|
|
760
|
+
throw new Error(
|
|
761
|
+
`Description too long: ${card.description.length} chars (max ${LIMITS.MAX_DESCRIPTION_LENGTH}).`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (card.capabilities.length > LIMITS.MAX_CAPABILITIES) {
|
|
765
|
+
throw new Error(
|
|
766
|
+
`Too many capabilities: ${card.capabilities.length} (max ${LIMITS.MAX_CAPABILITIES}).`
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
for (const cap of card.capabilities) {
|
|
770
|
+
if (cap.length > LIMITS.MAX_CAPABILITY_LENGTH) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`Capability name too long: "${cap}" (${cap.length} chars, max ${LIMITS.MAX_CAPABILITY_LENGTH}).`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
431
776
|
const tags = [
|
|
432
777
|
["d", toDTag(card.name)],
|
|
433
778
|
["t", "elisym"],
|
|
@@ -443,13 +788,28 @@ var DiscoveryService = class {
|
|
|
443
788
|
},
|
|
444
789
|
identity.secretKey
|
|
445
790
|
);
|
|
446
|
-
await this.pool.
|
|
791
|
+
await this.pool.publishAll(event);
|
|
447
792
|
return event.id;
|
|
448
793
|
}
|
|
449
794
|
/** Publish a Nostr profile (kind:0) as a provider. */
|
|
450
|
-
async publishProfile(identity, name, about, picture) {
|
|
795
|
+
async publishProfile(identity, name, about, picture, banner) {
|
|
796
|
+
if (name.length > LIMITS.MAX_AGENT_NAME_LENGTH) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`Profile name too long: ${name.length} chars (max ${LIMITS.MAX_AGENT_NAME_LENGTH}).`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (about.length > LIMITS.MAX_DESCRIPTION_LENGTH) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`Profile about too long: ${about.length} chars (max ${LIMITS.MAX_DESCRIPTION_LENGTH}).`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
451
806
|
const content = { name, about };
|
|
452
|
-
if (picture)
|
|
807
|
+
if (picture) {
|
|
808
|
+
content.picture = picture;
|
|
809
|
+
}
|
|
810
|
+
if (banner) {
|
|
811
|
+
content.banner = banner;
|
|
812
|
+
}
|
|
453
813
|
const event = nostrTools.finalizeEvent(
|
|
454
814
|
{
|
|
455
815
|
kind: 0,
|
|
@@ -459,7 +819,7 @@ var DiscoveryService = class {
|
|
|
459
819
|
},
|
|
460
820
|
identity.secretKey
|
|
461
821
|
);
|
|
462
|
-
await this.pool.
|
|
822
|
+
await this.pool.publishAll(event);
|
|
463
823
|
return event.id;
|
|
464
824
|
}
|
|
465
825
|
/**
|
|
@@ -486,20 +846,40 @@ var DiscoveryService = class {
|
|
|
486
846
|
return event.id;
|
|
487
847
|
}
|
|
488
848
|
};
|
|
489
|
-
function
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
function nip44Encrypt(plaintext, secretKey, recipientPubkey) {
|
|
493
|
-
const conversationKey = nip44__namespace.v2.utils.getConversationKey(secretKey, recipientPubkey);
|
|
849
|
+
function nip44Encrypt(plaintext, senderSk, recipientPubkey) {
|
|
850
|
+
const conversationKey = nip44__namespace.v2.utils.getConversationKey(senderSk, recipientPubkey);
|
|
494
851
|
return nip44__namespace.v2.encrypt(plaintext, conversationKey);
|
|
495
852
|
}
|
|
496
|
-
function nip44Decrypt(ciphertext,
|
|
497
|
-
const conversationKey = nip44__namespace.v2.utils.getConversationKey(
|
|
853
|
+
function nip44Decrypt(ciphertext, receiverSk, senderPubkey) {
|
|
854
|
+
const conversationKey = nip44__namespace.v2.utils.getConversationKey(receiverSk, senderPubkey);
|
|
498
855
|
return nip44__namespace.v2.decrypt(ciphertext, conversationKey);
|
|
499
856
|
}
|
|
857
|
+
|
|
858
|
+
// src/services/marketplace.ts
|
|
859
|
+
function isEncrypted(event) {
|
|
860
|
+
return event.tags.some((t) => t[0] === "encrypted" && t[1] === "nip44");
|
|
861
|
+
}
|
|
500
862
|
function resolveRequestId(event) {
|
|
501
863
|
return event.tags.find((t) => t[0] === "e")?.[1];
|
|
502
864
|
}
|
|
865
|
+
function safeParseInt(value) {
|
|
866
|
+
if (!value) {
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
const n = parseInt(value, 10);
|
|
870
|
+
return isNaN(n) ? void 0 : n;
|
|
871
|
+
}
|
|
872
|
+
var VALID_JOB_STATUSES = /* @__PURE__ */ new Set([
|
|
873
|
+
"payment-required",
|
|
874
|
+
"payment-completed",
|
|
875
|
+
"processing",
|
|
876
|
+
"error",
|
|
877
|
+
"success",
|
|
878
|
+
"partial"
|
|
879
|
+
]);
|
|
880
|
+
function toJobStatus(raw) {
|
|
881
|
+
return VALID_JOB_STATUSES.has(raw) ? raw : "unknown";
|
|
882
|
+
}
|
|
503
883
|
var MarketplaceService = class {
|
|
504
884
|
constructor(pool) {
|
|
505
885
|
this.pool = pool;
|
|
@@ -509,11 +889,21 @@ var MarketplaceService = class {
|
|
|
509
889
|
if (!options.input) {
|
|
510
890
|
throw new Error("Job input must not be empty.");
|
|
511
891
|
}
|
|
892
|
+
if (options.input.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
893
|
+
throw new Error(
|
|
894
|
+
`Job input too long: ${options.input.length} chars (max ${LIMITS.MAX_INPUT_LENGTH}).`
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
if (!options.capability || options.capability.length > LIMITS.MAX_CAPABILITY_LENGTH) {
|
|
898
|
+
throw new Error(`Invalid capability: must be 1-${LIMITS.MAX_CAPABILITY_LENGTH} characters.`);
|
|
899
|
+
}
|
|
900
|
+
if (options.providerPubkey && !/^[0-9a-f]{64}$/.test(options.providerPubkey)) {
|
|
901
|
+
throw new Error("Invalid provider pubkey: expected 64 hex characters.");
|
|
902
|
+
}
|
|
512
903
|
const plaintext = options.input;
|
|
513
904
|
const encrypted = options.providerPubkey ? nip44Encrypt(plaintext, identity.secretKey, options.providerPubkey) : plaintext;
|
|
514
|
-
const iValue = options.providerPubkey ? "encrypted" : "";
|
|
515
905
|
const tags = [
|
|
516
|
-
["i",
|
|
906
|
+
["i", options.providerPubkey ? "encrypted" : "text", "text"],
|
|
517
907
|
["t", options.capability],
|
|
518
908
|
["t", "elisym"],
|
|
519
909
|
["output", "text/plain"]
|
|
@@ -537,80 +927,148 @@ var MarketplaceService = class {
|
|
|
537
927
|
}
|
|
538
928
|
/**
|
|
539
929
|
* Subscribe to job updates (feedback + results) for a given job.
|
|
540
|
-
*
|
|
930
|
+
* Creates 3 subscriptions per call (feedback, result by #e, result by #p+#e)
|
|
931
|
+
* to cover different relay indexing strategies. Returns a cleanup function.
|
|
541
932
|
*/
|
|
542
|
-
subscribeToJobUpdates(
|
|
543
|
-
const
|
|
933
|
+
subscribeToJobUpdates(options) {
|
|
934
|
+
const {
|
|
935
|
+
jobEventId: jid,
|
|
936
|
+
providerPubkey: provPk,
|
|
937
|
+
customerPublicKey: custPk,
|
|
938
|
+
callbacks: cb,
|
|
939
|
+
timeoutMs = DEFAULTS.SUBSCRIPTION_TIMEOUT_MS,
|
|
940
|
+
customerSecretKey: custSk,
|
|
941
|
+
kindOffsets: offsets_,
|
|
942
|
+
sinceOverride: since_
|
|
943
|
+
} = options;
|
|
944
|
+
const offsets = offsets_ ?? [DEFAULT_KIND_OFFSET];
|
|
945
|
+
if (offsets.length === 0) {
|
|
946
|
+
throw new Error("kindOffsets must not be empty.");
|
|
947
|
+
}
|
|
544
948
|
const resultKinds = offsets.map(jobResultKind);
|
|
545
|
-
const since = Math.floor(Date.now() / 1e3) -
|
|
949
|
+
const since = since_ ?? Math.floor(Date.now() / 1e3) - 30;
|
|
546
950
|
const subs = [];
|
|
547
951
|
let resolved = false;
|
|
548
952
|
let resultDelivered = false;
|
|
549
953
|
let timer;
|
|
550
954
|
const done = () => {
|
|
551
955
|
resolved = true;
|
|
552
|
-
if (timer)
|
|
553
|
-
|
|
956
|
+
if (timer) {
|
|
957
|
+
clearTimeout(timer);
|
|
958
|
+
}
|
|
959
|
+
for (const s of subs) {
|
|
960
|
+
try {
|
|
961
|
+
s.close();
|
|
962
|
+
} catch {
|
|
963
|
+
}
|
|
964
|
+
}
|
|
554
965
|
};
|
|
555
966
|
const decryptResult = (ev) => {
|
|
556
|
-
if (
|
|
967
|
+
if (isEncrypted(ev)) {
|
|
968
|
+
if (!custSk) {
|
|
969
|
+
return null;
|
|
970
|
+
}
|
|
557
971
|
try {
|
|
558
|
-
return nip44Decrypt(ev.content,
|
|
972
|
+
return nip44Decrypt(ev.content, custSk, ev.pubkey);
|
|
559
973
|
} catch {
|
|
560
|
-
return
|
|
974
|
+
return null;
|
|
561
975
|
}
|
|
562
976
|
}
|
|
563
977
|
return ev.content;
|
|
564
978
|
};
|
|
565
979
|
const handleResult = (ev) => {
|
|
566
|
-
if (resolved || resultDelivered)
|
|
567
|
-
|
|
980
|
+
if (resolved || resultDelivered) {
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (provPk && ev.pubkey !== provPk) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const eTag = ev.tags.find((t) => t[0] === "e")?.[1];
|
|
990
|
+
if (eTag !== jid) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const content = decryptResult(ev);
|
|
994
|
+
if (content === null) {
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
568
997
|
resultDelivered = true;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
kinds: [KIND_JOB_FEEDBACK],
|
|
575
|
-
"#e": [jobEventId],
|
|
576
|
-
since
|
|
577
|
-
},
|
|
578
|
-
(ev) => {
|
|
579
|
-
if (resolved) return;
|
|
580
|
-
if (providerPubkey && ev.pubkey !== providerPubkey) return;
|
|
581
|
-
const statusTag = ev.tags.find((t) => t[0] === "status");
|
|
582
|
-
if (statusTag?.[1] === "payment-required") {
|
|
583
|
-
const amtTag = ev.tags.find((t) => t[0] === "amount");
|
|
584
|
-
const amt = amtTag?.[1] ? parseInt(amtTag[1], 10) : 0;
|
|
585
|
-
const paymentReq = amtTag?.[2];
|
|
586
|
-
callbacks.onFeedback?.("payment-required", amt, paymentReq);
|
|
587
|
-
}
|
|
998
|
+
try {
|
|
999
|
+
cb.onResult?.(content, ev.id);
|
|
1000
|
+
} catch {
|
|
1001
|
+
} finally {
|
|
1002
|
+
done();
|
|
588
1003
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
1004
|
+
};
|
|
1005
|
+
try {
|
|
1006
|
+
subs.push(
|
|
1007
|
+
this.pool.subscribe(
|
|
1008
|
+
{
|
|
1009
|
+
kinds: [KIND_JOB_FEEDBACK],
|
|
1010
|
+
"#e": [jid],
|
|
1011
|
+
since
|
|
1012
|
+
},
|
|
1013
|
+
(ev) => {
|
|
1014
|
+
if (resolved) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (provPk && ev.pubkey !== provPk) {
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
const eTag = ev.tags.find((t) => t[0] === "e")?.[1];
|
|
1024
|
+
if (eTag !== jid) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const statusTag = ev.tags.find((t) => t[0] === "status");
|
|
1028
|
+
if (statusTag?.[1]) {
|
|
1029
|
+
const amtTag = ev.tags.find((t) => t[0] === "amount");
|
|
1030
|
+
const amt = safeParseInt(amtTag?.[1]) ?? 0;
|
|
1031
|
+
const paymentReq = amtTag?.[2];
|
|
1032
|
+
try {
|
|
1033
|
+
cb.onFeedback?.(statusTag[1], amt, paymentReq, ev.pubkey);
|
|
1034
|
+
} catch {
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
)
|
|
1039
|
+
);
|
|
1040
|
+
subs.push(
|
|
1041
|
+
this.pool.subscribe(
|
|
1042
|
+
{
|
|
1043
|
+
kinds: resultKinds,
|
|
1044
|
+
"#e": [jid],
|
|
1045
|
+
since
|
|
1046
|
+
},
|
|
1047
|
+
handleResult
|
|
1048
|
+
)
|
|
1049
|
+
);
|
|
1050
|
+
subs.push(
|
|
1051
|
+
this.pool.subscribe(
|
|
1052
|
+
{
|
|
1053
|
+
kinds: resultKinds,
|
|
1054
|
+
"#p": [custPk],
|
|
1055
|
+
"#e": [jid],
|
|
1056
|
+
since
|
|
1057
|
+
},
|
|
1058
|
+
handleResult
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
done();
|
|
1063
|
+
throw err;
|
|
1064
|
+
}
|
|
610
1065
|
timer = setTimeout(() => {
|
|
611
1066
|
if (!resolved) {
|
|
612
1067
|
done();
|
|
613
|
-
|
|
1068
|
+
try {
|
|
1069
|
+
cb.onError?.(`Timed out waiting for response (${timeoutMs / 1e3}s).`);
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
614
1072
|
}
|
|
615
1073
|
}, timeoutMs);
|
|
616
1074
|
return done;
|
|
@@ -632,7 +1090,7 @@ var MarketplaceService = class {
|
|
|
632
1090
|
},
|
|
633
1091
|
identity.secretKey
|
|
634
1092
|
);
|
|
635
|
-
await this.pool.
|
|
1093
|
+
await this.pool.publishAll(event);
|
|
636
1094
|
}
|
|
637
1095
|
/** Submit rating feedback for a job. */
|
|
638
1096
|
async submitFeedback(identity, jobEventId, providerPubkey, positive, capability) {
|
|
@@ -643,7 +1101,9 @@ var MarketplaceService = class {
|
|
|
643
1101
|
["rating", positive ? "1" : "0"],
|
|
644
1102
|
["t", "elisym"]
|
|
645
1103
|
];
|
|
646
|
-
if (capability)
|
|
1104
|
+
if (capability) {
|
|
1105
|
+
tags.push(["t", capability]);
|
|
1106
|
+
}
|
|
647
1107
|
const event = nostrTools.finalizeEvent(
|
|
648
1108
|
{
|
|
649
1109
|
kind: KIND_JOB_FEEDBACK,
|
|
@@ -653,31 +1113,67 @@ var MarketplaceService = class {
|
|
|
653
1113
|
},
|
|
654
1114
|
identity.secretKey
|
|
655
1115
|
);
|
|
656
|
-
await this.pool.
|
|
1116
|
+
await this.pool.publishAll(event);
|
|
657
1117
|
}
|
|
658
1118
|
// --- Provider methods ---
|
|
659
|
-
/**
|
|
1119
|
+
/**
|
|
1120
|
+
* Subscribe to incoming job requests for specific kinds.
|
|
1121
|
+
* Automatically decrypts NIP-44 encrypted content.
|
|
1122
|
+
* Note: decrypted events have modified `content` - do not call `verifyEvent()` on them.
|
|
1123
|
+
* Signature verification is performed before decryption.
|
|
1124
|
+
*/
|
|
660
1125
|
subscribeToJobRequests(identity, kinds, onRequest) {
|
|
661
1126
|
return this.pool.subscribe(
|
|
662
1127
|
{
|
|
663
1128
|
kinds,
|
|
664
1129
|
"#p": [identity.publicKey],
|
|
665
|
-
|
|
1130
|
+
"#t": ["elisym"],
|
|
1131
|
+
since: Math.floor(Date.now() / 1e3) - 5
|
|
666
1132
|
},
|
|
667
|
-
|
|
1133
|
+
(event) => {
|
|
1134
|
+
if (!nostrTools.verifyEvent(event)) {
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
if (isEncrypted(event) && event.content) {
|
|
1138
|
+
try {
|
|
1139
|
+
const decrypted = nip44Decrypt(event.content, identity.secretKey, event.pubkey);
|
|
1140
|
+
onRequest({ ...event, content: decrypted });
|
|
1141
|
+
} catch {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
} else {
|
|
1145
|
+
onRequest(event);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
668
1148
|
);
|
|
669
1149
|
}
|
|
670
1150
|
/** Submit a job result with NIP-44 encrypted content. Result kind is derived from the request kind. */
|
|
671
1151
|
async submitJobResult(identity, requestEvent, content, amount) {
|
|
672
|
-
|
|
673
|
-
|
|
1152
|
+
if (!content) {
|
|
1153
|
+
throw new Error("Job result content must not be empty.");
|
|
1154
|
+
}
|
|
1155
|
+
if (!Number.isInteger(requestEvent.kind)) {
|
|
1156
|
+
throw new Error(`Invalid request event kind: expected integer, got ${requestEvent.kind}.`);
|
|
1157
|
+
}
|
|
1158
|
+
const offset = requestEvent.kind - KIND_JOB_REQUEST_BASE;
|
|
1159
|
+
if (offset < 0 || offset >= 1e3) {
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`Invalid request event kind ${requestEvent.kind}: expected a NIP-90 job request kind (5000-5999).`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
const shouldEncrypt = isEncrypted(requestEvent);
|
|
1165
|
+
const resultContent = shouldEncrypt ? nip44Encrypt(content, identity.secretKey, requestEvent.pubkey) : content;
|
|
1166
|
+
const resultKind = KIND_JOB_RESULT_BASE + offset;
|
|
674
1167
|
const tags = [
|
|
675
1168
|
["e", requestEvent.id],
|
|
676
1169
|
["p", requestEvent.pubkey],
|
|
677
|
-
["t", "elisym"]
|
|
678
|
-
["encrypted", "nip44"]
|
|
1170
|
+
["t", "elisym"]
|
|
679
1171
|
];
|
|
680
|
-
if (
|
|
1172
|
+
if (shouldEncrypt) {
|
|
1173
|
+
tags.push(["encrypted", "nip44"]);
|
|
1174
|
+
}
|
|
1175
|
+
if (amount !== null && amount !== void 0) {
|
|
1176
|
+
assertLamports(amount, "result amount");
|
|
681
1177
|
tags.push(["amount", String(amount)]);
|
|
682
1178
|
}
|
|
683
1179
|
const event = nostrTools.finalizeEvent(
|
|
@@ -685,15 +1181,50 @@ var MarketplaceService = class {
|
|
|
685
1181
|
kind: resultKind,
|
|
686
1182
|
created_at: Math.floor(Date.now() / 1e3),
|
|
687
1183
|
tags,
|
|
688
|
-
content:
|
|
1184
|
+
content: resultContent
|
|
689
1185
|
},
|
|
690
1186
|
identity.secretKey
|
|
691
1187
|
);
|
|
692
|
-
await this.pool.
|
|
1188
|
+
await this.pool.publishAll(event);
|
|
693
1189
|
return event.id;
|
|
694
1190
|
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Submit a job result with retry and exponential backoff.
|
|
1193
|
+
* Retries on publish failures (e.g. relay disconnects).
|
|
1194
|
+
* With maxAttempts=3: try, ~1s, try, ~2s, try, throw.
|
|
1195
|
+
* Jitter: 0.5x-1.0x of calculated delay.
|
|
1196
|
+
*/
|
|
1197
|
+
async submitJobResultWithRetry(identity, requestEvent, content, amount, maxAttempts = DEFAULTS.RESULT_RETRY_COUNT, baseDelayMs = DEFAULTS.RESULT_RETRY_BASE_MS) {
|
|
1198
|
+
const attempts = Math.max(1, maxAttempts);
|
|
1199
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1200
|
+
try {
|
|
1201
|
+
return await this.submitJobResult(identity, requestEvent, content, amount);
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
if (attempt >= attempts - 1) {
|
|
1204
|
+
throw e;
|
|
1205
|
+
}
|
|
1206
|
+
const jitter = 0.5 + Math.random() * 0.5;
|
|
1207
|
+
await new Promise((r) => setTimeout(r, baseDelayMs * Math.pow(2, attempt) * jitter));
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
throw new Error("All delivery attempts failed");
|
|
1211
|
+
}
|
|
695
1212
|
/** Submit payment-required feedback with a payment request. */
|
|
696
1213
|
async submitPaymentRequiredFeedback(identity, requestEvent, amount, paymentRequestJson) {
|
|
1214
|
+
assertLamports(amount, "payment amount");
|
|
1215
|
+
if (amount === 0) {
|
|
1216
|
+
throw new Error("Invalid payment amount: 0. Must be positive.");
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
JSON.parse(paymentRequestJson);
|
|
1220
|
+
} catch {
|
|
1221
|
+
throw new Error("Invalid paymentRequestJson: must be valid JSON.");
|
|
1222
|
+
}
|
|
1223
|
+
if (paymentRequestJson.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
1224
|
+
throw new Error(
|
|
1225
|
+
`paymentRequestJson too long: ${paymentRequestJson.length} chars (max ${LIMITS.MAX_INPUT_LENGTH}).`
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
697
1228
|
const event = nostrTools.finalizeEvent(
|
|
698
1229
|
{
|
|
699
1230
|
kind: KIND_JOB_FEEDBACK,
|
|
@@ -709,74 +1240,193 @@ var MarketplaceService = class {
|
|
|
709
1240
|
},
|
|
710
1241
|
identity.secretKey
|
|
711
1242
|
);
|
|
712
|
-
await this.pool.
|
|
1243
|
+
await this.pool.publishAll(event);
|
|
1244
|
+
}
|
|
1245
|
+
/** Submit processing feedback to notify customer that work has started. */
|
|
1246
|
+
async submitProcessingFeedback(identity, requestEvent) {
|
|
1247
|
+
const event = nostrTools.finalizeEvent(
|
|
1248
|
+
{
|
|
1249
|
+
kind: KIND_JOB_FEEDBACK,
|
|
1250
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1251
|
+
tags: [
|
|
1252
|
+
["e", requestEvent.id],
|
|
1253
|
+
["p", requestEvent.pubkey],
|
|
1254
|
+
["status", "processing"],
|
|
1255
|
+
["t", "elisym"]
|
|
1256
|
+
],
|
|
1257
|
+
content: ""
|
|
1258
|
+
},
|
|
1259
|
+
identity.secretKey
|
|
1260
|
+
);
|
|
1261
|
+
await this.pool.publishAll(event);
|
|
1262
|
+
}
|
|
1263
|
+
/** Submit error feedback to notify customer of a failure. */
|
|
1264
|
+
async submitErrorFeedback(identity, requestEvent, message) {
|
|
1265
|
+
if (!message) {
|
|
1266
|
+
throw new Error("Error message must not be empty.");
|
|
1267
|
+
}
|
|
1268
|
+
if (message.length > LIMITS.MAX_INPUT_LENGTH) {
|
|
1269
|
+
throw new Error(
|
|
1270
|
+
`Error message too long: ${message.length} chars (max ${LIMITS.MAX_INPUT_LENGTH}).`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
const event = nostrTools.finalizeEvent(
|
|
1274
|
+
{
|
|
1275
|
+
kind: KIND_JOB_FEEDBACK,
|
|
1276
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1277
|
+
tags: [
|
|
1278
|
+
["e", requestEvent.id],
|
|
1279
|
+
["p", requestEvent.pubkey],
|
|
1280
|
+
["status", "error"],
|
|
1281
|
+
["t", "elisym"]
|
|
1282
|
+
],
|
|
1283
|
+
content: message
|
|
1284
|
+
},
|
|
1285
|
+
identity.secretKey
|
|
1286
|
+
);
|
|
1287
|
+
await this.pool.publishAll(event);
|
|
1288
|
+
}
|
|
1289
|
+
/** Query job results by request IDs and decrypt NIP-44 content. */
|
|
1290
|
+
async queryJobResults(identity, requestIds, kindOffsets, providerPubkey) {
|
|
1291
|
+
const offsets = kindOffsets ?? [DEFAULT_KIND_OFFSET];
|
|
1292
|
+
if (offsets.length === 0) {
|
|
1293
|
+
throw new Error("kindOffsets must not be empty.");
|
|
1294
|
+
}
|
|
1295
|
+
const resultKinds = offsets.map(jobResultKind);
|
|
1296
|
+
const results = await this.pool.queryBatchedByTag(
|
|
1297
|
+
{ kinds: resultKinds },
|
|
1298
|
+
"e",
|
|
1299
|
+
requestIds
|
|
1300
|
+
);
|
|
1301
|
+
const resultByRequest = /* @__PURE__ */ new Map();
|
|
1302
|
+
const createdAtByRequest = /* @__PURE__ */ new Map();
|
|
1303
|
+
for (const r of results) {
|
|
1304
|
+
if (!nostrTools.verifyEvent(r)) {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
if (providerPubkey && r.pubkey !== providerPubkey) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const eTag = r.tags.find((t) => t[0] === "e");
|
|
1311
|
+
if (!eTag?.[1]) {
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const prevTs = createdAtByRequest.get(eTag[1]) ?? 0;
|
|
1315
|
+
if (r.created_at < prevTs) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
const amtTag = r.tags.find((t) => t[0] === "amount");
|
|
1319
|
+
let content = r.content;
|
|
1320
|
+
let decryptionFailed = false;
|
|
1321
|
+
if (isEncrypted(r)) {
|
|
1322
|
+
try {
|
|
1323
|
+
content = nip44Decrypt(r.content, identity.secretKey, r.pubkey);
|
|
1324
|
+
} catch {
|
|
1325
|
+
content = "";
|
|
1326
|
+
decryptionFailed = true;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
createdAtByRequest.set(eTag[1], r.created_at);
|
|
1330
|
+
resultByRequest.set(eTag[1], {
|
|
1331
|
+
content,
|
|
1332
|
+
amount: safeParseInt(amtTag?.[1]),
|
|
1333
|
+
senderPubkey: r.pubkey,
|
|
1334
|
+
decryptionFailed
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
return resultByRequest;
|
|
713
1338
|
}
|
|
714
1339
|
// --- Query methods ---
|
|
715
|
-
/**
|
|
1340
|
+
/**
|
|
1341
|
+
* Fetch recent jobs from the network.
|
|
1342
|
+
* NOTE: Job.result contains raw event content. For encrypted jobs,
|
|
1343
|
+
* this will be NIP-44 ciphertext - use queryJobResults() for decryption.
|
|
1344
|
+
*/
|
|
716
1345
|
async fetchRecentJobs(agentPubkeys, limit, since, kindOffsets) {
|
|
717
1346
|
const offsets = kindOffsets ?? [DEFAULT_KIND_OFFSET];
|
|
1347
|
+
if (offsets.length === 0) {
|
|
1348
|
+
throw new Error("kindOffsets must not be empty.");
|
|
1349
|
+
}
|
|
718
1350
|
const requestKinds = offsets.map(jobRequestKind);
|
|
719
1351
|
const resultKinds = offsets.map(jobResultKind);
|
|
720
1352
|
const reqFilter = {
|
|
721
1353
|
kinds: requestKinds,
|
|
722
1354
|
"#t": ["elisym"],
|
|
723
|
-
...limit
|
|
724
|
-
...since
|
|
1355
|
+
...limit !== null && limit !== void 0 && { limit },
|
|
1356
|
+
...since !== null && since !== void 0 && { since }
|
|
725
1357
|
};
|
|
726
|
-
const
|
|
1358
|
+
const rawRequests = await this.pool.querySync(reqFilter);
|
|
1359
|
+
const requests = rawRequests.filter(nostrTools.verifyEvent);
|
|
727
1360
|
const requestIds = requests.map((r) => r.id);
|
|
728
1361
|
let results = [];
|
|
729
1362
|
let feedbacks = [];
|
|
730
1363
|
if (requestIds.length > 0) {
|
|
731
|
-
const [
|
|
732
|
-
this.pool.queryBatchedByTag(
|
|
733
|
-
|
|
734
|
-
"e",
|
|
735
|
-
requestIds
|
|
736
|
-
),
|
|
737
|
-
this.pool.queryBatchedByTag(
|
|
738
|
-
{ kinds: [KIND_JOB_FEEDBACK] },
|
|
739
|
-
"e",
|
|
740
|
-
requestIds
|
|
741
|
-
)
|
|
1364
|
+
const [rawResults, rawFeedbacks] = await Promise.all([
|
|
1365
|
+
this.pool.queryBatchedByTag({ kinds: resultKinds }, "e", requestIds),
|
|
1366
|
+
this.pool.queryBatchedByTag({ kinds: [KIND_JOB_FEEDBACK] }, "e", requestIds)
|
|
742
1367
|
]);
|
|
743
|
-
results =
|
|
744
|
-
feedbacks =
|
|
1368
|
+
results = rawResults.filter(nostrTools.verifyEvent);
|
|
1369
|
+
feedbacks = rawFeedbacks.filter(nostrTools.verifyEvent);
|
|
745
1370
|
}
|
|
746
1371
|
const targetedAgentByRequest = /* @__PURE__ */ new Map();
|
|
747
1372
|
for (const req of requests) {
|
|
748
1373
|
const pTag = req.tags.find((t) => t[0] === "p");
|
|
749
|
-
if (pTag?.[1])
|
|
1374
|
+
if (pTag?.[1]) {
|
|
1375
|
+
targetedAgentByRequest.set(req.id, pTag[1]);
|
|
1376
|
+
}
|
|
750
1377
|
}
|
|
751
1378
|
const resultsByRequest = /* @__PURE__ */ new Map();
|
|
752
1379
|
for (const r of results) {
|
|
753
1380
|
const reqId = resolveRequestId(r);
|
|
754
|
-
if (!reqId)
|
|
1381
|
+
if (!reqId) {
|
|
1382
|
+
continue;
|
|
1383
|
+
}
|
|
755
1384
|
const targeted = targetedAgentByRequest.get(reqId);
|
|
756
|
-
if (targeted && r.pubkey !== targeted)
|
|
1385
|
+
if (targeted && r.pubkey !== targeted) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
757
1388
|
const existing = resultsByRequest.get(reqId);
|
|
758
|
-
if (!existing ||
|
|
1389
|
+
if (!existing || r.created_at > existing.created_at) {
|
|
759
1390
|
resultsByRequest.set(reqId, r);
|
|
760
1391
|
}
|
|
761
1392
|
}
|
|
762
1393
|
const feedbackByRequest = /* @__PURE__ */ new Map();
|
|
763
1394
|
for (const f of feedbacks) {
|
|
764
1395
|
const reqId = resolveRequestId(f);
|
|
765
|
-
if (!reqId)
|
|
1396
|
+
if (!reqId) {
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
766
1399
|
const targeted = targetedAgentByRequest.get(reqId);
|
|
767
|
-
if (targeted && f.pubkey !== targeted)
|
|
1400
|
+
if (targeted && f.pubkey !== targeted) {
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
768
1403
|
const existing = feedbackByRequest.get(reqId);
|
|
769
|
-
if (!existing ||
|
|
1404
|
+
if (!existing || f.created_at > existing.created_at) {
|
|
770
1405
|
feedbackByRequest.set(reqId, f);
|
|
771
1406
|
}
|
|
772
1407
|
}
|
|
1408
|
+
const feedbacksByRequestId = /* @__PURE__ */ new Map();
|
|
1409
|
+
for (const f of feedbacks) {
|
|
1410
|
+
const reqId = resolveRequestId(f);
|
|
1411
|
+
if (!reqId) {
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
const arr = feedbacksByRequestId.get(reqId);
|
|
1415
|
+
if (arr) {
|
|
1416
|
+
arr.push(f);
|
|
1417
|
+
} else {
|
|
1418
|
+
feedbacksByRequestId.set(reqId, [f]);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
773
1421
|
const jobs = [];
|
|
774
1422
|
for (const req of requests) {
|
|
775
1423
|
const result = resultsByRequest.get(req.id);
|
|
776
1424
|
const feedback = feedbackByRequest.get(req.id);
|
|
777
1425
|
const jobAgentPubkey = result?.pubkey ?? feedback?.pubkey;
|
|
778
1426
|
if (agentPubkeys && agentPubkeys.size > 0 && jobAgentPubkey) {
|
|
779
|
-
if (!agentPubkeys.has(jobAgentPubkey))
|
|
1427
|
+
if (!agentPubkeys.has(jobAgentPubkey)) {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
780
1430
|
}
|
|
781
1431
|
const capability = req.tags.find((t) => t[0] === "t" && t[1] !== "elisym")?.[1];
|
|
782
1432
|
const bid = req.tags.find((t) => t[0] === "bid")?.[1];
|
|
@@ -786,11 +1436,9 @@ var MarketplaceService = class {
|
|
|
786
1436
|
if (result) {
|
|
787
1437
|
status = "success";
|
|
788
1438
|
const amtTag = result.tags.find((t) => t[0] === "amount");
|
|
789
|
-
|
|
1439
|
+
amount = safeParseInt(amtTag?.[1]);
|
|
790
1440
|
}
|
|
791
|
-
const allFeedbacksForReq =
|
|
792
|
-
(f) => resolveRequestId(f) === req.id
|
|
793
|
-
);
|
|
1441
|
+
const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
|
|
794
1442
|
for (const fb of allFeedbacksForReq) {
|
|
795
1443
|
const txTag = fb.tags.find((t) => t[0] === "tx");
|
|
796
1444
|
if (txTag?.[1]) {
|
|
@@ -804,13 +1452,13 @@ var MarketplaceService = class {
|
|
|
804
1452
|
if (statusTag?.[1]) {
|
|
805
1453
|
const isTargeted = targetedAgentByRequest.has(req.id);
|
|
806
1454
|
if (statusTag[1] === "payment-required" && !bid && !isTargeted) ; else {
|
|
807
|
-
status = statusTag[1];
|
|
1455
|
+
status = toJobStatus(statusTag[1]);
|
|
808
1456
|
}
|
|
809
1457
|
}
|
|
810
1458
|
}
|
|
811
1459
|
if (!amount) {
|
|
812
1460
|
const amtTag = feedback.tags.find((t) => t[0] === "amount");
|
|
813
|
-
|
|
1461
|
+
amount = safeParseInt(amtTag?.[1]);
|
|
814
1462
|
}
|
|
815
1463
|
}
|
|
816
1464
|
jobs.push({
|
|
@@ -818,7 +1466,7 @@ var MarketplaceService = class {
|
|
|
818
1466
|
customer: req.pubkey,
|
|
819
1467
|
agentPubkey: jobAgentPubkey,
|
|
820
1468
|
capability,
|
|
821
|
-
bid:
|
|
1469
|
+
bid: safeParseInt(bid),
|
|
822
1470
|
status,
|
|
823
1471
|
result: result?.content,
|
|
824
1472
|
resultEventId: result?.id,
|
|
@@ -837,96 +1485,285 @@ var MarketplaceService = class {
|
|
|
837
1485
|
"#t": ["elisym"],
|
|
838
1486
|
since: Math.floor(Date.now() / 1e3)
|
|
839
1487
|
},
|
|
840
|
-
|
|
1488
|
+
(event) => {
|
|
1489
|
+
if (!nostrTools.verifyEvent(event)) {
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
onEvent(event);
|
|
1493
|
+
}
|
|
841
1494
|
);
|
|
842
1495
|
}
|
|
843
1496
|
};
|
|
844
|
-
var
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
this.
|
|
1497
|
+
var KIND_HTTP_AUTH = 27235;
|
|
1498
|
+
var DEFAULT_UPLOAD_URL = "https://nostr.build/api/v2/upload/files";
|
|
1499
|
+
var MediaService = class {
|
|
1500
|
+
constructor(uploadUrl = DEFAULT_UPLOAD_URL) {
|
|
1501
|
+
this.uploadUrl = uploadUrl;
|
|
849
1502
|
}
|
|
850
|
-
sessionIdentity;
|
|
851
|
-
pingCache = /* @__PURE__ */ new Map();
|
|
852
|
-
// pubkey → timestamp of last online result
|
|
853
|
-
pendingPings = /* @__PURE__ */ new Map();
|
|
854
|
-
// dedup in-flight pings
|
|
855
|
-
static PING_CACHE_TTL = 3e4;
|
|
856
1503
|
/**
|
|
857
|
-
*
|
|
858
|
-
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1504
|
+
* Upload a file with NIP-98 authentication.
|
|
1505
|
+
* Works with browser File objects and Node.js/Bun Blobs.
|
|
1506
|
+
*
|
|
1507
|
+
* @param identity - Nostr identity used to sign the NIP-98 auth event.
|
|
1508
|
+
* @param file - File or Blob to upload.
|
|
1509
|
+
* @param filename - Optional filename for the upload (defaults to "upload").
|
|
1510
|
+
* @returns URL of the uploaded file.
|
|
861
1511
|
*/
|
|
862
|
-
async
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1512
|
+
async upload(identity, file, filename) {
|
|
1513
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", await file.arrayBuffer());
|
|
1514
|
+
const hashHex = [...new Uint8Array(hashBuffer)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1515
|
+
const authEvent = nostrTools.finalizeEvent(
|
|
1516
|
+
{
|
|
1517
|
+
kind: KIND_HTTP_AUTH,
|
|
1518
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1519
|
+
tags: [
|
|
1520
|
+
["u", this.uploadUrl],
|
|
1521
|
+
["method", "POST"],
|
|
1522
|
+
["payload", hashHex]
|
|
1523
|
+
],
|
|
1524
|
+
content: ""
|
|
1525
|
+
},
|
|
1526
|
+
identity.secretKey
|
|
1527
|
+
);
|
|
1528
|
+
const authHeader = "Nostr " + btoa(JSON.stringify(authEvent));
|
|
1529
|
+
const formData = new FormData();
|
|
1530
|
+
formData.append("file", file, filename ?? "upload");
|
|
1531
|
+
const controller = new AbortController();
|
|
1532
|
+
const timer = setTimeout(() => controller.abort(), 3e4);
|
|
1533
|
+
try {
|
|
1534
|
+
const res = await fetch(this.uploadUrl, {
|
|
1535
|
+
method: "POST",
|
|
1536
|
+
headers: { Authorization: authHeader },
|
|
1537
|
+
body: formData,
|
|
1538
|
+
signal: controller.signal
|
|
1539
|
+
});
|
|
1540
|
+
if (!res.ok) {
|
|
1541
|
+
throw new Error(`Upload failed: ${res.status} ${res.statusText}`);
|
|
1542
|
+
}
|
|
1543
|
+
let data;
|
|
1544
|
+
try {
|
|
1545
|
+
data = await res.json();
|
|
1546
|
+
} catch {
|
|
1547
|
+
throw new Error("Invalid response from upload service.");
|
|
1548
|
+
}
|
|
1549
|
+
const url = data?.data?.[0]?.url;
|
|
1550
|
+
if (!url) {
|
|
1551
|
+
throw new Error("No URL returned from upload service.");
|
|
1552
|
+
}
|
|
1553
|
+
return url;
|
|
1554
|
+
} finally {
|
|
1555
|
+
clearTimeout(timer);
|
|
872
1556
|
}
|
|
873
|
-
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
// src/primitives/bounded-set.ts
|
|
1561
|
+
var BoundedSet = class {
|
|
1562
|
+
constructor(maxSize) {
|
|
1563
|
+
this.maxSize = maxSize;
|
|
1564
|
+
if (maxSize <= 0) {
|
|
1565
|
+
throw new Error("BoundedSet maxSize must be positive.");
|
|
1566
|
+
}
|
|
1567
|
+
this.items = new Array(maxSize);
|
|
1568
|
+
}
|
|
1569
|
+
items;
|
|
1570
|
+
set = /* @__PURE__ */ new Set();
|
|
1571
|
+
head = 0;
|
|
1572
|
+
count = 0;
|
|
1573
|
+
has(item) {
|
|
1574
|
+
return this.set.has(item);
|
|
1575
|
+
}
|
|
1576
|
+
add(item) {
|
|
1577
|
+
if (this.set.has(item)) {
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (this.count >= this.maxSize) {
|
|
1581
|
+
const evicted = this.items[this.head];
|
|
1582
|
+
this.set.delete(evicted);
|
|
1583
|
+
} else {
|
|
1584
|
+
this.count++;
|
|
1585
|
+
}
|
|
1586
|
+
this.items[this.head] = item;
|
|
1587
|
+
this.head = (this.head + 1) % this.maxSize;
|
|
1588
|
+
this.set.add(item);
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
var ElisymIdentity = class _ElisymIdentity {
|
|
1592
|
+
_secretKey;
|
|
1593
|
+
publicKey;
|
|
1594
|
+
npub;
|
|
1595
|
+
get secretKey() {
|
|
1596
|
+
return new Uint8Array(this._secretKey);
|
|
1597
|
+
}
|
|
1598
|
+
constructor(secretKey) {
|
|
1599
|
+
this._secretKey = new Uint8Array(secretKey);
|
|
1600
|
+
this.publicKey = nostrTools.getPublicKey(secretKey);
|
|
1601
|
+
this.npub = nostrTools.nip19.npubEncode(this.publicKey);
|
|
1602
|
+
}
|
|
1603
|
+
static generate() {
|
|
1604
|
+
return new _ElisymIdentity(nostrTools.generateSecretKey());
|
|
1605
|
+
}
|
|
1606
|
+
static fromSecretKey(sk) {
|
|
1607
|
+
if (sk.length !== 32) {
|
|
1608
|
+
throw new Error("Secret key must be exactly 32 bytes.");
|
|
1609
|
+
}
|
|
1610
|
+
return new _ElisymIdentity(sk);
|
|
1611
|
+
}
|
|
1612
|
+
toJSON() {
|
|
1613
|
+
return { publicKey: this.publicKey, npub: this.npub };
|
|
1614
|
+
}
|
|
1615
|
+
/** Best-effort scrub of the secret key bytes in memory. */
|
|
1616
|
+
scrub() {
|
|
1617
|
+
this._secretKey.fill(0);
|
|
1618
|
+
}
|
|
1619
|
+
static fromHex(hex) {
|
|
1620
|
+
if (hex.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
1621
|
+
throw new Error("Invalid secret key hex: expected 64 hex characters (32 bytes).");
|
|
1622
|
+
}
|
|
1623
|
+
const bytes = new Uint8Array(32);
|
|
1624
|
+
for (let i = 0; i < 64; i += 2) {
|
|
1625
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
1626
|
+
}
|
|
1627
|
+
return new _ElisymIdentity(bytes);
|
|
1628
|
+
}
|
|
1629
|
+
};
|
|
1630
|
+
|
|
1631
|
+
// src/services/messaging.ts
|
|
1632
|
+
var MessagingService = class _MessagingService {
|
|
1633
|
+
// dedup in-flight pings
|
|
1634
|
+
constructor(pool) {
|
|
1635
|
+
this.pool = pool;
|
|
1636
|
+
this.sessionIdentity = ElisymIdentity.generate();
|
|
1637
|
+
}
|
|
1638
|
+
static PING_CACHE_MAX = 1e3;
|
|
1639
|
+
sessionIdentity;
|
|
1640
|
+
pingCache = /* @__PURE__ */ new Map();
|
|
1641
|
+
// pubkey - timestamp of last online result
|
|
1642
|
+
pendingPings = /* @__PURE__ */ new Map();
|
|
1643
|
+
/**
|
|
1644
|
+
* Ping an agent via ephemeral Nostr events (kind 20200/20201).
|
|
1645
|
+
* Uses a persistent session identity to avoid relay rate-limiting.
|
|
1646
|
+
* Publishes to ALL relays for maximum delivery reliability.
|
|
1647
|
+
* Caches results for 30s to prevent redundant publishes.
|
|
1648
|
+
*/
|
|
1649
|
+
async pingAgent(agentPubkey, timeoutMs = DEFAULTS.PING_TIMEOUT_MS, signal, retries = DEFAULTS.PING_RETRIES) {
|
|
1650
|
+
const cachedAt = this.pingCache.get(agentPubkey);
|
|
1651
|
+
if (cachedAt) {
|
|
1652
|
+
if (Date.now() - cachedAt < DEFAULTS.PING_CACHE_TTL_MS) {
|
|
1653
|
+
return { online: true, identity: this.sessionIdentity };
|
|
1654
|
+
}
|
|
1655
|
+
this.pingCache.delete(agentPubkey);
|
|
1656
|
+
}
|
|
1657
|
+
if (this.pingCache.size > _MessagingService.PING_CACHE_MAX / 2) {
|
|
1658
|
+
const now = Date.now();
|
|
1659
|
+
for (const [key, ts] of this.pingCache) {
|
|
1660
|
+
if (now - ts >= DEFAULTS.PING_CACHE_TTL_MS) {
|
|
1661
|
+
this.pingCache.delete(key);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
const pending = this.pendingPings.get(agentPubkey);
|
|
1666
|
+
if (pending) {
|
|
1667
|
+
return pending;
|
|
1668
|
+
}
|
|
1669
|
+
if (this.pendingPings.size >= _MessagingService.PING_CACHE_MAX) {
|
|
1670
|
+
return { online: false, identity: null };
|
|
1671
|
+
}
|
|
1672
|
+
const promise = this._doPingWithRetry(agentPubkey, timeoutMs, retries, signal);
|
|
874
1673
|
this.pendingPings.set(agentPubkey, promise);
|
|
875
1674
|
promise.finally(() => this.pendingPings.delete(agentPubkey));
|
|
876
1675
|
return promise;
|
|
877
1676
|
}
|
|
1677
|
+
async _doPingWithRetry(agentPubkey, timeoutMs, retries, signal) {
|
|
1678
|
+
const attempts = retries + 1;
|
|
1679
|
+
const perAttemptTimeout = Math.floor(timeoutMs / attempts);
|
|
1680
|
+
for (let i = 0; i < attempts; i++) {
|
|
1681
|
+
if (signal?.aborted) {
|
|
1682
|
+
return { online: false, identity: null };
|
|
1683
|
+
}
|
|
1684
|
+
const result = await this._doPing(agentPubkey, perAttemptTimeout, signal);
|
|
1685
|
+
if (result.online) {
|
|
1686
|
+
return result;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return { online: false, identity: null };
|
|
1690
|
+
}
|
|
878
1691
|
async _doPing(agentPubkey, timeoutMs, signal) {
|
|
879
1692
|
const sk = this.sessionIdentity.secretKey;
|
|
880
1693
|
const pk = this.sessionIdentity.publicKey;
|
|
881
1694
|
const nonce = crypto.getRandomValues(new Uint8Array(16)).reduce((s, b) => s + b.toString(16).padStart(2, "0"), "");
|
|
882
|
-
const shortNonce = nonce.slice(0, 8);
|
|
883
|
-
const shortAgent = agentPubkey.slice(0, 8);
|
|
884
|
-
console.log(`[ping] \u2192 ping ${shortAgent} nonce=${shortNonce}`);
|
|
885
1695
|
if (signal?.aborted) {
|
|
886
1696
|
return { online: false, identity: null };
|
|
887
1697
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1698
|
+
let resolved = false;
|
|
1699
|
+
let sub;
|
|
1700
|
+
let timer;
|
|
1701
|
+
let resolvePing;
|
|
1702
|
+
const promise = new Promise((resolve) => {
|
|
1703
|
+
resolvePing = resolve;
|
|
1704
|
+
});
|
|
1705
|
+
const done = (online) => {
|
|
1706
|
+
if (resolved) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
resolved = true;
|
|
1710
|
+
if (timer) {
|
|
893
1711
|
clearTimeout(timer);
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
(ev) => {
|
|
905
|
-
try {
|
|
906
|
-
const msg = JSON.parse(ev.content);
|
|
907
|
-
if (msg.type === "elisym_pong" && msg.nonce === nonce) {
|
|
908
|
-
console.log(`[ping] \u2190 pong from ${ev.pubkey.slice(0, 8)} nonce=${shortNonce}`);
|
|
909
|
-
done(true, "pong matched");
|
|
910
|
-
}
|
|
911
|
-
} catch {
|
|
1712
|
+
}
|
|
1713
|
+
sub?.close();
|
|
1714
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1715
|
+
if (online) {
|
|
1716
|
+
this.pingCache.delete(agentPubkey);
|
|
1717
|
+
this.pingCache.set(agentPubkey, Date.now());
|
|
1718
|
+
if (this.pingCache.size > _MessagingService.PING_CACHE_MAX) {
|
|
1719
|
+
const oldest = this.pingCache.keys().next().value;
|
|
1720
|
+
if (oldest !== void 0) {
|
|
1721
|
+
this.pingCache.delete(oldest);
|
|
912
1722
|
}
|
|
913
1723
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
1724
|
+
}
|
|
1725
|
+
resolvePing({ online, identity: online ? this.sessionIdentity : null });
|
|
1726
|
+
};
|
|
1727
|
+
const onAbort = () => done(false);
|
|
1728
|
+
signal?.addEventListener("abort", onAbort);
|
|
1729
|
+
timer = setTimeout(() => done(false), timeoutMs);
|
|
1730
|
+
try {
|
|
1731
|
+
sub = await this.pool.subscribeAndWait({ kinds: [KIND_PONG], "#p": [pk] }, (ev) => {
|
|
1732
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
if (ev.pubkey !== agentPubkey) {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
try {
|
|
1739
|
+
const msg = JSON.parse(ev.content);
|
|
1740
|
+
if (msg.type === "elisym_pong" && typeof msg.nonce === "string" && msg.nonce.length === 32 && msg.nonce === nonce) {
|
|
1741
|
+
done(true);
|
|
1742
|
+
}
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
927
1745
|
});
|
|
928
|
-
|
|
1746
|
+
} catch {
|
|
1747
|
+
done(false);
|
|
1748
|
+
return promise;
|
|
1749
|
+
}
|
|
1750
|
+
if (resolved) {
|
|
1751
|
+
sub?.close();
|
|
1752
|
+
return promise;
|
|
1753
|
+
}
|
|
1754
|
+
const pingEvent = nostrTools.finalizeEvent(
|
|
1755
|
+
{
|
|
1756
|
+
kind: KIND_PING,
|
|
1757
|
+
created_at: Math.floor(Date.now() / 1e3),
|
|
1758
|
+
tags: [["p", agentPubkey]],
|
|
1759
|
+
content: JSON.stringify({ type: "elisym_ping", nonce })
|
|
1760
|
+
},
|
|
1761
|
+
sk
|
|
1762
|
+
);
|
|
1763
|
+
this.pool.publishAll(pingEvent).catch(() => {
|
|
1764
|
+
done(false);
|
|
929
1765
|
});
|
|
1766
|
+
return promise;
|
|
930
1767
|
}
|
|
931
1768
|
/**
|
|
932
1769
|
* Subscribe to incoming ephemeral ping events (kind 20200).
|
|
@@ -936,9 +1773,12 @@ var MessagingService = class _MessagingService {
|
|
|
936
1773
|
return this.pool.subscribe(
|
|
937
1774
|
{ kinds: [KIND_PING], "#p": [identity.publicKey] },
|
|
938
1775
|
(ev) => {
|
|
1776
|
+
if (!nostrTools.verifyEvent(ev)) {
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
939
1779
|
try {
|
|
940
1780
|
const msg = JSON.parse(ev.content);
|
|
941
|
-
if (msg.type === "elisym_ping" && msg.nonce) {
|
|
1781
|
+
if (msg.type === "elisym_ping" && typeof msg.nonce === "string" && msg.nonce.length === 32) {
|
|
942
1782
|
onPing(ev.pubkey, msg.nonce);
|
|
943
1783
|
}
|
|
944
1784
|
} catch {
|
|
@@ -961,11 +1801,15 @@ var MessagingService = class _MessagingService {
|
|
|
961
1801
|
}
|
|
962
1802
|
/** Send a NIP-17 DM. */
|
|
963
1803
|
async sendMessage(identity, recipientPubkey, content) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1804
|
+
if (!/^[0-9a-f]{64}$/.test(recipientPubkey)) {
|
|
1805
|
+
throw new Error("Invalid recipient pubkey: expected 64 hex characters.");
|
|
1806
|
+
}
|
|
1807
|
+
if (content.length > LIMITS.MAX_MESSAGE_LENGTH) {
|
|
1808
|
+
throw new Error(
|
|
1809
|
+
`Message too long: ${content.length} chars (max ${LIMITS.MAX_MESSAGE_LENGTH}).`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
const wrap = nip17__namespace.wrapEvent(identity.secretKey, { publicKey: recipientPubkey }, content);
|
|
969
1813
|
await this.pool.publish(wrap);
|
|
970
1814
|
}
|
|
971
1815
|
/** Fetch historical NIP-17 DMs from relays. Returns decrypted messages sorted by time. */
|
|
@@ -975,12 +1819,17 @@ var MessagingService = class _MessagingService {
|
|
|
975
1819
|
"#p": [identity.publicKey],
|
|
976
1820
|
since
|
|
977
1821
|
});
|
|
978
|
-
const seen =
|
|
1822
|
+
const seen = new BoundedSet(1e4);
|
|
979
1823
|
const messages = [];
|
|
980
1824
|
for (const ev of events) {
|
|
981
1825
|
try {
|
|
982
1826
|
const rumor = nip59__namespace.unwrapEvent(ev, identity.secretKey);
|
|
983
|
-
if (
|
|
1827
|
+
if (rumor.kind !== 14) {
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
if (seen.has(rumor.id)) {
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
984
1833
|
seen.add(rumor.id);
|
|
985
1834
|
messages.push({
|
|
986
1835
|
senderPubkey: rumor.pubkey,
|
|
@@ -995,134 +1844,312 @@ var MessagingService = class _MessagingService {
|
|
|
995
1844
|
}
|
|
996
1845
|
/** Subscribe to incoming NIP-17 DMs. */
|
|
997
1846
|
subscribeToMessages(identity, onMessage, since) {
|
|
998
|
-
const seen =
|
|
1847
|
+
const seen = new BoundedSet(1e4);
|
|
999
1848
|
const filter = {
|
|
1000
1849
|
kinds: [KIND_GIFT_WRAP],
|
|
1001
1850
|
"#p": [identity.publicKey]
|
|
1002
1851
|
};
|
|
1003
|
-
if (since !== void 0)
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1852
|
+
if (since !== void 0) {
|
|
1853
|
+
filter.since = since;
|
|
1854
|
+
}
|
|
1855
|
+
return this.pool.subscribe(filter, (ev) => {
|
|
1856
|
+
try {
|
|
1857
|
+
const rumor = nip59__namespace.unwrapEvent(ev, identity.secretKey);
|
|
1858
|
+
if (rumor.kind !== 14) {
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (seen.has(rumor.id)) {
|
|
1862
|
+
return;
|
|
1013
1863
|
}
|
|
1864
|
+
seen.add(rumor.id);
|
|
1865
|
+
onMessage(rumor.pubkey, rumor.content, rumor.created_at, rumor.id);
|
|
1866
|
+
} catch {
|
|
1014
1867
|
}
|
|
1015
|
-
);
|
|
1868
|
+
});
|
|
1016
1869
|
}
|
|
1017
1870
|
};
|
|
1018
|
-
var
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
return new Decimal__default.default(amount).mul(PROTOCOL_FEE_BPS).div(1e4).toDecimalPlaces(0, Decimal__default.default.ROUND_CEIL).toNumber();
|
|
1871
|
+
var NostrPool = class {
|
|
1872
|
+
pool;
|
|
1873
|
+
relays;
|
|
1874
|
+
activeSubscriptions = /* @__PURE__ */ new Set();
|
|
1875
|
+
constructor(relays = RELAYS) {
|
|
1876
|
+
this.pool = new nostrTools.SimplePool();
|
|
1877
|
+
this.relays = relays;
|
|
1026
1878
|
}
|
|
1027
|
-
/**
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1879
|
+
/** Query relays synchronously. Returns `[]` on timeout (no error thrown). */
|
|
1880
|
+
async querySync(filter) {
|
|
1881
|
+
let timer;
|
|
1882
|
+
const query = this.pool.querySync(this.relays, filter);
|
|
1883
|
+
query.catch(() => {
|
|
1884
|
+
});
|
|
1033
1885
|
try {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1886
|
+
const result = await Promise.race([
|
|
1887
|
+
query,
|
|
1888
|
+
new Promise((resolve) => {
|
|
1889
|
+
timer = setTimeout(() => resolve([]), DEFAULTS.QUERY_TIMEOUT_MS);
|
|
1890
|
+
})
|
|
1891
|
+
]);
|
|
1892
|
+
return result;
|
|
1893
|
+
} finally {
|
|
1894
|
+
clearTimeout(timer);
|
|
1037
1895
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1896
|
+
}
|
|
1897
|
+
async queryBatched(filter, keys, batchSize = DEFAULTS.BATCH_SIZE, maxConcurrency = DEFAULTS.QUERY_MAX_CONCURRENCY) {
|
|
1898
|
+
const batchKeys = [];
|
|
1899
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
1900
|
+
batchKeys.push(keys.slice(i, i + batchSize));
|
|
1901
|
+
}
|
|
1902
|
+
const results = [];
|
|
1903
|
+
for (let c = 0; c < batchKeys.length; c += maxConcurrency) {
|
|
1904
|
+
const chunk = batchKeys.slice(c, c + maxConcurrency);
|
|
1905
|
+
const chunkResults = await Promise.all(
|
|
1906
|
+
chunk.map((batch) => {
|
|
1907
|
+
let timer;
|
|
1908
|
+
const query = this.pool.querySync(this.relays, {
|
|
1909
|
+
...filter,
|
|
1910
|
+
authors: batch
|
|
1911
|
+
});
|
|
1912
|
+
query.catch(() => {
|
|
1913
|
+
});
|
|
1914
|
+
return (async () => {
|
|
1915
|
+
try {
|
|
1916
|
+
return await Promise.race([
|
|
1917
|
+
query,
|
|
1918
|
+
new Promise((resolve) => {
|
|
1919
|
+
timer = setTimeout(() => resolve([]), DEFAULTS.QUERY_TIMEOUT_MS);
|
|
1920
|
+
})
|
|
1921
|
+
]);
|
|
1922
|
+
} finally {
|
|
1923
|
+
clearTimeout(timer);
|
|
1924
|
+
}
|
|
1925
|
+
})();
|
|
1926
|
+
})
|
|
1927
|
+
);
|
|
1928
|
+
results.push(...chunkResults.flat());
|
|
1040
1929
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1930
|
+
return results;
|
|
1931
|
+
}
|
|
1932
|
+
async queryBatchedByTag(filter, tagName, values, batchSize = DEFAULTS.BATCH_SIZE, maxConcurrency = DEFAULTS.QUERY_MAX_CONCURRENCY) {
|
|
1933
|
+
const batchValues = [];
|
|
1934
|
+
for (let i = 0; i < values.length; i += batchSize) {
|
|
1935
|
+
batchValues.push(values.slice(i, i + batchSize));
|
|
1936
|
+
}
|
|
1937
|
+
const results = [];
|
|
1938
|
+
for (let c = 0; c < batchValues.length; c += maxConcurrency) {
|
|
1939
|
+
const chunk = batchValues.slice(c, c + maxConcurrency);
|
|
1940
|
+
const chunkResults = await Promise.all(
|
|
1941
|
+
chunk.map((batch) => {
|
|
1942
|
+
let timer;
|
|
1943
|
+
const query = this.pool.querySync(this.relays, {
|
|
1944
|
+
...filter,
|
|
1945
|
+
[`#${tagName}`]: batch
|
|
1946
|
+
});
|
|
1947
|
+
query.catch(() => {
|
|
1948
|
+
});
|
|
1949
|
+
return (async () => {
|
|
1950
|
+
try {
|
|
1951
|
+
return await Promise.race([
|
|
1952
|
+
query,
|
|
1953
|
+
new Promise((resolve) => {
|
|
1954
|
+
timer = setTimeout(() => resolve([]), DEFAULTS.QUERY_TIMEOUT_MS);
|
|
1955
|
+
})
|
|
1956
|
+
]);
|
|
1957
|
+
} finally {
|
|
1958
|
+
clearTimeout(timer);
|
|
1959
|
+
}
|
|
1960
|
+
})();
|
|
1961
|
+
})
|
|
1962
|
+
);
|
|
1963
|
+
results.push(...chunkResults.flat());
|
|
1964
|
+
}
|
|
1965
|
+
return results;
|
|
1966
|
+
}
|
|
1967
|
+
async publish(event) {
|
|
1968
|
+
try {
|
|
1969
|
+
await Promise.any(this.pool.publish(this.relays, event));
|
|
1970
|
+
} catch (err) {
|
|
1971
|
+
if (err instanceof AggregateError) {
|
|
1972
|
+
throw new Error(
|
|
1973
|
+
`Failed to publish to all ${this.relays.length} relays: ${err.errors.map((e) => e instanceof Error ? e.message : String(e)).join(", ")}`
|
|
1974
|
+
);
|
|
1045
1975
|
}
|
|
1976
|
+
throw err;
|
|
1046
1977
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1978
|
+
}
|
|
1979
|
+
/** Publish to all relays and wait for all to settle. Throws if none accepted. */
|
|
1980
|
+
async publishAll(event) {
|
|
1981
|
+
const results = await Promise.allSettled(this.pool.publish(this.relays, event));
|
|
1982
|
+
const anyOk = results.some((r) => r.status === "fulfilled");
|
|
1983
|
+
if (!anyOk) {
|
|
1984
|
+
throw new Error(`Failed to publish to all ${this.relays.length} relays`);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
subscribe(filter, onEvent) {
|
|
1988
|
+
const rawSub = this.pool.subscribeMany(this.relays, filter, { onevent: onEvent });
|
|
1989
|
+
const tracked = {
|
|
1990
|
+
close: (reason) => {
|
|
1991
|
+
this.activeSubscriptions.delete(tracked);
|
|
1992
|
+
rawSub.close(reason);
|
|
1052
1993
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1994
|
+
};
|
|
1995
|
+
this.activeSubscriptions.add(tracked);
|
|
1996
|
+
return tracked;
|
|
1997
|
+
}
|
|
1998
|
+
/**
|
|
1999
|
+
* Subscribe and wait until at least one relay confirms the subscription
|
|
2000
|
+
* is active (EOSE). Resolves on the first relay that responds.
|
|
2001
|
+
* Essential for ephemeral events where the subscription must be live
|
|
2002
|
+
* before publishing.
|
|
2003
|
+
*
|
|
2004
|
+
* Note: resolves on timeout even if no relay sent EOSE. The caller
|
|
2005
|
+
* cannot distinguish timeout from success - this is intentional for
|
|
2006
|
+
* best-effort ephemeral event delivery.
|
|
2007
|
+
*/
|
|
2008
|
+
subscribeAndWait(filter, onEvent, timeoutMs = DEFAULTS.EOSE_TIMEOUT_MS) {
|
|
2009
|
+
return new Promise((resolve) => {
|
|
2010
|
+
let resolved = false;
|
|
2011
|
+
let timer;
|
|
2012
|
+
const subs = [];
|
|
2013
|
+
const combinedSub = {
|
|
2014
|
+
close: (reason) => {
|
|
2015
|
+
this.activeSubscriptions.delete(combinedSub);
|
|
2016
|
+
for (const s of subs) {
|
|
2017
|
+
s.close(reason);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
this.activeSubscriptions.add(combinedSub);
|
|
2022
|
+
const done = () => {
|
|
2023
|
+
if (resolved) {
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
resolved = true;
|
|
2027
|
+
if (timer) {
|
|
2028
|
+
clearTimeout(timer);
|
|
2029
|
+
}
|
|
2030
|
+
resolve(combinedSub);
|
|
2031
|
+
};
|
|
2032
|
+
const seen = new BoundedSet(1e4);
|
|
2033
|
+
const dedupedOnEvent = (ev) => {
|
|
2034
|
+
if (seen.has(ev.id)) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
seen.add(ev.id);
|
|
2038
|
+
onEvent(ev);
|
|
2039
|
+
};
|
|
2040
|
+
for (const relay of this.relays) {
|
|
2041
|
+
try {
|
|
2042
|
+
const sub = this.pool.subscribeMany([relay], filter, {
|
|
2043
|
+
onevent: dedupedOnEvent,
|
|
2044
|
+
oneose: done
|
|
2045
|
+
});
|
|
2046
|
+
subs.push(sub);
|
|
2047
|
+
} catch {
|
|
2048
|
+
}
|
|
1055
2049
|
}
|
|
1056
|
-
|
|
2050
|
+
if (subs.length === 0) {
|
|
2051
|
+
done();
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
if (!resolved) {
|
|
2055
|
+
timer = setTimeout(done, timeoutMs);
|
|
2056
|
+
}
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* Tear down pool and create a fresh one.
|
|
2061
|
+
* Works around nostr-tools `onerror - skipReconnection = true` bug
|
|
2062
|
+
* that permanently kills subscriptions. Callers must re-subscribe.
|
|
2063
|
+
*/
|
|
2064
|
+
reset() {
|
|
2065
|
+
for (const sub of this.activeSubscriptions) {
|
|
2066
|
+
sub.close("pool reset");
|
|
1057
2067
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
2068
|
+
this.activeSubscriptions.clear();
|
|
2069
|
+
try {
|
|
2070
|
+
this.pool.close(this.relays);
|
|
2071
|
+
} catch {
|
|
1060
2072
|
}
|
|
1061
|
-
|
|
2073
|
+
this.pool = new nostrTools.SimplePool();
|
|
1062
2074
|
}
|
|
1063
2075
|
/**
|
|
1064
|
-
*
|
|
1065
|
-
* The caller must sign and send via wallet adapter.
|
|
2076
|
+
* Lightweight connectivity probe. Returns true if at least one relay responds.
|
|
1066
2077
|
*/
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
const feeAmount = paymentRequest.fee_amount ?? 0;
|
|
1072
|
-
const providerAmount = feeAddress && feeAmount > 0 ? new Decimal__default.default(paymentRequest.amount).minus(feeAmount).toNumber() : paymentRequest.amount;
|
|
1073
|
-
const transferIx = web3_js.SystemProgram.transfer({
|
|
1074
|
-
fromPubkey: payerPubkey,
|
|
1075
|
-
toPubkey: recipient,
|
|
1076
|
-
lamports: providerAmount
|
|
1077
|
-
});
|
|
1078
|
-
transferIx.keys.push({
|
|
1079
|
-
pubkey: reference,
|
|
1080
|
-
isSigner: false,
|
|
1081
|
-
isWritable: false
|
|
2078
|
+
async probe(timeoutMs = DEFAULTS.EOSE_TIMEOUT_MS) {
|
|
2079
|
+
let timer;
|
|
2080
|
+
const query = this.pool.querySync(this.relays, { kinds: [0], limit: 1 });
|
|
2081
|
+
query.catch(() => {
|
|
1082
2082
|
});
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
toPubkey: feeAddress,
|
|
1089
|
-
lamports: feeAmount
|
|
2083
|
+
try {
|
|
2084
|
+
await Promise.race([
|
|
2085
|
+
query,
|
|
2086
|
+
new Promise((_, reject) => {
|
|
2087
|
+
timer = setTimeout(() => reject(new Error("probe timeout")), timeoutMs);
|
|
1090
2088
|
})
|
|
1091
|
-
);
|
|
2089
|
+
]);
|
|
2090
|
+
return true;
|
|
2091
|
+
} catch {
|
|
2092
|
+
return false;
|
|
2093
|
+
} finally {
|
|
2094
|
+
clearTimeout(timer);
|
|
1092
2095
|
}
|
|
1093
|
-
return tx;
|
|
1094
2096
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
2097
|
+
getRelays() {
|
|
2098
|
+
return this.relays;
|
|
2099
|
+
}
|
|
2100
|
+
close() {
|
|
2101
|
+
for (const sub of this.activeSubscriptions) {
|
|
2102
|
+
sub.close("pool closed");
|
|
2103
|
+
}
|
|
2104
|
+
this.activeSubscriptions.clear();
|
|
2105
|
+
try {
|
|
2106
|
+
this.pool.close(this.relays);
|
|
2107
|
+
} catch {
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
|
|
2112
|
+
// src/client.ts
|
|
2113
|
+
var ElisymClient = class {
|
|
2114
|
+
pool;
|
|
2115
|
+
discovery;
|
|
2116
|
+
marketplace;
|
|
2117
|
+
messaging;
|
|
2118
|
+
media;
|
|
2119
|
+
payment;
|
|
2120
|
+
constructor(config = {}) {
|
|
2121
|
+
this.pool = new NostrPool(config.relays ?? RELAYS);
|
|
2122
|
+
this.discovery = new DiscoveryService(this.pool);
|
|
2123
|
+
this.marketplace = new MarketplaceService(this.pool);
|
|
2124
|
+
this.messaging = new MessagingService(this.pool);
|
|
2125
|
+
this.media = new MediaService(config.uploadUrl);
|
|
2126
|
+
this.payment = config.payment ?? new SolanaPaymentStrategy();
|
|
2127
|
+
}
|
|
2128
|
+
close() {
|
|
2129
|
+
this.pool.close();
|
|
1111
2130
|
}
|
|
1112
2131
|
};
|
|
1113
2132
|
function formatSol(lamports) {
|
|
1114
|
-
const sol = new
|
|
1115
|
-
if (sol.gte(1e6))
|
|
1116
|
-
|
|
2133
|
+
const sol = new Decimal2__default.default(lamports).div(LAMPORTS_PER_SOL);
|
|
2134
|
+
if (sol.gte(1e6)) {
|
|
2135
|
+
return `${sol.idiv(1e6)}m SOL`;
|
|
2136
|
+
}
|
|
2137
|
+
if (sol.gte(1e4)) {
|
|
2138
|
+
return `${sol.idiv(1e3)}k SOL`;
|
|
2139
|
+
}
|
|
1117
2140
|
return `${compactSol(sol)} SOL`;
|
|
1118
2141
|
}
|
|
1119
2142
|
function compactSol(sol) {
|
|
1120
|
-
if (sol.isZero())
|
|
1121
|
-
|
|
2143
|
+
if (sol.isZero()) {
|
|
2144
|
+
return "0";
|
|
2145
|
+
}
|
|
2146
|
+
if (sol.gte(1e3)) {
|
|
2147
|
+
return sol.toDecimalPlaces(0, Decimal2__default.default.ROUND_FLOOR).toString();
|
|
2148
|
+
}
|
|
1122
2149
|
const maxFrac = 9;
|
|
1123
2150
|
for (let d = 1; d <= maxFrac; d++) {
|
|
1124
2151
|
const s = sol.toFixed(d);
|
|
1125
|
-
if (new
|
|
2152
|
+
if (new Decimal2__default.default(s).eq(sol)) {
|
|
1126
2153
|
return s.replace(/0+$/, "").replace(/\.$/, "");
|
|
1127
2154
|
}
|
|
1128
2155
|
}
|
|
@@ -1130,43 +2157,39 @@ function compactSol(sol) {
|
|
|
1130
2157
|
}
|
|
1131
2158
|
function timeAgo(unix) {
|
|
1132
2159
|
const seconds = Math.max(0, Math.floor(Date.now() / 1e3 - unix));
|
|
1133
|
-
if (seconds < 60)
|
|
2160
|
+
if (seconds < 60) {
|
|
2161
|
+
return `${seconds}s ago`;
|
|
2162
|
+
}
|
|
1134
2163
|
const minutes = Math.floor(seconds / 60);
|
|
1135
|
-
if (minutes < 60)
|
|
2164
|
+
if (minutes < 60) {
|
|
2165
|
+
return `${minutes}m ago`;
|
|
2166
|
+
}
|
|
1136
2167
|
const hours = Math.floor(minutes / 60);
|
|
1137
|
-
if (hours < 24)
|
|
2168
|
+
if (hours < 24) {
|
|
2169
|
+
return `${hours}h ago`;
|
|
2170
|
+
}
|
|
1138
2171
|
const days = Math.floor(hours / 24);
|
|
1139
2172
|
return `${days}d ago`;
|
|
1140
2173
|
}
|
|
1141
2174
|
function truncateKey(hex, chars = 6) {
|
|
1142
|
-
if (hex.length <= chars * 2)
|
|
2175
|
+
if (hex.length <= chars * 2) {
|
|
2176
|
+
return hex;
|
|
2177
|
+
}
|
|
1143
2178
|
return `${hex.slice(0, chars)}...${hex.slice(-chars)}`;
|
|
1144
2179
|
}
|
|
1145
|
-
function makeNjumpUrl(eventId, relays = RELAYS) {
|
|
1146
|
-
const nevent = nostrTools.nip19.neventEncode({
|
|
1147
|
-
id: eventId,
|
|
1148
|
-
relays: relays.slice(0, 2)
|
|
1149
|
-
});
|
|
1150
|
-
return `https://njump.me/${nevent}`;
|
|
1151
|
-
}
|
|
1152
2180
|
|
|
1153
|
-
// src/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
marketplace;
|
|
1158
|
-
messaging;
|
|
1159
|
-
constructor(config = {}) {
|
|
1160
|
-
this.pool = new NostrPool(config.relays ?? RELAYS);
|
|
1161
|
-
this.discovery = new DiscoveryService(this.pool);
|
|
1162
|
-
this.marketplace = new MarketplaceService(this.pool);
|
|
1163
|
-
this.messaging = new MessagingService(this.pool);
|
|
1164
|
-
}
|
|
1165
|
-
close() {
|
|
1166
|
-
this.pool.close();
|
|
2181
|
+
// src/primitives/config.ts
|
|
2182
|
+
function validateAgentName(name) {
|
|
2183
|
+
if (!name || name.length > LIMITS.MAX_AGENT_NAME_LENGTH || !/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
2184
|
+
throw new Error("Agent name must be 1-64 characters, alphanumeric, underscore, or hyphen.");
|
|
1167
2185
|
}
|
|
1168
|
-
}
|
|
2186
|
+
}
|
|
2187
|
+
function serializeConfig(config) {
|
|
2188
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
2189
|
+
}
|
|
1169
2190
|
|
|
2191
|
+
exports.BoundedSet = BoundedSet;
|
|
2192
|
+
exports.DEFAULTS = DEFAULTS;
|
|
1170
2193
|
exports.DEFAULT_KIND_OFFSET = DEFAULT_KIND_OFFSET;
|
|
1171
2194
|
exports.DiscoveryService = DiscoveryService;
|
|
1172
2195
|
exports.ElisymClient = ElisymClient;
|
|
@@ -1181,19 +2204,28 @@ exports.KIND_JOB_RESULT_BASE = KIND_JOB_RESULT_BASE;
|
|
|
1181
2204
|
exports.KIND_PING = KIND_PING;
|
|
1182
2205
|
exports.KIND_PONG = KIND_PONG;
|
|
1183
2206
|
exports.LAMPORTS_PER_SOL = LAMPORTS_PER_SOL;
|
|
2207
|
+
exports.LIMITS = LIMITS;
|
|
1184
2208
|
exports.MarketplaceService = MarketplaceService;
|
|
2209
|
+
exports.MediaService = MediaService;
|
|
1185
2210
|
exports.MessagingService = MessagingService;
|
|
1186
2211
|
exports.NostrPool = NostrPool;
|
|
1187
2212
|
exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
|
|
1188
2213
|
exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
|
|
1189
|
-
exports.PaymentService = PaymentService;
|
|
1190
2214
|
exports.RELAYS = RELAYS;
|
|
2215
|
+
exports.SolanaPaymentStrategy = SolanaPaymentStrategy;
|
|
2216
|
+
exports.assertExpiry = assertExpiry;
|
|
2217
|
+
exports.assertLamports = assertLamports;
|
|
2218
|
+
exports.calculateProtocolFee = calculateProtocolFee;
|
|
1191
2219
|
exports.formatSol = formatSol;
|
|
1192
2220
|
exports.jobRequestKind = jobRequestKind;
|
|
1193
2221
|
exports.jobResultKind = jobResultKind;
|
|
1194
|
-
exports.
|
|
2222
|
+
exports.nip44Decrypt = nip44Decrypt;
|
|
2223
|
+
exports.nip44Encrypt = nip44Encrypt;
|
|
2224
|
+
exports.serializeConfig = serializeConfig;
|
|
1195
2225
|
exports.timeAgo = timeAgo;
|
|
1196
2226
|
exports.toDTag = toDTag;
|
|
1197
2227
|
exports.truncateKey = truncateKey;
|
|
2228
|
+
exports.validateAgentName = validateAgentName;
|
|
2229
|
+
exports.validateExpiry = validateExpiry;
|
|
1198
2230
|
//# sourceMappingURL=index.cjs.map
|
|
1199
2231
|
//# sourceMappingURL=index.cjs.map
|