@toon-protocol/core 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -6
- package/dist/index.d.ts +1343 -166
- package/dist/index.js +1521 -196
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -22,6 +22,260 @@ var TEXT_GENERATION_KIND = 5100;
|
|
|
22
22
|
var IMAGE_GENERATION_KIND = 5200;
|
|
23
23
|
var TEXT_TO_SPEECH_KIND = 5300;
|
|
24
24
|
var TRANSLATION_KIND = 5302;
|
|
25
|
+
var WORKFLOW_CHAIN_KIND = 10040;
|
|
26
|
+
var JOB_REVIEW_KIND = 31117;
|
|
27
|
+
var WEB_OF_TRUST_KIND = 30382;
|
|
28
|
+
var PREFIX_CLAIM_KIND = 10034;
|
|
29
|
+
var PREFIX_GRANT_KIND = 10037;
|
|
30
|
+
var BLOB_STORAGE_REQUEST_KIND = 5094;
|
|
31
|
+
var BLOB_STORAGE_RESULT_KIND = 6094;
|
|
32
|
+
var PET_INTERACTION_REQUEST_KIND = 5900;
|
|
33
|
+
var PET_INTERACTION_RESULT_KIND = 6900;
|
|
34
|
+
var PET_INTERACTION_EVENT_KIND = 14919;
|
|
35
|
+
var ILP_ROOT_PREFIX = "g.toon";
|
|
36
|
+
|
|
37
|
+
// src/address/ilp-address-validation.ts
|
|
38
|
+
var ILP_SEGMENT_PATTERN = /^[a-z0-9-]+$/;
|
|
39
|
+
var MAX_ILP_ADDRESS_LENGTH = 1023;
|
|
40
|
+
function isValidIlpAddressStructure(address) {
|
|
41
|
+
if (!address) return false;
|
|
42
|
+
if (address.length > MAX_ILP_ADDRESS_LENGTH) return false;
|
|
43
|
+
const segments = address.split(".");
|
|
44
|
+
for (const segment of segments) {
|
|
45
|
+
if (segment.length === 0) return false;
|
|
46
|
+
if (!ILP_SEGMENT_PATTERN.test(segment)) return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function validateIlpAddress(address) {
|
|
51
|
+
if (address.length > MAX_ILP_ADDRESS_LENGTH) {
|
|
52
|
+
throw new ToonError(
|
|
53
|
+
`Invalid ILP address: exceeds maximum length of ${MAX_ILP_ADDRESS_LENGTH}`,
|
|
54
|
+
"ADDRESS_INVALID_PREFIX"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const segments = address.split(".");
|
|
58
|
+
for (const segment of segments) {
|
|
59
|
+
if (segment.length === 0) {
|
|
60
|
+
throw new ToonError(
|
|
61
|
+
`Invalid ILP address: empty segment in "${address}"`,
|
|
62
|
+
"ADDRESS_INVALID_PREFIX"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!ILP_SEGMENT_PATTERN.test(segment)) {
|
|
66
|
+
throw new ToonError(
|
|
67
|
+
`Invalid ILP address: segment "${segment}" contains invalid characters`,
|
|
68
|
+
"ADDRESS_INVALID_PREFIX"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/address/derive-child-address.ts
|
|
75
|
+
var HEX_PATTERN = /^[0-9a-fA-F]+$/;
|
|
76
|
+
var PUBKEY_TRUNCATION_LENGTH = 8;
|
|
77
|
+
var MAX_PUBKEY_LENGTH = 128;
|
|
78
|
+
var MAX_ILP_ADDRESS_LENGTH2 = 1023;
|
|
79
|
+
function deriveChildAddress(parentPrefix, childPubkey) {
|
|
80
|
+
if (!parentPrefix) {
|
|
81
|
+
throw new ToonError(
|
|
82
|
+
"Parent prefix must not be empty",
|
|
83
|
+
"ADDRESS_INVALID_PREFIX"
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
validateIlpAddress(parentPrefix);
|
|
87
|
+
if (childPubkey.length < PUBKEY_TRUNCATION_LENGTH) {
|
|
88
|
+
throw new ToonError(
|
|
89
|
+
`Invalid pubkey: must be at least ${PUBKEY_TRUNCATION_LENGTH} hex characters, got ${childPubkey.length}`,
|
|
90
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (childPubkey.length > MAX_PUBKEY_LENGTH) {
|
|
94
|
+
throw new ToonError(
|
|
95
|
+
`Invalid pubkey: exceeds maximum length of ${MAX_PUBKEY_LENGTH} characters`,
|
|
96
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (!HEX_PATTERN.test(childPubkey)) {
|
|
100
|
+
throw new ToonError(
|
|
101
|
+
`Invalid pubkey: contains non-hex characters`,
|
|
102
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
const childSegment = childPubkey.slice(0, PUBKEY_TRUNCATION_LENGTH).toLowerCase();
|
|
106
|
+
const derivedAddress = `${parentPrefix}.${childSegment}`;
|
|
107
|
+
if (derivedAddress.length > MAX_ILP_ADDRESS_LENGTH2) {
|
|
108
|
+
throw new ToonError(
|
|
109
|
+
`Derived ILP address exceeds maximum length of ${MAX_ILP_ADDRESS_LENGTH2}`,
|
|
110
|
+
"ADDRESS_TOO_LONG"
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return derivedAddress;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/address/btp-prefix-exchange.ts
|
|
117
|
+
var MAX_PREFIX_LENGTH = 1023;
|
|
118
|
+
function extractPrefixFromHandshake(handshakeData) {
|
|
119
|
+
const prefix = handshakeData["prefix"];
|
|
120
|
+
if (prefix === void 0 || prefix === null || prefix === "") {
|
|
121
|
+
throw new ToonError(
|
|
122
|
+
"BTP handshake response missing required prefix field",
|
|
123
|
+
"ADDRESS_MISSING_PREFIX"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (typeof prefix !== "string") {
|
|
127
|
+
throw new ToonError(
|
|
128
|
+
"BTP handshake response missing required prefix field",
|
|
129
|
+
"ADDRESS_MISSING_PREFIX"
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (prefix.length > MAX_PREFIX_LENGTH) {
|
|
133
|
+
throw new ToonError(
|
|
134
|
+
`BTP handshake prefix exceeds maximum length of ${MAX_PREFIX_LENGTH}`,
|
|
135
|
+
"ADDRESS_INVALID_PREFIX"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!isValidIlpAddressStructure(prefix)) {
|
|
139
|
+
throw new ToonError(
|
|
140
|
+
`BTP handshake prefix is not a valid ILP address: "${prefix}"`,
|
|
141
|
+
"ADDRESS_INVALID_PREFIX"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return prefix;
|
|
145
|
+
}
|
|
146
|
+
function buildPrefixHandshakeData(ownIlpAddress) {
|
|
147
|
+
if (ownIlpAddress.length > MAX_PREFIX_LENGTH) {
|
|
148
|
+
throw new ToonError(
|
|
149
|
+
`Cannot build handshake data: address exceeds maximum length of ${MAX_PREFIX_LENGTH}`,
|
|
150
|
+
"ADDRESS_INVALID_PREFIX"
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!isValidIlpAddressStructure(ownIlpAddress)) {
|
|
154
|
+
throw new ToonError(
|
|
155
|
+
`Cannot build handshake data: "${ownIlpAddress}" is not a valid ILP address`,
|
|
156
|
+
"ADDRESS_INVALID_PREFIX"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return { prefix: ownIlpAddress };
|
|
160
|
+
}
|
|
161
|
+
function validatePrefixConsistency(handshakePrefix, advertisedPrefix) {
|
|
162
|
+
if (advertisedPrefix === void 0) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (handshakePrefix !== advertisedPrefix) {
|
|
166
|
+
throw new ToonError(
|
|
167
|
+
`BTP handshake prefix "${handshakePrefix}" does not match upstream kind:10032 advertised address "${advertisedPrefix}"`,
|
|
168
|
+
"ADDRESS_PREFIX_MISMATCH"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function checkAddressCollision(derivedAddress, knownPeerAddresses) {
|
|
173
|
+
if (knownPeerAddresses.includes(derivedAddress)) {
|
|
174
|
+
throw new ToonError(
|
|
175
|
+
`Derived ILP address "${derivedAddress}" collides with an existing peer address`,
|
|
176
|
+
"ADDRESS_COLLISION"
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/address/address-assignment.ts
|
|
182
|
+
function assignAddressFromHandshake(handshakeData, ownPubkey) {
|
|
183
|
+
const prefix = extractPrefixFromHandshake(handshakeData);
|
|
184
|
+
return deriveChildAddress(prefix, ownPubkey);
|
|
185
|
+
}
|
|
186
|
+
function isGenesisNode(config) {
|
|
187
|
+
return config.ilpAddress === ILP_ROOT_PREFIX;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/address/address-registry.ts
|
|
191
|
+
var AddressRegistry = class {
|
|
192
|
+
prefixToAddress = /* @__PURE__ */ new Map();
|
|
193
|
+
/**
|
|
194
|
+
* Registers a new upstream prefix -> derived address mapping.
|
|
195
|
+
*
|
|
196
|
+
* @param upstreamPrefix - The upstream peer's ILP address prefix
|
|
197
|
+
* @param derivedAddress - The derived ILP address for this node under that prefix
|
|
198
|
+
*/
|
|
199
|
+
addAddress(upstreamPrefix, derivedAddress) {
|
|
200
|
+
this.prefixToAddress.set(upstreamPrefix, derivedAddress);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Removes the mapping for the given upstream prefix.
|
|
204
|
+
*
|
|
205
|
+
* @param upstreamPrefix - The upstream prefix to remove
|
|
206
|
+
* @returns The removed derived address, or `undefined` if the prefix was not found
|
|
207
|
+
*/
|
|
208
|
+
removeAddress(upstreamPrefix) {
|
|
209
|
+
const address = this.prefixToAddress.get(upstreamPrefix);
|
|
210
|
+
if (address !== void 0) {
|
|
211
|
+
this.prefixToAddress.delete(upstreamPrefix);
|
|
212
|
+
}
|
|
213
|
+
return address;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Returns all derived addresses in insertion order.
|
|
217
|
+
*/
|
|
218
|
+
getAddresses() {
|
|
219
|
+
return [...this.prefixToAddress.values()];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Returns `true` if the given upstream prefix is registered.
|
|
223
|
+
*/
|
|
224
|
+
hasPrefix(upstreamPrefix) {
|
|
225
|
+
return this.prefixToAddress.has(upstreamPrefix);
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Returns the number of registered addresses.
|
|
229
|
+
*/
|
|
230
|
+
get size() {
|
|
231
|
+
return this.prefixToAddress.size;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Returns the primary (first inserted) address.
|
|
235
|
+
*
|
|
236
|
+
* @returns The first address, or `undefined` if the registry is empty
|
|
237
|
+
*/
|
|
238
|
+
getPrimaryAddress() {
|
|
239
|
+
const first = this.prefixToAddress.values().next();
|
|
240
|
+
return first.done ? void 0 : first.value;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/address/prefix-validation.ts
|
|
245
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set(["toon", "ilp", "local", "peer", "test"]);
|
|
246
|
+
var PREFIX_PATTERN = /^[a-z0-9]+$/;
|
|
247
|
+
var MIN_LENGTH = 2;
|
|
248
|
+
var MAX_LENGTH = 16;
|
|
249
|
+
function validatePrefix(prefix) {
|
|
250
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
251
|
+
return { valid: false, reason: "Prefix must be a non-empty string" };
|
|
252
|
+
}
|
|
253
|
+
if (prefix.length < MIN_LENGTH) {
|
|
254
|
+
return {
|
|
255
|
+
valid: false,
|
|
256
|
+
reason: `Prefix must have a minimum of ${MIN_LENGTH} characters, got ${prefix.length}`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (prefix.length > MAX_LENGTH) {
|
|
260
|
+
return {
|
|
261
|
+
valid: false,
|
|
262
|
+
reason: `Prefix must have a maximum of ${MAX_LENGTH} characters, got ${prefix.length}`
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
if (!PREFIX_PATTERN.test(prefix)) {
|
|
266
|
+
return {
|
|
267
|
+
valid: false,
|
|
268
|
+
reason: "Prefix must contain only lowercase alphanumeric characters [a-z0-9]"
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (RESERVED_WORDS.has(prefix)) {
|
|
272
|
+
return {
|
|
273
|
+
valid: false,
|
|
274
|
+
reason: `Prefix "${prefix}" is a reserved word`
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return { valid: true };
|
|
278
|
+
}
|
|
25
279
|
|
|
26
280
|
// src/events/parsers.ts
|
|
27
281
|
function validateChainId(chainId) {
|
|
@@ -57,7 +311,8 @@ function parseIlpPeerInfo(event) {
|
|
|
57
311
|
blsHttpEndpoint,
|
|
58
312
|
settlementEngine,
|
|
59
313
|
assetCode,
|
|
60
|
-
assetScale
|
|
314
|
+
assetScale,
|
|
315
|
+
ilpAddresses: rawIlpAddresses
|
|
61
316
|
} = parsed;
|
|
62
317
|
if (typeof ilpAddress !== "string" || ilpAddress.length === 0) {
|
|
63
318
|
throw new InvalidEventError(
|
|
@@ -142,6 +397,52 @@ function parseIlpPeerInfo(event) {
|
|
|
142
397
|
throw new InvalidEventError("tokenNetworks must be an object");
|
|
143
398
|
}
|
|
144
399
|
}
|
|
400
|
+
const { feePerByte: rawFeePerByte } = parsed;
|
|
401
|
+
let feePerByte;
|
|
402
|
+
if (rawFeePerByte === void 0) {
|
|
403
|
+
feePerByte = "0";
|
|
404
|
+
} else if (typeof rawFeePerByte !== "string" || !/^\d+$/.test(rawFeePerByte)) {
|
|
405
|
+
throw new InvalidEventError(
|
|
406
|
+
`Invalid feePerByte: "${String(rawFeePerByte)}" must be a non-negative integer string`
|
|
407
|
+
);
|
|
408
|
+
} else {
|
|
409
|
+
feePerByte = rawFeePerByte;
|
|
410
|
+
}
|
|
411
|
+
const { prefixPricing: rawPrefixPricing } = parsed;
|
|
412
|
+
let prefixPricing;
|
|
413
|
+
if (rawPrefixPricing !== void 0) {
|
|
414
|
+
if (!isObject(rawPrefixPricing)) {
|
|
415
|
+
throw new InvalidEventError("prefixPricing must be an object");
|
|
416
|
+
}
|
|
417
|
+
const { basePrice } = rawPrefixPricing;
|
|
418
|
+
if (typeof basePrice !== "string" || !/^\d+$/.test(basePrice)) {
|
|
419
|
+
throw new InvalidEventError(
|
|
420
|
+
`Invalid prefixPricing.basePrice: "${String(basePrice)}" must be a non-negative integer string`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
prefixPricing = { basePrice };
|
|
424
|
+
}
|
|
425
|
+
let ilpAddresses;
|
|
426
|
+
if (rawIlpAddresses !== void 0) {
|
|
427
|
+
if (!Array.isArray(rawIlpAddresses)) {
|
|
428
|
+
throw new InvalidEventError("ilpAddresses must be an array");
|
|
429
|
+
}
|
|
430
|
+
for (const addr of rawIlpAddresses) {
|
|
431
|
+
if (typeof addr !== "string" || addr.length === 0) {
|
|
432
|
+
throw new InvalidEventError(
|
|
433
|
+
"ilpAddresses elements must be non-empty strings"
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (!isValidIlpAddressStructure(addr)) {
|
|
437
|
+
throw new InvalidEventError(
|
|
438
|
+
`Invalid ILP address in ilpAddresses: "${addr}"`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
ilpAddresses = rawIlpAddresses;
|
|
443
|
+
} else {
|
|
444
|
+
ilpAddresses = [ilpAddress];
|
|
445
|
+
}
|
|
145
446
|
return {
|
|
146
447
|
ilpAddress,
|
|
147
448
|
btpEndpoint,
|
|
@@ -156,17 +457,51 @@ function parseIlpPeerInfo(event) {
|
|
|
156
457
|
},
|
|
157
458
|
...tokenNetworks !== void 0 && {
|
|
158
459
|
tokenNetworks
|
|
159
|
-
}
|
|
460
|
+
},
|
|
461
|
+
ilpAddresses,
|
|
462
|
+
feePerByte,
|
|
463
|
+
...prefixPricing !== void 0 && { prefixPricing }
|
|
160
464
|
};
|
|
161
465
|
}
|
|
162
466
|
|
|
163
467
|
// src/events/builders.ts
|
|
164
468
|
import { finalizeEvent } from "nostr-tools/pure";
|
|
165
469
|
function buildIlpPeerInfoEvent(info, secretKey) {
|
|
470
|
+
if (info.feePerByte !== void 0) {
|
|
471
|
+
if (typeof info.feePerByte !== "string" || !/^\d+$/.test(info.feePerByte)) {
|
|
472
|
+
throw new ToonError(
|
|
473
|
+
`Invalid feePerByte: "${String(info.feePerByte)}" must be a non-negative integer string`,
|
|
474
|
+
"INVALID_FEE"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
let effectiveInfo = info;
|
|
479
|
+
if (info.ilpAddresses !== void 0) {
|
|
480
|
+
const addresses = info.ilpAddresses;
|
|
481
|
+
if (addresses.length === 0) {
|
|
482
|
+
throw new ToonError(
|
|
483
|
+
"ilpAddresses must be a non-empty array: a node must have at least one address",
|
|
484
|
+
"ADDRESS_EMPTY_ADDRESSES"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
for (const addr of addresses) {
|
|
488
|
+
if (!isValidIlpAddressStructure(addr)) {
|
|
489
|
+
throw new ToonError(
|
|
490
|
+
`Invalid ILP address in ilpAddresses: "${addr}"`,
|
|
491
|
+
"ADDRESS_INVALID_PREFIX"
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const primaryAddress = addresses[0];
|
|
496
|
+
effectiveInfo = {
|
|
497
|
+
...info,
|
|
498
|
+
ilpAddress: primaryAddress
|
|
499
|
+
};
|
|
500
|
+
}
|
|
166
501
|
return finalizeEvent(
|
|
167
502
|
{
|
|
168
503
|
kind: ILP_PEER_INFO_KIND,
|
|
169
|
-
content: JSON.stringify(
|
|
504
|
+
content: JSON.stringify(effectiveInfo),
|
|
170
505
|
tags: [],
|
|
171
506
|
created_at: Math.floor(Date.now() / 1e3)
|
|
172
507
|
},
|
|
@@ -376,6 +711,32 @@ function parseServiceDiscovery(event) {
|
|
|
376
711
|
return null;
|
|
377
712
|
skillResult.attestation = attestation;
|
|
378
713
|
}
|
|
714
|
+
const reputation = skillRecord["reputation"];
|
|
715
|
+
if (reputation !== void 0) {
|
|
716
|
+
if (typeof reputation !== "object" || reputation === null || Array.isArray(reputation))
|
|
717
|
+
return null;
|
|
718
|
+
const repRecord = reputation;
|
|
719
|
+
const repScore = repRecord["score"];
|
|
720
|
+
if (typeof repScore !== "number" || !isFinite(repScore)) return null;
|
|
721
|
+
const signals = repRecord["signals"];
|
|
722
|
+
if (typeof signals !== "object" || signals === null || Array.isArray(signals))
|
|
723
|
+
return null;
|
|
724
|
+
const sigRecord = signals;
|
|
725
|
+
const trustedBy = sigRecord["trustedBy"];
|
|
726
|
+
if (typeof trustedBy !== "number" || !isFinite(trustedBy)) return null;
|
|
727
|
+
const channelVolumeUsdc = sigRecord["channelVolumeUsdc"];
|
|
728
|
+
if (typeof channelVolumeUsdc !== "number" || !isFinite(channelVolumeUsdc))
|
|
729
|
+
return null;
|
|
730
|
+
const jobsCompleted = sigRecord["jobsCompleted"];
|
|
731
|
+
if (typeof jobsCompleted !== "number" || !isFinite(jobsCompleted))
|
|
732
|
+
return null;
|
|
733
|
+
const avgRating = sigRecord["avgRating"];
|
|
734
|
+
if (typeof avgRating !== "number" || !isFinite(avgRating)) return null;
|
|
735
|
+
skillResult.reputation = {
|
|
736
|
+
score: repScore,
|
|
737
|
+
signals: { trustedBy, channelVolumeUsdc, jobsCompleted, avgRating }
|
|
738
|
+
};
|
|
739
|
+
}
|
|
379
740
|
result.skill = skillResult;
|
|
380
741
|
}
|
|
381
742
|
return result;
|
|
@@ -577,11 +938,22 @@ function buildJobResultEvent(params, secretKey) {
|
|
|
577
938
|
"DVM_MISSING_CONTENT"
|
|
578
939
|
);
|
|
579
940
|
}
|
|
941
|
+
if (params.attestationEventId !== void 0) {
|
|
942
|
+
if (!HEX_64_REGEX.test(params.attestationEventId)) {
|
|
943
|
+
throw new ToonError(
|
|
944
|
+
"Job result attestationEventId must be a 64-character lowercase hex string",
|
|
945
|
+
"DVM_INVALID_ATTESTATION_EVENT_ID"
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
580
949
|
const tags = [
|
|
581
950
|
["e", params.requestEventId],
|
|
582
951
|
["p", params.customerPubkey],
|
|
583
952
|
["amount", params.amount, "usdc"]
|
|
584
953
|
];
|
|
954
|
+
if (params.attestationEventId !== void 0) {
|
|
955
|
+
tags.push(["attestation", params.attestationEventId]);
|
|
956
|
+
}
|
|
585
957
|
return finalizeEvent5(
|
|
586
958
|
{
|
|
587
959
|
kind: params.kind,
|
|
@@ -711,42 +1083,834 @@ function parseJobResult(event) {
|
|
|
711
1083
|
const amount = amountTag[1];
|
|
712
1084
|
if (amount === void 0 || amount === "") return null;
|
|
713
1085
|
if (!/^\d+$/.test(amount)) return null;
|
|
714
|
-
|
|
1086
|
+
const attestationTag = event.tags.find(
|
|
1087
|
+
(t) => t[0] === "attestation"
|
|
1088
|
+
);
|
|
1089
|
+
const attestationEventId = attestationTag?.[1];
|
|
1090
|
+
const result = {
|
|
715
1091
|
kind: event.kind,
|
|
716
1092
|
requestEventId,
|
|
717
1093
|
customerPubkey,
|
|
718
1094
|
amount,
|
|
719
1095
|
content: event.content
|
|
720
1096
|
};
|
|
1097
|
+
if (attestationEventId !== void 0 && HEX_64_REGEX.test(attestationEventId)) {
|
|
1098
|
+
result.attestationEventId = attestationEventId;
|
|
1099
|
+
}
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
function parseJobFeedback(event) {
|
|
1103
|
+
if (event.kind !== JOB_FEEDBACK_KIND) {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
1107
|
+
if (!eTag) return null;
|
|
1108
|
+
const requestEventId = eTag[1];
|
|
1109
|
+
if (requestEventId === void 0 || !HEX_64_REGEX.test(requestEventId))
|
|
1110
|
+
return null;
|
|
1111
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
1112
|
+
if (!pTag) return null;
|
|
1113
|
+
const customerPubkey = pTag[1];
|
|
1114
|
+
if (customerPubkey === void 0 || !HEX_64_REGEX.test(customerPubkey))
|
|
1115
|
+
return null;
|
|
1116
|
+
const statusTag = event.tags.find((t) => t[0] === "status");
|
|
1117
|
+
if (!statusTag) return null;
|
|
1118
|
+
const statusValue = statusTag[1];
|
|
1119
|
+
if (statusValue === void 0) return null;
|
|
1120
|
+
if (!VALID_STATUSES.has(statusValue)) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
requestEventId,
|
|
1125
|
+
customerPubkey,
|
|
1126
|
+
status: statusValue,
|
|
1127
|
+
content: event.content
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/events/workflow.ts
|
|
1132
|
+
import { finalizeEvent as finalizeEvent6 } from "nostr-tools/pure";
|
|
1133
|
+
var HEX_64_REGEX2 = /^[0-9a-f]{64}$/;
|
|
1134
|
+
function buildWorkflowDefinitionEvent(params, secretKey) {
|
|
1135
|
+
if (!params.steps || params.steps.length === 0) {
|
|
1136
|
+
throw new ToonError(
|
|
1137
|
+
"Workflow definition must have at least one step",
|
|
1138
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
for (const step of params.steps) {
|
|
1142
|
+
if (step.kind < 5e3 || step.kind > 5999) {
|
|
1143
|
+
throw new ToonError(
|
|
1144
|
+
`Workflow step kind must be in range 5000-5999, got ${step.kind}`,
|
|
1145
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof step.description !== "string" || step.description === "") {
|
|
1149
|
+
throw new ToonError(
|
|
1150
|
+
"Workflow step description must be a non-empty string",
|
|
1151
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
if (step.targetProvider !== void 0 && !HEX_64_REGEX2.test(step.targetProvider)) {
|
|
1155
|
+
throw new ToonError(
|
|
1156
|
+
"Workflow step targetProvider must be a 64-character lowercase hex string",
|
|
1157
|
+
"DVM_WORKFLOW_INVALID_PROVIDER"
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if (typeof params.initialInput.data !== "string") {
|
|
1162
|
+
throw new ToonError(
|
|
1163
|
+
"Workflow definition initial input data must be a string",
|
|
1164
|
+
"DVM_WORKFLOW_MISSING_INPUT"
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
if (!params.initialInput.type) {
|
|
1168
|
+
throw new ToonError(
|
|
1169
|
+
"Workflow definition initial input type is required",
|
|
1170
|
+
"DVM_WORKFLOW_MISSING_INPUT"
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof params.totalBid !== "string" || params.totalBid === "") {
|
|
1174
|
+
throw new ToonError(
|
|
1175
|
+
"Workflow definition totalBid must be a non-empty string (USDC micro-units)",
|
|
1176
|
+
"DVM_WORKFLOW_INVALID_BID"
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
const stepsWithAllocation = params.steps.filter(
|
|
1180
|
+
(s) => s.bidAllocation !== void 0
|
|
1181
|
+
);
|
|
1182
|
+
if (stepsWithAllocation.length > 0) {
|
|
1183
|
+
let allocationSum;
|
|
1184
|
+
let totalBidBigInt;
|
|
1185
|
+
try {
|
|
1186
|
+
allocationSum = 0n;
|
|
1187
|
+
for (const step of stepsWithAllocation) {
|
|
1188
|
+
allocationSum += BigInt(step.bidAllocation);
|
|
1189
|
+
}
|
|
1190
|
+
totalBidBigInt = BigInt(params.totalBid);
|
|
1191
|
+
} catch {
|
|
1192
|
+
throw new ToonError(
|
|
1193
|
+
"Workflow bid amounts must be valid numeric strings (USDC micro-units)",
|
|
1194
|
+
"DVM_WORKFLOW_INVALID_BID"
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
if (allocationSum > totalBidBigInt) {
|
|
1198
|
+
throw new ToonError(
|
|
1199
|
+
`Sum of step bid allocations (${allocationSum}) exceeds total bid (${totalBidBigInt})`,
|
|
1200
|
+
"DVM_WORKFLOW_BID_OVERFLOW"
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
const contentBody = {
|
|
1205
|
+
steps: params.steps.map((step) => {
|
|
1206
|
+
const s = {
|
|
1207
|
+
kind: step.kind,
|
|
1208
|
+
description: step.description
|
|
1209
|
+
};
|
|
1210
|
+
if (step.targetProvider !== void 0) {
|
|
1211
|
+
s["targetProvider"] = step.targetProvider;
|
|
1212
|
+
}
|
|
1213
|
+
if (step.bidAllocation !== void 0) {
|
|
1214
|
+
s["bidAllocation"] = step.bidAllocation;
|
|
1215
|
+
}
|
|
1216
|
+
return s;
|
|
1217
|
+
}),
|
|
1218
|
+
initialInput: params.initialInput,
|
|
1219
|
+
totalBid: params.totalBid
|
|
1220
|
+
};
|
|
1221
|
+
const contentJson = JSON.stringify(contentBody);
|
|
1222
|
+
const tags = [];
|
|
1223
|
+
const dTag = params.workflowId ?? `wf-${Date.now()}-${params.steps.length}`;
|
|
1224
|
+
tags.push(["d", dTag]);
|
|
1225
|
+
tags.push(["bid", params.totalBid, "usdc"]);
|
|
1226
|
+
return finalizeEvent6(
|
|
1227
|
+
{
|
|
1228
|
+
kind: WORKFLOW_CHAIN_KIND,
|
|
1229
|
+
content: contentJson,
|
|
1230
|
+
tags,
|
|
1231
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1232
|
+
},
|
|
1233
|
+
secretKey
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
function parseWorkflowDefinition(event) {
|
|
1237
|
+
if (event.kind !== WORKFLOW_CHAIN_KIND) {
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
let body;
|
|
1241
|
+
try {
|
|
1242
|
+
body = JSON.parse(event.content);
|
|
1243
|
+
} catch {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
if (typeof body !== "object" || body === null) {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
const obj = body;
|
|
1250
|
+
const rawSteps = obj["steps"];
|
|
1251
|
+
if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1254
|
+
const steps = [];
|
|
1255
|
+
for (const rawStep of rawSteps) {
|
|
1256
|
+
if (typeof rawStep !== "object" || rawStep === null) {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
const stepObj = rawStep;
|
|
1260
|
+
const kind = stepObj["kind"];
|
|
1261
|
+
const description = stepObj["description"];
|
|
1262
|
+
if (typeof kind !== "number" || typeof description !== "string") {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
if (kind < 5e3 || kind > 5999) {
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
const step = { kind, description };
|
|
1269
|
+
const targetProvider = stepObj["targetProvider"];
|
|
1270
|
+
if (typeof targetProvider === "string" && targetProvider.length > 0) {
|
|
1271
|
+
step.targetProvider = targetProvider;
|
|
1272
|
+
}
|
|
1273
|
+
const bidAllocation = stepObj["bidAllocation"];
|
|
1274
|
+
if (typeof bidAllocation === "string" && bidAllocation.length > 0) {
|
|
1275
|
+
step.bidAllocation = bidAllocation;
|
|
1276
|
+
}
|
|
1277
|
+
steps.push(step);
|
|
1278
|
+
}
|
|
1279
|
+
const rawInput = obj["initialInput"];
|
|
1280
|
+
if (typeof rawInput !== "object" || rawInput === null) {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
const inputObj = rawInput;
|
|
1284
|
+
const inputData = inputObj["data"];
|
|
1285
|
+
const inputType = inputObj["type"];
|
|
1286
|
+
if (typeof inputData !== "string" || typeof inputType !== "string") {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
const totalBid = obj["totalBid"];
|
|
1290
|
+
if (typeof totalBid !== "string" || totalBid === "") {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
steps,
|
|
1295
|
+
initialInput: { data: inputData, type: inputType },
|
|
1296
|
+
totalBid,
|
|
1297
|
+
content: event.content
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/events/swarm.ts
|
|
1302
|
+
import { finalizeEvent as finalizeEvent7 } from "nostr-tools/pure";
|
|
1303
|
+
var HEX_64_REGEX3 = /^[0-9a-f]{64}$/;
|
|
1304
|
+
function buildSwarmRequestEvent(params, secretKey) {
|
|
1305
|
+
if (!Number.isInteger(params.maxProviders) || params.maxProviders < 1) {
|
|
1306
|
+
throw new ToonError(
|
|
1307
|
+
`Swarm maxProviders must be a positive integer >= 1, got ${params.maxProviders}`,
|
|
1308
|
+
"DVM_SWARM_INVALID_MAX_PROVIDERS"
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
const baseEvent = buildJobRequestEvent(params, secretKey);
|
|
1312
|
+
const judge = params.judge ?? "customer";
|
|
1313
|
+
const tags = [
|
|
1314
|
+
...baseEvent.tags,
|
|
1315
|
+
["swarm", params.maxProviders.toString()],
|
|
1316
|
+
["judge", judge]
|
|
1317
|
+
];
|
|
1318
|
+
return finalizeEvent7(
|
|
1319
|
+
{
|
|
1320
|
+
kind: baseEvent.kind,
|
|
1321
|
+
content: baseEvent.content,
|
|
1322
|
+
tags,
|
|
1323
|
+
created_at: baseEvent.created_at
|
|
1324
|
+
},
|
|
1325
|
+
secretKey
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
function buildSwarmSelectionEvent(params, secretKey) {
|
|
1329
|
+
if (!HEX_64_REGEX3.test(params.swarmRequestEventId)) {
|
|
1330
|
+
throw new ToonError(
|
|
1331
|
+
"Swarm selection swarmRequestEventId must be a 64-character lowercase hex string",
|
|
1332
|
+
"DVM_INVALID_EVENT_ID"
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (!HEX_64_REGEX3.test(params.winnerResultEventId)) {
|
|
1336
|
+
throw new ToonError(
|
|
1337
|
+
"Swarm selection winnerResultEventId must be a 64-character lowercase hex string",
|
|
1338
|
+
"DVM_INVALID_EVENT_ID"
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
if (!HEX_64_REGEX3.test(params.customerPubkey)) {
|
|
1342
|
+
throw new ToonError(
|
|
1343
|
+
"Swarm selection customerPubkey must be a 64-character lowercase hex string",
|
|
1344
|
+
"DVM_INVALID_PUBKEY"
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
const tags = [
|
|
1348
|
+
["e", params.swarmRequestEventId],
|
|
1349
|
+
["p", params.customerPubkey],
|
|
1350
|
+
["status", "success"],
|
|
1351
|
+
["winner", params.winnerResultEventId]
|
|
1352
|
+
];
|
|
1353
|
+
return finalizeEvent7(
|
|
1354
|
+
{
|
|
1355
|
+
kind: JOB_FEEDBACK_KIND,
|
|
1356
|
+
content: "",
|
|
1357
|
+
tags,
|
|
1358
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1359
|
+
},
|
|
1360
|
+
secretKey
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
function parseSwarmRequest(event) {
|
|
1364
|
+
const base = parseJobRequest(event);
|
|
1365
|
+
if (!base) return null;
|
|
1366
|
+
const swarmTag = event.tags.find((t) => t[0] === "swarm");
|
|
1367
|
+
if (!swarmTag) return null;
|
|
1368
|
+
const maxProvidersStr = swarmTag[1];
|
|
1369
|
+
if (maxProvidersStr === void 0) return null;
|
|
1370
|
+
const maxProviders = parseInt(maxProvidersStr, 10);
|
|
1371
|
+
if (isNaN(maxProviders) || maxProviders < 1) return null;
|
|
1372
|
+
const judgeTag = event.tags.find((t) => t[0] === "judge");
|
|
1373
|
+
const judge = judgeTag?.[1] ?? "customer";
|
|
1374
|
+
return {
|
|
1375
|
+
...base,
|
|
1376
|
+
maxProviders,
|
|
1377
|
+
judge
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function parseSwarmSelection(event) {
|
|
1381
|
+
if (event.kind !== JOB_FEEDBACK_KIND) return null;
|
|
1382
|
+
const winnerTag = event.tags.find((t) => t[0] === "winner");
|
|
1383
|
+
if (!winnerTag) return null;
|
|
1384
|
+
const winnerResultEventId = winnerTag[1];
|
|
1385
|
+
if (winnerResultEventId === void 0 || !HEX_64_REGEX3.test(winnerResultEventId))
|
|
1386
|
+
return null;
|
|
1387
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
1388
|
+
if (!eTag) return null;
|
|
1389
|
+
const swarmRequestEventId = eTag[1];
|
|
1390
|
+
if (swarmRequestEventId === void 0 || !HEX_64_REGEX3.test(swarmRequestEventId))
|
|
1391
|
+
return null;
|
|
1392
|
+
return {
|
|
1393
|
+
swarmRequestEventId,
|
|
1394
|
+
winnerResultEventId
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// src/events/prefix-claim.ts
|
|
1399
|
+
import { finalizeEvent as finalizeEvent8 } from "nostr-tools/pure";
|
|
1400
|
+
function buildPrefixClaimEvent(content, secretKey) {
|
|
1401
|
+
return finalizeEvent8(
|
|
1402
|
+
{
|
|
1403
|
+
kind: PREFIX_CLAIM_KIND,
|
|
1404
|
+
content: JSON.stringify(content),
|
|
1405
|
+
tags: [],
|
|
1406
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1407
|
+
},
|
|
1408
|
+
secretKey
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
function parsePrefixClaimEvent(event) {
|
|
1412
|
+
if (event.kind !== PREFIX_CLAIM_KIND) {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
const parsed = JSON.parse(event.content);
|
|
1417
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
const obj = parsed;
|
|
1421
|
+
const requestedPrefix = obj["requestedPrefix"];
|
|
1422
|
+
if (typeof requestedPrefix !== "string" || requestedPrefix.length === 0) {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
return { requestedPrefix };
|
|
1426
|
+
} catch {
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
function buildPrefixGrantEvent(content, secretKey) {
|
|
1431
|
+
return finalizeEvent8(
|
|
1432
|
+
{
|
|
1433
|
+
kind: PREFIX_GRANT_KIND,
|
|
1434
|
+
content: JSON.stringify(content),
|
|
1435
|
+
tags: [],
|
|
1436
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1437
|
+
},
|
|
1438
|
+
secretKey
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
function parsePrefixGrantEvent(event) {
|
|
1442
|
+
if (event.kind !== PREFIX_GRANT_KIND) {
|
|
1443
|
+
return null;
|
|
1444
|
+
}
|
|
1445
|
+
try {
|
|
1446
|
+
const parsed = JSON.parse(event.content);
|
|
1447
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
const obj = parsed;
|
|
1451
|
+
const grantedPrefix = obj["grantedPrefix"];
|
|
1452
|
+
const claimerPubkey = obj["claimerPubkey"];
|
|
1453
|
+
const ilpAddress = obj["ilpAddress"];
|
|
1454
|
+
if (typeof grantedPrefix !== "string" || grantedPrefix.length === 0) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof claimerPubkey !== "string" || claimerPubkey.length === 0) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
if (typeof ilpAddress !== "string" || ilpAddress.length === 0) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
return {
|
|
1464
|
+
grantedPrefix,
|
|
1465
|
+
claimerPubkey,
|
|
1466
|
+
ilpAddress
|
|
1467
|
+
};
|
|
1468
|
+
} catch {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/bootstrap/AttestationVerifier.ts
|
|
1474
|
+
var AttestationState = /* @__PURE__ */ ((AttestationState2) => {
|
|
1475
|
+
AttestationState2["VALID"] = "valid";
|
|
1476
|
+
AttestationState2["STALE"] = "stale";
|
|
1477
|
+
AttestationState2["UNATTESTED"] = "unattested";
|
|
1478
|
+
return AttestationState2;
|
|
1479
|
+
})(AttestationState || {});
|
|
1480
|
+
var DEFAULT_VALIDITY_SECONDS = 300;
|
|
1481
|
+
var DEFAULT_GRACE_SECONDS = 30;
|
|
1482
|
+
var AttestationVerifier = class {
|
|
1483
|
+
knownGoodPcrs;
|
|
1484
|
+
validitySeconds;
|
|
1485
|
+
graceSeconds;
|
|
1486
|
+
constructor(config) {
|
|
1487
|
+
this.knownGoodPcrs = new Map(config.knownGoodPcrs);
|
|
1488
|
+
const validity = config.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
1489
|
+
const grace = config.graceSeconds ?? DEFAULT_GRACE_SECONDS;
|
|
1490
|
+
if (!Number.isFinite(validity) || validity < 0) {
|
|
1491
|
+
throw new Error(
|
|
1492
|
+
`validitySeconds must be a non-negative finite number, got ${String(validity)}`
|
|
1493
|
+
);
|
|
1494
|
+
}
|
|
1495
|
+
if (!Number.isFinite(grace) || grace < 0) {
|
|
1496
|
+
throw new Error(
|
|
1497
|
+
`graceSeconds must be a non-negative finite number, got ${String(grace)}`
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
this.validitySeconds = validity;
|
|
1501
|
+
this.graceSeconds = grace;
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Verifies a TEE attestation's PCR values against the known-good registry.
|
|
1505
|
+
*
|
|
1506
|
+
* All three PCR values (pcr0, pcr1, pcr2) must be present and truthy in
|
|
1507
|
+
* the registry for verification to pass.
|
|
1508
|
+
*
|
|
1509
|
+
* @param attestation - The TEE attestation to verify.
|
|
1510
|
+
* @returns Verification result with `valid: true` or `valid: false` with reason.
|
|
1511
|
+
*/
|
|
1512
|
+
verify(attestation) {
|
|
1513
|
+
const pcr0Valid = this.knownGoodPcrs.get(attestation.pcr0) === true;
|
|
1514
|
+
const pcr1Valid = this.knownGoodPcrs.get(attestation.pcr1) === true;
|
|
1515
|
+
const pcr2Valid = this.knownGoodPcrs.get(attestation.pcr2) === true;
|
|
1516
|
+
if (pcr0Valid && pcr1Valid && pcr2Valid) {
|
|
1517
|
+
return { valid: true };
|
|
1518
|
+
}
|
|
1519
|
+
return { valid: false, reason: "PCR mismatch" };
|
|
1520
|
+
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Computes the attestation lifecycle state based on timing.
|
|
1523
|
+
*
|
|
1524
|
+
* Boundary behavior:
|
|
1525
|
+
* - At exactly `attestedAt + validitySeconds`: VALID (inclusive <=)
|
|
1526
|
+
* - At exactly `attestedAt + validitySeconds + graceSeconds`: STALE (inclusive <=)
|
|
1527
|
+
* - After grace expires: UNATTESTED
|
|
1528
|
+
*
|
|
1529
|
+
* @param _attestation - The TEE attestation (unused, reserved for future per-attestation logic).
|
|
1530
|
+
* @param attestedAt - Unix timestamp when the attestation was created.
|
|
1531
|
+
* @param now - Optional current unix timestamp (defaults to real clock).
|
|
1532
|
+
* @returns The current attestation state.
|
|
1533
|
+
*/
|
|
1534
|
+
getAttestationState(_attestation, attestedAt, now) {
|
|
1535
|
+
if (!Number.isFinite(attestedAt)) {
|
|
1536
|
+
return "unattested" /* UNATTESTED */;
|
|
1537
|
+
}
|
|
1538
|
+
const currentTime = now ?? Math.floor(Date.now() / 1e3);
|
|
1539
|
+
const validityEnd = attestedAt + this.validitySeconds;
|
|
1540
|
+
const graceEnd = validityEnd + this.graceSeconds;
|
|
1541
|
+
if (currentTime <= validityEnd) {
|
|
1542
|
+
return "valid" /* VALID */;
|
|
1543
|
+
}
|
|
1544
|
+
if (currentTime <= graceEnd) {
|
|
1545
|
+
return "stale" /* STALE */;
|
|
1546
|
+
}
|
|
1547
|
+
return "unattested" /* UNATTESTED */;
|
|
1548
|
+
}
|
|
1549
|
+
/**
|
|
1550
|
+
* Ranks peers by attestation status: attested peers first, then non-attested.
|
|
1551
|
+
*
|
|
1552
|
+
* Preserves relative order within each group (stable sort via filter).
|
|
1553
|
+
* Does NOT mutate the input array -- returns a new sorted array.
|
|
1554
|
+
*
|
|
1555
|
+
* Attestation is a preference, not a requirement. Non-attested peers
|
|
1556
|
+
* remain in the result and are connectable.
|
|
1557
|
+
*
|
|
1558
|
+
* @param peers - Array of peer descriptors to rank.
|
|
1559
|
+
* @returns New array with attested peers first, preserving relative order.
|
|
1560
|
+
*/
|
|
1561
|
+
rankPeers(peers) {
|
|
1562
|
+
const attested = peers.filter((p) => p.attested);
|
|
1563
|
+
const nonAttested = peers.filter((p) => !p.attested);
|
|
1564
|
+
return [...attested, ...nonAttested];
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
// src/events/attested-result-verifier.ts
|
|
1569
|
+
var AttestedResultVerifier = class {
|
|
1570
|
+
attestationVerifier;
|
|
1571
|
+
constructor(options) {
|
|
1572
|
+
this.attestationVerifier = options.attestationVerifier;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* Verifies that a Kind 6xxx result was computed in a valid TEE enclave.
|
|
1576
|
+
*
|
|
1577
|
+
* Performs three checks:
|
|
1578
|
+
* (a) Pubkey match: attestationEvent.pubkey === resultEvent.pubkey
|
|
1579
|
+
* (b) PCR validity: AttestationVerifier.verify(parsedAttestation.attestation)
|
|
1580
|
+
* (c) Time validity: attestation was VALID at resultEvent.created_at
|
|
1581
|
+
*
|
|
1582
|
+
* @param resultEvent - The Kind 6xxx result Nostr event.
|
|
1583
|
+
* @param _parsedResult - The parsed job result (reserved for future use).
|
|
1584
|
+
* @param attestationEvent - The kind:10033 attestation Nostr event.
|
|
1585
|
+
* @param parsedAttestation - The parsed attestation data.
|
|
1586
|
+
* @returns Verification result with valid flag, reason, and attestation state.
|
|
1587
|
+
*/
|
|
1588
|
+
verifyAttestedResult(resultEvent, _parsedResult, attestationEvent, parsedAttestation) {
|
|
1589
|
+
if (attestationEvent.pubkey !== resultEvent.pubkey) {
|
|
1590
|
+
return { valid: false, reason: "pubkey mismatch" };
|
|
1591
|
+
}
|
|
1592
|
+
const pcrResult = this.attestationVerifier.verify(
|
|
1593
|
+
parsedAttestation.attestation
|
|
1594
|
+
);
|
|
1595
|
+
if (!pcrResult.valid) {
|
|
1596
|
+
return { valid: false, reason: "PCR mismatch" };
|
|
1597
|
+
}
|
|
1598
|
+
const state = this.attestationVerifier.getAttestationState(
|
|
1599
|
+
parsedAttestation.attestation,
|
|
1600
|
+
attestationEvent.created_at,
|
|
1601
|
+
resultEvent.created_at
|
|
1602
|
+
);
|
|
1603
|
+
if (state !== "valid" /* VALID */) {
|
|
1604
|
+
return {
|
|
1605
|
+
valid: false,
|
|
1606
|
+
reason: "attestation expired at result creation time",
|
|
1607
|
+
attestationState: state
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
return { valid: true, attestationState: "valid" /* VALID */ };
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
function hasRequireAttestation(params) {
|
|
1614
|
+
return params.some(
|
|
1615
|
+
(p) => p.key === "require_attestation" && p.value === "true"
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/events/arweave-storage.ts
|
|
1620
|
+
import { finalizeEvent as finalizeEvent9 } from "nostr-tools/pure";
|
|
1621
|
+
var BASE64_REGEX2 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
1622
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
1623
|
+
function isValidBase64(str) {
|
|
1624
|
+
if (str.length === 0) return false;
|
|
1625
|
+
if (!BASE64_REGEX2.test(str)) return false;
|
|
1626
|
+
try {
|
|
1627
|
+
Buffer.from(str, "base64");
|
|
1628
|
+
return true;
|
|
1629
|
+
} catch {
|
|
1630
|
+
return false;
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function buildBlobStorageRequest(params, secretKey) {
|
|
1634
|
+
if (!params.blobData || params.blobData.length === 0) {
|
|
1635
|
+
throw new ToonError(
|
|
1636
|
+
"Blob storage request blobData is required and must not be empty",
|
|
1637
|
+
"DVM_MISSING_INPUT"
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
if (typeof params.bid !== "string" || params.bid === "") {
|
|
1641
|
+
throw new ToonError(
|
|
1642
|
+
"Blob storage request bid must be a non-empty string (USDC micro-units)",
|
|
1643
|
+
"DVM_INVALID_BID"
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
const contentType = params.contentType ?? "application/octet-stream";
|
|
1647
|
+
const base64Blob = params.blobData.toString("base64");
|
|
1648
|
+
const tags = [];
|
|
1649
|
+
tags.push(["i", base64Blob, "blob"]);
|
|
1650
|
+
tags.push(["bid", params.bid, "usdc"]);
|
|
1651
|
+
tags.push(["output", contentType]);
|
|
1652
|
+
if (params.params !== void 0) {
|
|
1653
|
+
for (const p of params.params) {
|
|
1654
|
+
tags.push(["param", p.key, p.value]);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
return finalizeEvent9(
|
|
1658
|
+
{
|
|
1659
|
+
kind: BLOB_STORAGE_REQUEST_KIND,
|
|
1660
|
+
content: "",
|
|
1661
|
+
tags,
|
|
1662
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1663
|
+
},
|
|
1664
|
+
secretKey
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
function parseBlobStorageRequest(event) {
|
|
1668
|
+
if (event.kind !== BLOB_STORAGE_REQUEST_KIND) {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
const iTag = event.tags.find((t) => t[0] === "i");
|
|
1672
|
+
if (!iTag) return null;
|
|
1673
|
+
const base64Data = iTag[1];
|
|
1674
|
+
const inputType = iTag[2];
|
|
1675
|
+
if (base64Data === void 0 || base64Data === "") return null;
|
|
1676
|
+
if (inputType !== "blob") return null;
|
|
1677
|
+
if (!isValidBase64(base64Data)) {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
1681
|
+
if (!bidTag) return null;
|
|
1682
|
+
const bidAmount = bidTag[1];
|
|
1683
|
+
if (bidAmount === void 0 || bidAmount === "") return null;
|
|
1684
|
+
const outputTag = event.tags.find((t) => t[0] === "output");
|
|
1685
|
+
const contentType = outputTag?.[1] && outputTag[1] !== "" ? outputTag[1] : "application/octet-stream";
|
|
1686
|
+
const blobData = Buffer.from(base64Data, "base64");
|
|
1687
|
+
const paramTags = event.tags.filter((t) => t[0] === "param");
|
|
1688
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
1689
|
+
for (const pt of paramTags) {
|
|
1690
|
+
const key = pt[1];
|
|
1691
|
+
const value = pt[2];
|
|
1692
|
+
if (key !== void 0 && value !== void 0) {
|
|
1693
|
+
paramMap.set(key, value);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const result = {
|
|
1697
|
+
blobData,
|
|
1698
|
+
contentType
|
|
1699
|
+
};
|
|
1700
|
+
const uploadId = paramMap.get("uploadId");
|
|
1701
|
+
if (uploadId !== void 0 && UUID_REGEX.test(uploadId)) {
|
|
1702
|
+
result.uploadId = uploadId;
|
|
1703
|
+
}
|
|
1704
|
+
const chunkIndexStr = paramMap.get("chunkIndex");
|
|
1705
|
+
if (chunkIndexStr !== void 0) {
|
|
1706
|
+
const chunkIndex = parseInt(chunkIndexStr, 10);
|
|
1707
|
+
if (!isNaN(chunkIndex) && chunkIndex >= 0) {
|
|
1708
|
+
result.chunkIndex = chunkIndex;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const totalChunksStr = paramMap.get("totalChunks");
|
|
1712
|
+
if (totalChunksStr !== void 0) {
|
|
1713
|
+
const totalChunks = parseInt(totalChunksStr, 10);
|
|
1714
|
+
if (!isNaN(totalChunks) && totalChunks > 0) {
|
|
1715
|
+
result.totalChunks = totalChunks;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
return result;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// src/events/reputation.ts
|
|
1722
|
+
import { finalizeEvent as finalizeEvent10 } from "nostr-tools/pure";
|
|
1723
|
+
var HEX_64_REGEX4 = /^[0-9a-f]{64}$/;
|
|
1724
|
+
function buildJobReviewEvent(params, secretKey) {
|
|
1725
|
+
if (!HEX_64_REGEX4.test(params.jobRequestEventId)) {
|
|
1726
|
+
throw new ToonError(
|
|
1727
|
+
"Job review jobRequestEventId must be a 64-character lowercase hex string",
|
|
1728
|
+
"REPUTATION_INVALID_JOB_REQUEST_EVENT_ID"
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
if (!HEX_64_REGEX4.test(params.targetPubkey)) {
|
|
1732
|
+
throw new ToonError(
|
|
1733
|
+
"Job review targetPubkey must be a 64-character lowercase hex string",
|
|
1734
|
+
"REPUTATION_INVALID_TARGET_PUBKEY"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
if (typeof params.rating !== "number" || !Number.isInteger(params.rating) || params.rating < 1 || params.rating > 5) {
|
|
1738
|
+
throw new ToonError(
|
|
1739
|
+
`Job review rating must be an integer 1-5, got ${String(params.rating)}`,
|
|
1740
|
+
"REPUTATION_INVALID_RATING"
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (params.role !== "customer" && params.role !== "provider") {
|
|
1744
|
+
throw new ToonError(
|
|
1745
|
+
`Job review role must be 'customer' or 'provider', got '${String(params.role)}'`,
|
|
1746
|
+
"REPUTATION_INVALID_ROLE"
|
|
1747
|
+
);
|
|
1748
|
+
}
|
|
1749
|
+
const tags = [
|
|
1750
|
+
["d", params.jobRequestEventId],
|
|
1751
|
+
["p", params.targetPubkey],
|
|
1752
|
+
["rating", params.rating.toString()],
|
|
1753
|
+
["role", params.role]
|
|
1754
|
+
];
|
|
1755
|
+
return finalizeEvent10(
|
|
1756
|
+
{
|
|
1757
|
+
kind: JOB_REVIEW_KIND,
|
|
1758
|
+
content: params.content ?? "",
|
|
1759
|
+
tags,
|
|
1760
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1761
|
+
},
|
|
1762
|
+
secretKey
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
function parseJobReview(event) {
|
|
1766
|
+
if (event.kind !== JOB_REVIEW_KIND) {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
1770
|
+
if (!dTag) return null;
|
|
1771
|
+
const jobRequestEventId = dTag[1];
|
|
1772
|
+
if (jobRequestEventId === void 0 || !HEX_64_REGEX4.test(jobRequestEventId))
|
|
1773
|
+
return null;
|
|
1774
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
1775
|
+
if (!pTag) return null;
|
|
1776
|
+
const targetPubkey = pTag[1];
|
|
1777
|
+
if (targetPubkey === void 0 || !HEX_64_REGEX4.test(targetPubkey))
|
|
1778
|
+
return null;
|
|
1779
|
+
const ratingTag = event.tags.find((t) => t[0] === "rating");
|
|
1780
|
+
if (!ratingTag) return null;
|
|
1781
|
+
const ratingStr = ratingTag[1];
|
|
1782
|
+
if (ratingStr === void 0) return null;
|
|
1783
|
+
const rating = Number(ratingStr);
|
|
1784
|
+
if (!Number.isInteger(rating) || rating < 1 || rating > 5) return null;
|
|
1785
|
+
const roleTag = event.tags.find((t) => t[0] === "role");
|
|
1786
|
+
if (!roleTag) return null;
|
|
1787
|
+
const role = roleTag[1];
|
|
1788
|
+
if (role !== "customer" && role !== "provider") return null;
|
|
1789
|
+
return {
|
|
1790
|
+
jobRequestEventId,
|
|
1791
|
+
targetPubkey,
|
|
1792
|
+
rating,
|
|
1793
|
+
role,
|
|
1794
|
+
content: event.content
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
function buildWotDeclarationEvent(params, secretKey) {
|
|
1798
|
+
if (!HEX_64_REGEX4.test(params.targetPubkey)) {
|
|
1799
|
+
throw new ToonError(
|
|
1800
|
+
"WoT declaration targetPubkey must be a 64-character lowercase hex string",
|
|
1801
|
+
"REPUTATION_INVALID_TARGET_PUBKEY"
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
const tags = [
|
|
1805
|
+
["d", params.targetPubkey],
|
|
1806
|
+
["p", params.targetPubkey]
|
|
1807
|
+
];
|
|
1808
|
+
return finalizeEvent10(
|
|
1809
|
+
{
|
|
1810
|
+
kind: WEB_OF_TRUST_KIND,
|
|
1811
|
+
content: params.content ?? "",
|
|
1812
|
+
tags,
|
|
1813
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1814
|
+
},
|
|
1815
|
+
secretKey
|
|
1816
|
+
);
|
|
721
1817
|
}
|
|
722
|
-
function
|
|
723
|
-
if (event.kind !==
|
|
1818
|
+
function parseWotDeclaration(event) {
|
|
1819
|
+
if (event.kind !== WEB_OF_TRUST_KIND) {
|
|
724
1820
|
return null;
|
|
725
1821
|
}
|
|
726
|
-
const
|
|
727
|
-
if (!
|
|
728
|
-
const
|
|
729
|
-
if (
|
|
730
|
-
return null;
|
|
1822
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
1823
|
+
if (!dTag) return null;
|
|
1824
|
+
const dValue = dTag[1];
|
|
1825
|
+
if (dValue === void 0) return null;
|
|
731
1826
|
const pTag = event.tags.find((t) => t[0] === "p");
|
|
732
1827
|
if (!pTag) return null;
|
|
733
|
-
const
|
|
734
|
-
if (
|
|
1828
|
+
const targetPubkey = pTag[1];
|
|
1829
|
+
if (targetPubkey === void 0 || !HEX_64_REGEX4.test(targetPubkey))
|
|
735
1830
|
return null;
|
|
736
|
-
|
|
737
|
-
if (!statusTag) return null;
|
|
738
|
-
const statusValue = statusTag[1];
|
|
739
|
-
if (statusValue === void 0) return null;
|
|
740
|
-
if (!VALID_STATUSES.has(statusValue)) {
|
|
741
|
-
return null;
|
|
742
|
-
}
|
|
1831
|
+
if (dValue !== targetPubkey) return null;
|
|
743
1832
|
return {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
status: statusValue,
|
|
1833
|
+
targetPubkey,
|
|
1834
|
+
declarerPubkey: event.pubkey,
|
|
747
1835
|
content: event.content
|
|
748
1836
|
};
|
|
749
1837
|
}
|
|
1838
|
+
var ReputationScoreCalculator = class {
|
|
1839
|
+
/**
|
|
1840
|
+
* Computes the composite reputation score from pre-computed signals.
|
|
1841
|
+
*
|
|
1842
|
+
* @param signals - The individual signal values.
|
|
1843
|
+
* @returns The composite score with individual signals.
|
|
1844
|
+
*/
|
|
1845
|
+
calculateScore(signals) {
|
|
1846
|
+
const score = signals.trustedBy * 100 + Math.log10(Math.max(1, signals.channelVolumeUsdc)) * 10 + signals.jobsCompleted * 5 + signals.avgRating * 20;
|
|
1847
|
+
if (!isFinite(score)) {
|
|
1848
|
+
return { score: 0, signals };
|
|
1849
|
+
}
|
|
1850
|
+
return { score, signals };
|
|
1851
|
+
}
|
|
1852
|
+
/**
|
|
1853
|
+
* Computes the threshold-filtered trusted_by count from WoT declarations.
|
|
1854
|
+
*
|
|
1855
|
+
* Declarers with non-zero channel volume contribute 1 to the count.
|
|
1856
|
+
* Declarers with zero channel volume contribute 0 (sybil defense).
|
|
1857
|
+
*
|
|
1858
|
+
* @param wotDeclarations - Parsed WoT declarations targeting the provider.
|
|
1859
|
+
* @param getChannelVolume - Callback to look up a declarer's channel volume.
|
|
1860
|
+
* @returns The count of WoT declarations from non-zero-volume declarers.
|
|
1861
|
+
*/
|
|
1862
|
+
computeTrustedBy(wotDeclarations, getChannelVolume) {
|
|
1863
|
+
let count = 0;
|
|
1864
|
+
for (const declaration of wotDeclarations) {
|
|
1865
|
+
const volume = getChannelVolume(declaration.declarerPubkey);
|
|
1866
|
+
if (volume > 0) {
|
|
1867
|
+
count += 1;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return count;
|
|
1871
|
+
}
|
|
1872
|
+
/**
|
|
1873
|
+
* Computes the average rating from verified customer reviews only.
|
|
1874
|
+
*
|
|
1875
|
+
* Reviews are provided as tuples of `{ review, reviewerPubkey }` so the
|
|
1876
|
+
* calculator can filter by the verified customer set. Reviews from pubkeys
|
|
1877
|
+
* NOT in `verifiedCustomerPubkeys` are excluded entirely (customer-gate
|
|
1878
|
+
* sybil defense per E6-R013).
|
|
1879
|
+
*
|
|
1880
|
+
* @param reviews - Parsed job reviews with reviewer pubkeys.
|
|
1881
|
+
* @param verifiedCustomerPubkeys - Set of pubkeys that authored Kind 5xxx requests.
|
|
1882
|
+
* @returns The mean rating from verified reviews, or 0 when no verified reviews exist.
|
|
1883
|
+
*/
|
|
1884
|
+
computeAvgRating(reviews, verifiedCustomerPubkeys) {
|
|
1885
|
+
let sum = 0;
|
|
1886
|
+
let count = 0;
|
|
1887
|
+
for (const { review, reviewerPubkey } of reviews) {
|
|
1888
|
+
if (!verifiedCustomerPubkeys.has(reviewerPubkey)) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
if (Number.isInteger(review.rating) && review.rating >= 1 && review.rating <= 5) {
|
|
1892
|
+
sum += review.rating;
|
|
1893
|
+
count += 1;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
return count === 0 ? 0 : sum / count;
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
function hasMinReputation(params) {
|
|
1900
|
+
const param = params.find((p) => p.key === "min_reputation");
|
|
1901
|
+
if (param === void 0) {
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
const trimmed = param.value.trim();
|
|
1905
|
+
const numericValue = Number(trimmed);
|
|
1906
|
+
if (trimmed === "" || isNaN(numericValue) || !isFinite(numericValue)) {
|
|
1907
|
+
throw new ToonError(
|
|
1908
|
+
`min_reputation value must be numeric, got '${param.value}'`,
|
|
1909
|
+
"REPUTATION_INVALID_MIN_REPUTATION"
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
return numericValue;
|
|
1913
|
+
}
|
|
750
1914
|
|
|
751
1915
|
// src/discovery/NostrPeerDiscovery.ts
|
|
752
1916
|
import { SimplePool } from "nostr-tools/pool";
|
|
@@ -1456,6 +2620,98 @@ async function publishSeedRelayEntry(config) {
|
|
|
1456
2620
|
return { publishedTo, eventId: event.id };
|
|
1457
2621
|
}
|
|
1458
2622
|
|
|
2623
|
+
// src/fee/calculate-route-amount.ts
|
|
2624
|
+
function calculateRouteAmount(params) {
|
|
2625
|
+
const { basePricePerByte, packetByteLength, hopFees } = params;
|
|
2626
|
+
if (packetByteLength < 0) {
|
|
2627
|
+
return 0n;
|
|
2628
|
+
}
|
|
2629
|
+
const bytes = BigInt(packetByteLength);
|
|
2630
|
+
const baseAmount = basePricePerByte * bytes;
|
|
2631
|
+
const intermediaryFees = hopFees.reduce((sum, fee) => sum + fee * bytes, 0n);
|
|
2632
|
+
return baseAmount + intermediaryFees;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/fee/resolve-route-fees.ts
|
|
2636
|
+
var _cachedPeersRef = null;
|
|
2637
|
+
var _cachedPeerFingerprint = "";
|
|
2638
|
+
var _cachedPeerMap = null;
|
|
2639
|
+
function peerFingerprint(peers) {
|
|
2640
|
+
let fp = `${peers.length}:`;
|
|
2641
|
+
for (const p of peers) {
|
|
2642
|
+
fp += `${p.peerInfo.ilpAddress}=${p.peerInfo.feePerByte};`;
|
|
2643
|
+
}
|
|
2644
|
+
return fp;
|
|
2645
|
+
}
|
|
2646
|
+
function getPeerByAddressMap(discoveredPeers) {
|
|
2647
|
+
const fp = peerFingerprint(discoveredPeers);
|
|
2648
|
+
if (_cachedPeersRef === discoveredPeers && _cachedPeerFingerprint === fp && _cachedPeerMap) {
|
|
2649
|
+
return _cachedPeerMap;
|
|
2650
|
+
}
|
|
2651
|
+
const peerByAddress = /* @__PURE__ */ new Map();
|
|
2652
|
+
for (const peer of discoveredPeers) {
|
|
2653
|
+
peerByAddress.set(peer.peerInfo.ilpAddress, peer);
|
|
2654
|
+
if (peer.peerInfo.ilpAddresses) {
|
|
2655
|
+
for (const addr of peer.peerInfo.ilpAddresses) {
|
|
2656
|
+
peerByAddress.set(addr, peer);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
_cachedPeersRef = discoveredPeers;
|
|
2661
|
+
_cachedPeerFingerprint = fp;
|
|
2662
|
+
_cachedPeerMap = peerByAddress;
|
|
2663
|
+
return peerByAddress;
|
|
2664
|
+
}
|
|
2665
|
+
function resolveRouteFees(params) {
|
|
2666
|
+
const { destination, ownIlpAddress, discoveredPeers } = params;
|
|
2667
|
+
if (!destination || !destination.trim() || !ownIlpAddress || !ownIlpAddress.trim()) {
|
|
2668
|
+
return { hopFees: [], warnings: [] };
|
|
2669
|
+
}
|
|
2670
|
+
const senderSegments = ownIlpAddress.split(".");
|
|
2671
|
+
const destSegments = destination.split(".");
|
|
2672
|
+
let lcaLength = 0;
|
|
2673
|
+
const minLength = Math.min(senderSegments.length, destSegments.length);
|
|
2674
|
+
for (let i = 0; i < minLength; i++) {
|
|
2675
|
+
if (senderSegments[i] === destSegments[i]) {
|
|
2676
|
+
lcaLength = i + 1;
|
|
2677
|
+
} else {
|
|
2678
|
+
break;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
const intermediaryPrefixes = [];
|
|
2682
|
+
for (let i = lcaLength; i < destSegments.length - 1; i++) {
|
|
2683
|
+
const prefix = destSegments.slice(0, i + 1).join(".");
|
|
2684
|
+
intermediaryPrefixes.push(prefix);
|
|
2685
|
+
}
|
|
2686
|
+
if (intermediaryPrefixes.length === 0) {
|
|
2687
|
+
return { hopFees: [], warnings: [] };
|
|
2688
|
+
}
|
|
2689
|
+
const peerByAddress = getPeerByAddressMap(discoveredPeers);
|
|
2690
|
+
const hopFees = [];
|
|
2691
|
+
const warnings = [];
|
|
2692
|
+
for (const prefix of intermediaryPrefixes) {
|
|
2693
|
+
const peer = peerByAddress.get(prefix);
|
|
2694
|
+
if (peer) {
|
|
2695
|
+
let fee;
|
|
2696
|
+
try {
|
|
2697
|
+
fee = BigInt(peer.peerInfo.feePerByte ?? "0");
|
|
2698
|
+
} catch {
|
|
2699
|
+
fee = 0n;
|
|
2700
|
+
warnings.push(
|
|
2701
|
+
`Invalid feePerByte "${peer.peerInfo.feePerByte}" at ${prefix}: defaulting to 0`
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
hopFees.push(fee < 0n ? 0n : fee);
|
|
2705
|
+
} else {
|
|
2706
|
+
hopFees.push(0n);
|
|
2707
|
+
warnings.push(
|
|
2708
|
+
`Unknown intermediary at ${prefix}: defaulting feePerByte to 0`
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return { hopFees, warnings };
|
|
2713
|
+
}
|
|
2714
|
+
|
|
1459
2715
|
// src/settlement/settlement.ts
|
|
1460
2716
|
function negotiateSettlementChain(requesterChains, responderChains, requesterPreferredTokens, responderPreferredTokens) {
|
|
1461
2717
|
const responderSet = new Set(responderChains);
|
|
@@ -1788,29 +3044,12 @@ var BootstrapService = class {
|
|
|
1788
3044
|
const tokenNetwork = peerInfo.tokenNetworks?.[negotiatedChain];
|
|
1789
3045
|
if (peerAddress) {
|
|
1790
3046
|
console.log(
|
|
1791
|
-
`[Bootstrap]
|
|
3047
|
+
`[Bootstrap] Negotiated ${negotiatedChain} with ${registeredPeerId} (lazy \u2014 channel deferred)`
|
|
1792
3048
|
);
|
|
1793
|
-
const channelResult = await this.channelClient.openChannel({
|
|
1794
|
-
peerId: registeredPeerId,
|
|
1795
|
-
chain: negotiatedChain,
|
|
1796
|
-
token: tokenAddress,
|
|
1797
|
-
tokenNetwork,
|
|
1798
|
-
peerAddress,
|
|
1799
|
-
initialDeposit: "100000",
|
|
1800
|
-
settlementTimeout: 86400
|
|
1801
|
-
});
|
|
1802
|
-
result.channelId = channelResult.channelId;
|
|
1803
3049
|
result.negotiatedChain = negotiatedChain;
|
|
1804
3050
|
result.settlementAddress = peerAddress;
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
);
|
|
1808
|
-
this.emit({
|
|
1809
|
-
type: "bootstrap:channel-opened",
|
|
1810
|
-
peerId: registeredPeerId,
|
|
1811
|
-
channelId: channelResult.channelId,
|
|
1812
|
-
negotiatedChain
|
|
1813
|
-
});
|
|
3051
|
+
result.tokenAddress = tokenAddress;
|
|
3052
|
+
result.tokenNetwork = tokenNetwork;
|
|
1814
3053
|
if (this.connectorAdmin) {
|
|
1815
3054
|
await this.connectorAdmin.addPeer({
|
|
1816
3055
|
id: registeredPeerId,
|
|
@@ -1821,10 +3060,7 @@ var BootstrapService = class {
|
|
|
1821
3060
|
preference: negotiatedChain,
|
|
1822
3061
|
...peerAddress && { evmAddress: peerAddress },
|
|
1823
3062
|
...tokenAddress && { tokenAddress },
|
|
1824
|
-
...tokenNetwork && { tokenNetworkAddress: tokenNetwork }
|
|
1825
|
-
...channelResult.channelId && {
|
|
1826
|
-
channelId: channelResult.channelId
|
|
1827
|
-
}
|
|
3063
|
+
...tokenNetwork && { tokenNetworkAddress: tokenNetwork }
|
|
1828
3064
|
}
|
|
1829
3065
|
});
|
|
1830
3066
|
}
|
|
@@ -1885,7 +3121,7 @@ var BootstrapService = class {
|
|
|
1885
3121
|
}
|
|
1886
3122
|
if (ilpResult.accepted) {
|
|
1887
3123
|
console.log(
|
|
1888
|
-
`[Bootstrap] Announced to ${result.registeredPeerId} via ILP (
|
|
3124
|
+
`[Bootstrap] Announced to ${result.registeredPeerId} via ILP (eventId: ${ilpInfoEvent.id})`
|
|
1889
3125
|
);
|
|
1890
3126
|
this.emit({
|
|
1891
3127
|
type: "bootstrap:announced",
|
|
@@ -2148,6 +3384,56 @@ var BootstrapService = class {
|
|
|
2148
3384
|
);
|
|
2149
3385
|
}
|
|
2150
3386
|
}
|
|
3387
|
+
/**
|
|
3388
|
+
* Re-advertise own kind:10032 to all previously bootstrapped peers.
|
|
3389
|
+
*
|
|
3390
|
+
* Called after topology changes (addUpstreamPeer/removeUpstreamPeer) to
|
|
3391
|
+
* propagate updated ILP address lists to the network. Uses the same
|
|
3392
|
+
* ILP-first announcement path as the initial Phase 2 bootstrap.
|
|
3393
|
+
*
|
|
3394
|
+
* For non-ILP mode, publishes directly to each peer's relay URL.
|
|
3395
|
+
*
|
|
3396
|
+
* @param results - The bootstrap results from the initial bootstrap() call
|
|
3397
|
+
* @returns Number of peers successfully re-announced to
|
|
3398
|
+
*/
|
|
3399
|
+
async republish(results) {
|
|
3400
|
+
if (results.length === 0) {
|
|
3401
|
+
return 0;
|
|
3402
|
+
}
|
|
3403
|
+
let successCount = 0;
|
|
3404
|
+
if (this.ilpClient && this.toonEncoder) {
|
|
3405
|
+
for (const result of results) {
|
|
3406
|
+
try {
|
|
3407
|
+
await this.announceViaIlp(result);
|
|
3408
|
+
successCount++;
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
3411
|
+
this.emit({
|
|
3412
|
+
type: "bootstrap:announce-failed",
|
|
3413
|
+
peerId: result.registeredPeerId,
|
|
3414
|
+
reason: `republish: ${reason}`
|
|
3415
|
+
});
|
|
3416
|
+
console.warn(
|
|
3417
|
+
`[Bootstrap] Republish failed for ${result.registeredPeerId}:`,
|
|
3418
|
+
reason
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
} else {
|
|
3423
|
+
for (const result of results) {
|
|
3424
|
+
try {
|
|
3425
|
+
await this.publishOurInfo(result.knownPeer.relayUrl);
|
|
3426
|
+
successCount++;
|
|
3427
|
+
} catch (error) {
|
|
3428
|
+
console.warn(
|
|
3429
|
+
`[Bootstrap] Republish to ${result.knownPeer.relayUrl} failed:`,
|
|
3430
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
3431
|
+
);
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return successCount;
|
|
3436
|
+
}
|
|
2151
3437
|
/**
|
|
2152
3438
|
* Get our pubkey.
|
|
2153
3439
|
*/
|
|
@@ -2170,10 +3456,11 @@ function createDiscoveryTracker(config) {
|
|
|
2170
3456
|
const pubkey = getPublicKey4(config.secretKey);
|
|
2171
3457
|
const excludedPubkeys = /* @__PURE__ */ new Set([pubkey]);
|
|
2172
3458
|
let connectorAdmin;
|
|
2173
|
-
let
|
|
3459
|
+
let _channelClient;
|
|
2174
3460
|
let listeners = [];
|
|
2175
3461
|
const discoveredPeers = /* @__PURE__ */ new Map();
|
|
2176
3462
|
const peeredPubkeys = /* @__PURE__ */ new Set();
|
|
3463
|
+
const peerNegotiations = /* @__PURE__ */ new Map();
|
|
2177
3464
|
const peerTimestamps = /* @__PURE__ */ new Map();
|
|
2178
3465
|
function emit(event) {
|
|
2179
3466
|
for (const listener of listeners) {
|
|
@@ -2197,7 +3484,8 @@ function createDiscoveryTracker(config) {
|
|
|
2197
3484
|
});
|
|
2198
3485
|
}).catch((error) => {
|
|
2199
3486
|
console.warn(
|
|
2200
|
-
|
|
3487
|
+
"[DiscoveryTracker] Failed to deregister %s: %s",
|
|
3488
|
+
peerId,
|
|
2201
3489
|
error instanceof Error ? error.message : "Unknown error"
|
|
2202
3490
|
);
|
|
2203
3491
|
});
|
|
@@ -2278,7 +3566,8 @@ function createDiscoveryTracker(config) {
|
|
|
2278
3566
|
peeredPubkeys.delete(targetPubkey);
|
|
2279
3567
|
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2280
3568
|
console.warn(
|
|
2281
|
-
|
|
3569
|
+
"[DiscoveryTracker] Failed to register %s: %s",
|
|
3570
|
+
peerId,
|
|
2282
3571
|
reason
|
|
2283
3572
|
);
|
|
2284
3573
|
emit({
|
|
@@ -2288,7 +3577,7 @@ function createDiscoveryTracker(config) {
|
|
|
2288
3577
|
});
|
|
2289
3578
|
return;
|
|
2290
3579
|
}
|
|
2291
|
-
if (
|
|
3580
|
+
if (config.settlementInfo?.supportedChains?.length && peerInfo.supportedChains?.length && peerInfo.settlementAddresses) {
|
|
2292
3581
|
try {
|
|
2293
3582
|
const negotiatedChain = negotiateSettlementChain(
|
|
2294
3583
|
config.settlementInfo.supportedChains,
|
|
@@ -2305,44 +3594,25 @@ function createDiscoveryTracker(config) {
|
|
|
2305
3594
|
);
|
|
2306
3595
|
const tokenNetwork = peerInfo.tokenNetworks?.[negotiatedChain];
|
|
2307
3596
|
if (peerAddress) {
|
|
2308
|
-
|
|
2309
|
-
peerId,
|
|
3597
|
+
peerNegotiations.set(peerId, {
|
|
2310
3598
|
chain: negotiatedChain,
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
settlementTimeout: 86400
|
|
2316
|
-
});
|
|
2317
|
-
await admin.addPeer({
|
|
2318
|
-
id: peerId,
|
|
2319
|
-
url: peerInfo.btpEndpoint,
|
|
2320
|
-
authToken: "",
|
|
2321
|
-
routes: [{ prefix: peerInfo.ilpAddress }],
|
|
2322
|
-
settlement: {
|
|
2323
|
-
preference: negotiatedChain,
|
|
2324
|
-
...peerAddress && { evmAddress: peerAddress },
|
|
2325
|
-
...tokenAddress && { tokenAddress },
|
|
2326
|
-
...tokenNetwork && {
|
|
2327
|
-
tokenNetworkAddress: tokenNetwork
|
|
2328
|
-
},
|
|
2329
|
-
...channelResult.channelId && {
|
|
2330
|
-
channelId: channelResult.channelId
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
});
|
|
2334
|
-
emit({
|
|
2335
|
-
type: "bootstrap:channel-opened",
|
|
2336
|
-
peerId,
|
|
2337
|
-
channelId: channelResult.channelId,
|
|
2338
|
-
negotiatedChain
|
|
3599
|
+
chainType: negotiatedChain.split(":")[0] ?? negotiatedChain,
|
|
3600
|
+
settlementAddress: peerAddress,
|
|
3601
|
+
tokenAddress,
|
|
3602
|
+
tokenNetwork
|
|
2339
3603
|
});
|
|
3604
|
+
console.log(
|
|
3605
|
+
"[DiscoveryTracker] Negotiated %s with %s (lazy \u2014 channel deferred)",
|
|
3606
|
+
negotiatedChain,
|
|
3607
|
+
peerId
|
|
3608
|
+
);
|
|
2340
3609
|
}
|
|
2341
3610
|
}
|
|
2342
3611
|
} catch (error) {
|
|
2343
3612
|
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2344
3613
|
console.warn(
|
|
2345
|
-
|
|
3614
|
+
"[DiscoveryTracker] Settlement negotiation failed for %s: %s",
|
|
3615
|
+
peerId,
|
|
2346
3616
|
reason
|
|
2347
3617
|
);
|
|
2348
3618
|
emit({
|
|
@@ -2358,6 +3628,9 @@ function createDiscoveryTracker(config) {
|
|
|
2358
3628
|
(p) => !peeredPubkeys.has(p.pubkey)
|
|
2359
3629
|
);
|
|
2360
3630
|
},
|
|
3631
|
+
getAllDiscoveredPeers() {
|
|
3632
|
+
return [...discoveredPeers.values()];
|
|
3633
|
+
},
|
|
2361
3634
|
isPeered(targetPubkey) {
|
|
2362
3635
|
return peeredPubkeys.has(targetPubkey);
|
|
2363
3636
|
},
|
|
@@ -2377,7 +3650,10 @@ function createDiscoveryTracker(config) {
|
|
|
2377
3650
|
connectorAdmin = admin;
|
|
2378
3651
|
},
|
|
2379
3652
|
setChannelClient(client) {
|
|
2380
|
-
|
|
3653
|
+
_channelClient = client;
|
|
3654
|
+
},
|
|
3655
|
+
getPeerNegotiation(peerId) {
|
|
3656
|
+
return peerNegotiations.get(peerId);
|
|
2381
3657
|
},
|
|
2382
3658
|
addExcludedPubkeys(pubkeys) {
|
|
2383
3659
|
for (const pk of pubkeys) {
|
|
@@ -2425,7 +3701,6 @@ function createHttpIlpClient(baseUrl) {
|
|
|
2425
3701
|
const data = await response.json();
|
|
2426
3702
|
return {
|
|
2427
3703
|
accepted: data["accepted"] ?? data["fulfilled"] ?? false,
|
|
2428
|
-
fulfillment: data["fulfillment"],
|
|
2429
3704
|
data: data["data"],
|
|
2430
3705
|
code: data["code"],
|
|
2431
3706
|
message: data["message"]
|
|
@@ -2437,29 +3712,21 @@ var createHttpRuntimeClient = createHttpIlpClient;
|
|
|
2437
3712
|
var createAgentRuntimeClient = createHttpIlpClient;
|
|
2438
3713
|
|
|
2439
3714
|
// src/bootstrap/direct-ilp-client.ts
|
|
2440
|
-
|
|
2441
|
-
function createDirectIlpClient(connector, config) {
|
|
3715
|
+
function createDirectIlpClient(connector, _config) {
|
|
2442
3716
|
return {
|
|
2443
3717
|
async sendIlpPacket(params) {
|
|
2444
3718
|
try {
|
|
2445
3719
|
const amount = BigInt(params.amount);
|
|
2446
3720
|
const data = Uint8Array.from(Buffer.from(params.data, "base64"));
|
|
2447
|
-
let executionCondition;
|
|
2448
|
-
if (data.length > 0) {
|
|
2449
|
-
const fulfillment = createHash("sha256").update(data).digest();
|
|
2450
|
-
executionCondition = createHash("sha256").update(fulfillment).digest();
|
|
2451
|
-
}
|
|
2452
3721
|
const result = await connector.sendPacket({
|
|
2453
3722
|
destination: params.destination,
|
|
2454
3723
|
amount,
|
|
2455
3724
|
data,
|
|
2456
|
-
executionCondition,
|
|
2457
3725
|
expiresAt: new Date(Date.now() + 3e4)
|
|
2458
3726
|
});
|
|
2459
3727
|
if (result.type === "fulfill" || result.type === 13) {
|
|
2460
3728
|
return {
|
|
2461
3729
|
accepted: true,
|
|
2462
|
-
fulfillment: Buffer.from(result.fulfillment).toString("base64"),
|
|
2463
3730
|
data: result.data ? Buffer.from(result.data).toString("base64") : void 0
|
|
2464
3731
|
};
|
|
2465
3732
|
}
|
|
@@ -2638,7 +3905,6 @@ function createHttpIlpClient2(connectorUrl) {
|
|
|
2638
3905
|
if (result["accept"] || result["accepted"]) {
|
|
2639
3906
|
return {
|
|
2640
3907
|
accepted: true,
|
|
2641
|
-
fulfillment: result["fulfillment"],
|
|
2642
3908
|
data: result["data"]
|
|
2643
3909
|
};
|
|
2644
3910
|
} else {
|
|
@@ -2726,101 +3992,6 @@ function createHttpChannelClient(adminUrl) {
|
|
|
2726
3992
|
};
|
|
2727
3993
|
}
|
|
2728
3994
|
|
|
2729
|
-
// src/bootstrap/AttestationVerifier.ts
|
|
2730
|
-
var AttestationState = /* @__PURE__ */ ((AttestationState2) => {
|
|
2731
|
-
AttestationState2["VALID"] = "valid";
|
|
2732
|
-
AttestationState2["STALE"] = "stale";
|
|
2733
|
-
AttestationState2["UNATTESTED"] = "unattested";
|
|
2734
|
-
return AttestationState2;
|
|
2735
|
-
})(AttestationState || {});
|
|
2736
|
-
var DEFAULT_VALIDITY_SECONDS = 300;
|
|
2737
|
-
var DEFAULT_GRACE_SECONDS = 30;
|
|
2738
|
-
var AttestationVerifier = class {
|
|
2739
|
-
knownGoodPcrs;
|
|
2740
|
-
validitySeconds;
|
|
2741
|
-
graceSeconds;
|
|
2742
|
-
constructor(config) {
|
|
2743
|
-
this.knownGoodPcrs = new Map(config.knownGoodPcrs);
|
|
2744
|
-
const validity = config.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
2745
|
-
const grace = config.graceSeconds ?? DEFAULT_GRACE_SECONDS;
|
|
2746
|
-
if (!Number.isFinite(validity) || validity < 0) {
|
|
2747
|
-
throw new Error(
|
|
2748
|
-
`validitySeconds must be a non-negative finite number, got ${String(validity)}`
|
|
2749
|
-
);
|
|
2750
|
-
}
|
|
2751
|
-
if (!Number.isFinite(grace) || grace < 0) {
|
|
2752
|
-
throw new Error(
|
|
2753
|
-
`graceSeconds must be a non-negative finite number, got ${String(grace)}`
|
|
2754
|
-
);
|
|
2755
|
-
}
|
|
2756
|
-
this.validitySeconds = validity;
|
|
2757
|
-
this.graceSeconds = grace;
|
|
2758
|
-
}
|
|
2759
|
-
/**
|
|
2760
|
-
* Verifies a TEE attestation's PCR values against the known-good registry.
|
|
2761
|
-
*
|
|
2762
|
-
* All three PCR values (pcr0, pcr1, pcr2) must be present and truthy in
|
|
2763
|
-
* the registry for verification to pass.
|
|
2764
|
-
*
|
|
2765
|
-
* @param attestation - The TEE attestation to verify.
|
|
2766
|
-
* @returns Verification result with `valid: true` or `valid: false` with reason.
|
|
2767
|
-
*/
|
|
2768
|
-
verify(attestation) {
|
|
2769
|
-
const pcr0Valid = this.knownGoodPcrs.get(attestation.pcr0) === true;
|
|
2770
|
-
const pcr1Valid = this.knownGoodPcrs.get(attestation.pcr1) === true;
|
|
2771
|
-
const pcr2Valid = this.knownGoodPcrs.get(attestation.pcr2) === true;
|
|
2772
|
-
if (pcr0Valid && pcr1Valid && pcr2Valid) {
|
|
2773
|
-
return { valid: true };
|
|
2774
|
-
}
|
|
2775
|
-
return { valid: false, reason: "PCR mismatch" };
|
|
2776
|
-
}
|
|
2777
|
-
/**
|
|
2778
|
-
* Computes the attestation lifecycle state based on timing.
|
|
2779
|
-
*
|
|
2780
|
-
* Boundary behavior:
|
|
2781
|
-
* - At exactly `attestedAt + validitySeconds`: VALID (inclusive <=)
|
|
2782
|
-
* - At exactly `attestedAt + validitySeconds + graceSeconds`: STALE (inclusive <=)
|
|
2783
|
-
* - After grace expires: UNATTESTED
|
|
2784
|
-
*
|
|
2785
|
-
* @param _attestation - The TEE attestation (unused, reserved for future per-attestation logic).
|
|
2786
|
-
* @param attestedAt - Unix timestamp when the attestation was created.
|
|
2787
|
-
* @param now - Optional current unix timestamp (defaults to real clock).
|
|
2788
|
-
* @returns The current attestation state.
|
|
2789
|
-
*/
|
|
2790
|
-
getAttestationState(_attestation, attestedAt, now) {
|
|
2791
|
-
if (!Number.isFinite(attestedAt)) {
|
|
2792
|
-
return "unattested" /* UNATTESTED */;
|
|
2793
|
-
}
|
|
2794
|
-
const currentTime = now ?? Math.floor(Date.now() / 1e3);
|
|
2795
|
-
const validityEnd = attestedAt + this.validitySeconds;
|
|
2796
|
-
const graceEnd = validityEnd + this.graceSeconds;
|
|
2797
|
-
if (currentTime <= validityEnd) {
|
|
2798
|
-
return "valid" /* VALID */;
|
|
2799
|
-
}
|
|
2800
|
-
if (currentTime <= graceEnd) {
|
|
2801
|
-
return "stale" /* STALE */;
|
|
2802
|
-
}
|
|
2803
|
-
return "unattested" /* UNATTESTED */;
|
|
2804
|
-
}
|
|
2805
|
-
/**
|
|
2806
|
-
* Ranks peers by attestation status: attested peers first, then non-attested.
|
|
2807
|
-
*
|
|
2808
|
-
* Preserves relative order within each group (stable sort via filter).
|
|
2809
|
-
* Does NOT mutate the input array -- returns a new sorted array.
|
|
2810
|
-
*
|
|
2811
|
-
* Attestation is a preference, not a requirement. Non-attested peers
|
|
2812
|
-
* remain in the result and are connectable.
|
|
2813
|
-
*
|
|
2814
|
-
* @param peers - Array of peer descriptors to rank.
|
|
2815
|
-
* @returns New array with attested peers first, preserving relative order.
|
|
2816
|
-
*/
|
|
2817
|
-
rankPeers(peers) {
|
|
2818
|
-
const attested = peers.filter((p) => p.attested);
|
|
2819
|
-
const nonAttested = peers.filter((p) => !p.attested);
|
|
2820
|
-
return [...attested, ...nonAttested];
|
|
2821
|
-
}
|
|
2822
|
-
};
|
|
2823
|
-
|
|
2824
3995
|
// src/bootstrap/AttestationBootstrap.ts
|
|
2825
3996
|
var AttestationBootstrap = class {
|
|
2826
3997
|
config;
|
|
@@ -2978,7 +4149,19 @@ function createToonNode(config) {
|
|
|
2978
4149
|
}
|
|
2979
4150
|
try {
|
|
2980
4151
|
if (config.connector.setPacketHandler) {
|
|
2981
|
-
|
|
4152
|
+
const adaptedHandler = async (request) => {
|
|
4153
|
+
const result = await config.handlePacket(request);
|
|
4154
|
+
if (result.accept) {
|
|
4155
|
+
return result;
|
|
4156
|
+
}
|
|
4157
|
+
const adapted = result;
|
|
4158
|
+
adapted.rejectReason = {
|
|
4159
|
+
code: result.code,
|
|
4160
|
+
message: result.message
|
|
4161
|
+
};
|
|
4162
|
+
return adapted;
|
|
4163
|
+
};
|
|
4164
|
+
config.connector.setPacketHandler(adaptedHandler);
|
|
2982
4165
|
}
|
|
2983
4166
|
const results = await bootstrapService.bootstrap(
|
|
2984
4167
|
config.additionalPeersJson
|
|
@@ -3079,6 +4262,97 @@ function buildEip712Domain(config) {
|
|
|
3079
4262
|
verifyingContract: config.tokenNetworkAddress
|
|
3080
4263
|
};
|
|
3081
4264
|
}
|
|
4265
|
+
var SOLANA_CHAIN_PRESETS = {
|
|
4266
|
+
"solana-devnet": {
|
|
4267
|
+
name: "solana-devnet",
|
|
4268
|
+
chainType: "solana",
|
|
4269
|
+
rpcUrl: "http://localhost:19899",
|
|
4270
|
+
programId: "",
|
|
4271
|
+
// TBD: set after program deployment
|
|
4272
|
+
cluster: "devnet"
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
var MINA_CHAIN_PRESETS = {
|
|
4276
|
+
"mina-devnet": {
|
|
4277
|
+
name: "mina-devnet",
|
|
4278
|
+
chainType: "mina",
|
|
4279
|
+
graphqlUrl: "http://localhost:19085/graphql",
|
|
4280
|
+
zkAppAddress: "",
|
|
4281
|
+
// TBD: set after zkApp deployment
|
|
4282
|
+
network: "devnet"
|
|
4283
|
+
}
|
|
4284
|
+
};
|
|
4285
|
+
function resolveSolanaChainConfig(name) {
|
|
4286
|
+
const preset = SOLANA_CHAIN_PRESETS[name];
|
|
4287
|
+
if (!preset) {
|
|
4288
|
+
const validNames = Object.keys(SOLANA_CHAIN_PRESETS).join(", ");
|
|
4289
|
+
throw new ToonError(
|
|
4290
|
+
`Unknown Solana chain "${name}". Valid Solana chains: ${validNames}`,
|
|
4291
|
+
"INVALID_CHAIN"
|
|
4292
|
+
);
|
|
4293
|
+
}
|
|
4294
|
+
const resolved = { ...preset };
|
|
4295
|
+
const envRpcUrl = process.env["SOLANA_RPC_URL"];
|
|
4296
|
+
if (envRpcUrl) {
|
|
4297
|
+
resolved.rpcUrl = envRpcUrl;
|
|
4298
|
+
}
|
|
4299
|
+
const envProgramId = process.env["SOLANA_PROGRAM_ID"];
|
|
4300
|
+
if (envProgramId) {
|
|
4301
|
+
resolved.programId = envProgramId;
|
|
4302
|
+
}
|
|
4303
|
+
return resolved;
|
|
4304
|
+
}
|
|
4305
|
+
function resolveMinaChainConfig(name) {
|
|
4306
|
+
const preset = MINA_CHAIN_PRESETS[name];
|
|
4307
|
+
if (!preset) {
|
|
4308
|
+
const validNames = Object.keys(MINA_CHAIN_PRESETS).join(", ");
|
|
4309
|
+
throw new ToonError(
|
|
4310
|
+
`Unknown Mina chain "${name}". Valid Mina chains: ${validNames}`,
|
|
4311
|
+
"INVALID_CHAIN"
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
const resolved = { ...preset };
|
|
4315
|
+
const envGraphqlUrl = process.env["MINA_GRAPHQL_URL"];
|
|
4316
|
+
if (envGraphqlUrl) {
|
|
4317
|
+
resolved.graphqlUrl = envGraphqlUrl;
|
|
4318
|
+
}
|
|
4319
|
+
const envZkAppAddress = process.env["MINA_ZKAPP_ADDRESS"];
|
|
4320
|
+
if (envZkAppAddress) {
|
|
4321
|
+
resolved.zkAppAddress = envZkAppAddress;
|
|
4322
|
+
}
|
|
4323
|
+
return resolved;
|
|
4324
|
+
}
|
|
4325
|
+
function buildEvmProviderEntry(config, keyId) {
|
|
4326
|
+
return {
|
|
4327
|
+
chainType: "evm",
|
|
4328
|
+
chainId: `evm:${config.chainId}`,
|
|
4329
|
+
rpcUrl: config.rpcUrl,
|
|
4330
|
+
registryAddress: config.registryAddress,
|
|
4331
|
+
keyId
|
|
4332
|
+
};
|
|
4333
|
+
}
|
|
4334
|
+
function buildSolanaProviderEntry(config, keyId) {
|
|
4335
|
+
return {
|
|
4336
|
+
chainType: "solana",
|
|
4337
|
+
chainId: `solana:${config.cluster}`,
|
|
4338
|
+
rpcUrl: config.rpcUrl,
|
|
4339
|
+
programId: config.programId,
|
|
4340
|
+
keyId,
|
|
4341
|
+
cluster: config.cluster,
|
|
4342
|
+
...config.tokenMint && { tokenMint: config.tokenMint }
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
function buildMinaProviderEntry(config, keyId) {
|
|
4346
|
+
return {
|
|
4347
|
+
chainType: "mina",
|
|
4348
|
+
chainId: `mina:${config.network}`,
|
|
4349
|
+
graphqlUrl: config.graphqlUrl,
|
|
4350
|
+
zkAppAddress: config.zkAppAddress,
|
|
4351
|
+
...keyId && { keyId },
|
|
4352
|
+
...config.tokenId && { tokenId: config.tokenId },
|
|
4353
|
+
network: config.network
|
|
4354
|
+
};
|
|
4355
|
+
}
|
|
3082
4356
|
|
|
3083
4357
|
// src/x402/build-ilp-prepare.ts
|
|
3084
4358
|
function buildIlpPrepare(params) {
|
|
@@ -3165,7 +4439,7 @@ import { execFile as execFileCb } from "child_process";
|
|
|
3165
4439
|
import { readFile, mkdtemp, cp, writeFile, rm } from "fs/promises";
|
|
3166
4440
|
import { tmpdir } from "os";
|
|
3167
4441
|
import path from "path";
|
|
3168
|
-
import { createHash
|
|
4442
|
+
import { createHash } from "crypto";
|
|
3169
4443
|
function execFileAsync(cmd, args, options) {
|
|
3170
4444
|
return new Promise((resolve, reject) => {
|
|
3171
4445
|
execFileCb(cmd, args, options, (error, stdout, stderr) => {
|
|
@@ -3226,19 +4500,19 @@ var NixBuilder = class {
|
|
|
3226
4500
|
);
|
|
3227
4501
|
}
|
|
3228
4502
|
const imageData = await readFile(imagePath);
|
|
3229
|
-
const sha256 =
|
|
4503
|
+
const sha256 = createHash("sha256").update(imageData).digest("hex");
|
|
3230
4504
|
const imageHash = `sha256:${sha256}`;
|
|
3231
|
-
const pcr0 =
|
|
4505
|
+
const pcr0 = createHash("sha384").update(imageData).digest("hex");
|
|
3232
4506
|
const KERNEL_REGION_SIZE = 1024 * 1024;
|
|
3233
4507
|
const kernelRegion = imageData.subarray(
|
|
3234
4508
|
0,
|
|
3235
4509
|
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3236
4510
|
);
|
|
3237
|
-
const pcr1 =
|
|
4511
|
+
const pcr1 = createHash("sha384").update(kernelRegion).digest("hex");
|
|
3238
4512
|
const appRegion = imageData.subarray(
|
|
3239
4513
|
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3240
4514
|
);
|
|
3241
|
-
const pcr2 = appRegion.length > 0 ?
|
|
4515
|
+
const pcr2 = appRegion.length > 0 ? createHash("sha384").update(appRegion).digest("hex") : createHash("sha384").update("pcr2:").update(imageData).digest("hex");
|
|
3242
4516
|
return {
|
|
3243
4517
|
imageHash,
|
|
3244
4518
|
pcr0,
|
|
@@ -3429,29 +4703,43 @@ function createLogger(config) {
|
|
|
3429
4703
|
// src/index.ts
|
|
3430
4704
|
var VERSION = "0.1.0";
|
|
3431
4705
|
export {
|
|
4706
|
+
AddressRegistry,
|
|
3432
4707
|
ArDrivePeerRegistry,
|
|
3433
4708
|
AttestationBootstrap,
|
|
3434
4709
|
AttestationState,
|
|
3435
4710
|
AttestationVerifier,
|
|
4711
|
+
AttestedResultVerifier,
|
|
4712
|
+
BLOB_STORAGE_REQUEST_KIND,
|
|
4713
|
+
BLOB_STORAGE_RESULT_KIND,
|
|
3436
4714
|
BootstrapError,
|
|
3437
4715
|
BootstrapService,
|
|
3438
4716
|
CHAIN_PRESETS,
|
|
3439
4717
|
GenesisPeerLoader,
|
|
3440
4718
|
ILP_PEER_INFO_KIND,
|
|
4719
|
+
ILP_ROOT_PREFIX,
|
|
3441
4720
|
IMAGE_GENERATION_KIND,
|
|
3442
4721
|
InvalidEventError,
|
|
3443
4722
|
JOB_FEEDBACK_KIND,
|
|
3444
4723
|
JOB_REQUEST_KIND_BASE,
|
|
3445
4724
|
JOB_RESULT_KIND_BASE,
|
|
4725
|
+
JOB_REVIEW_KIND,
|
|
3446
4726
|
KmsIdentityError,
|
|
4727
|
+
MINA_CHAIN_PRESETS,
|
|
3447
4728
|
MOCK_USDC_ADDRESS,
|
|
3448
4729
|
MOCK_USDC_CONFIG,
|
|
3449
4730
|
NixBuilder,
|
|
3450
4731
|
NostrPeerDiscovery,
|
|
4732
|
+
PET_INTERACTION_EVENT_KIND,
|
|
4733
|
+
PET_INTERACTION_REQUEST_KIND,
|
|
4734
|
+
PET_INTERACTION_RESULT_KIND,
|
|
4735
|
+
PREFIX_CLAIM_KIND,
|
|
4736
|
+
PREFIX_GRANT_KIND,
|
|
3451
4737
|
PcrReproducibilityError,
|
|
3452
4738
|
PeerDiscoveryError,
|
|
4739
|
+
ReputationScoreCalculator,
|
|
3453
4740
|
SEED_RELAY_LIST_KIND,
|
|
3454
4741
|
SERVICE_DISCOVERY_KIND,
|
|
4742
|
+
SOLANA_CHAIN_PRESETS,
|
|
3455
4743
|
SeedRelayDiscovery,
|
|
3456
4744
|
SocialPeerDiscovery,
|
|
3457
4745
|
TEE_ATTESTATION_KIND,
|
|
@@ -3465,16 +4753,33 @@ export {
|
|
|
3465
4753
|
USDC_NAME,
|
|
3466
4754
|
USDC_SYMBOL,
|
|
3467
4755
|
VERSION,
|
|
4756
|
+
WEB_OF_TRUST_KIND,
|
|
4757
|
+
WORKFLOW_CHAIN_KIND,
|
|
3468
4758
|
analyzeDockerfileForNonDeterminism,
|
|
4759
|
+
assignAddressFromHandshake,
|
|
3469
4760
|
buildAttestationEvent,
|
|
4761
|
+
buildBlobStorageRequest,
|
|
3470
4762
|
buildEip712Domain,
|
|
4763
|
+
buildEvmProviderEntry,
|
|
3471
4764
|
buildIlpPeerInfoEvent,
|
|
3472
4765
|
buildIlpPrepare,
|
|
3473
4766
|
buildJobFeedbackEvent,
|
|
3474
4767
|
buildJobRequestEvent,
|
|
3475
4768
|
buildJobResultEvent,
|
|
4769
|
+
buildJobReviewEvent,
|
|
4770
|
+
buildMinaProviderEntry,
|
|
4771
|
+
buildPrefixClaimEvent,
|
|
4772
|
+
buildPrefixGrantEvent,
|
|
4773
|
+
buildPrefixHandshakeData,
|
|
3476
4774
|
buildSeedRelayListEvent,
|
|
3477
4775
|
buildServiceDiscoveryEvent,
|
|
4776
|
+
buildSolanaProviderEntry,
|
|
4777
|
+
buildSwarmRequestEvent,
|
|
4778
|
+
buildSwarmSelectionEvent,
|
|
4779
|
+
buildWorkflowDefinitionEvent,
|
|
4780
|
+
buildWotDeclarationEvent,
|
|
4781
|
+
calculateRouteAmount,
|
|
4782
|
+
checkAddressCollision,
|
|
3478
4783
|
createAgentRuntimeClient,
|
|
3479
4784
|
createDirectChannelClient,
|
|
3480
4785
|
createDirectConnectorAdmin,
|
|
@@ -3489,23 +4794,43 @@ export {
|
|
|
3489
4794
|
createLogger,
|
|
3490
4795
|
createToonNode,
|
|
3491
4796
|
decodeEventFromToon,
|
|
4797
|
+
deriveChildAddress,
|
|
3492
4798
|
deriveFromKmsSeed,
|
|
3493
4799
|
encodeEventToToon,
|
|
3494
4800
|
encodeEventToToonString,
|
|
4801
|
+
extractPrefixFromHandshake,
|
|
4802
|
+
hasMinReputation,
|
|
4803
|
+
hasRequireAttestation,
|
|
4804
|
+
isGenesisNode,
|
|
4805
|
+
isValidIlpAddressStructure,
|
|
3495
4806
|
negotiateSettlementChain,
|
|
3496
4807
|
parseAttestation,
|
|
4808
|
+
parseBlobStorageRequest,
|
|
3497
4809
|
parseIlpPeerInfo,
|
|
3498
4810
|
parseJobFeedback,
|
|
3499
4811
|
parseJobRequest,
|
|
3500
4812
|
parseJobResult,
|
|
4813
|
+
parseJobReview,
|
|
4814
|
+
parsePrefixClaimEvent,
|
|
4815
|
+
parsePrefixGrantEvent,
|
|
3501
4816
|
parseSeedRelayList,
|
|
3502
4817
|
parseServiceDiscovery,
|
|
4818
|
+
parseSwarmRequest,
|
|
4819
|
+
parseSwarmSelection,
|
|
4820
|
+
parseWorkflowDefinition,
|
|
4821
|
+
parseWotDeclaration,
|
|
3503
4822
|
publishSeedRelayEntry,
|
|
3504
4823
|
readDockerfileNix,
|
|
3505
4824
|
resolveChainConfig,
|
|
4825
|
+
resolveMinaChainConfig,
|
|
4826
|
+
resolveRouteFees,
|
|
4827
|
+
resolveSolanaChainConfig,
|
|
3506
4828
|
resolveTokenForChain,
|
|
3507
4829
|
shallowParseToon,
|
|
3508
4830
|
validateChainId,
|
|
4831
|
+
validateIlpAddress,
|
|
4832
|
+
validatePrefix,
|
|
4833
|
+
validatePrefixConsistency,
|
|
3509
4834
|
verifyPcrReproducibility
|
|
3510
4835
|
};
|
|
3511
4836
|
//# sourceMappingURL=index.js.map
|