@toon-protocol/core 1.1.2 → 1.3.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/{chunk-7AHDGE4H.js → chunk-5WT7ISKC.js} +7 -3
- package/dist/chunk-5WT7ISKC.js.map +1 -0
- package/dist/index.d.ts +1316 -176
- package/dist/index.js +1514 -155
- package/dist/index.js.map +1 -1
- package/dist/toon/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-7AHDGE4H.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
encodeEventToToon,
|
|
9
9
|
encodeEventToToonString,
|
|
10
10
|
shallowParseToon
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-5WT7ISKC.js";
|
|
12
12
|
|
|
13
13
|
// src/constants.ts
|
|
14
14
|
var ILP_PEER_INFO_KIND = 10032;
|
|
@@ -22,6 +22,257 @@ 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 ILP_ROOT_PREFIX = "g.toon";
|
|
33
|
+
|
|
34
|
+
// src/address/ilp-address-validation.ts
|
|
35
|
+
var ILP_SEGMENT_PATTERN = /^[a-z0-9-]+$/;
|
|
36
|
+
var MAX_ILP_ADDRESS_LENGTH = 1023;
|
|
37
|
+
function isValidIlpAddressStructure(address) {
|
|
38
|
+
if (!address) return false;
|
|
39
|
+
if (address.length > MAX_ILP_ADDRESS_LENGTH) return false;
|
|
40
|
+
const segments = address.split(".");
|
|
41
|
+
for (const segment of segments) {
|
|
42
|
+
if (segment.length === 0) return false;
|
|
43
|
+
if (!ILP_SEGMENT_PATTERN.test(segment)) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function validateIlpAddress(address) {
|
|
48
|
+
if (address.length > MAX_ILP_ADDRESS_LENGTH) {
|
|
49
|
+
throw new ToonError(
|
|
50
|
+
`Invalid ILP address: exceeds maximum length of ${MAX_ILP_ADDRESS_LENGTH}`,
|
|
51
|
+
"ADDRESS_INVALID_PREFIX"
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const segments = address.split(".");
|
|
55
|
+
for (const segment of segments) {
|
|
56
|
+
if (segment.length === 0) {
|
|
57
|
+
throw new ToonError(
|
|
58
|
+
`Invalid ILP address: empty segment in "${address}"`,
|
|
59
|
+
"ADDRESS_INVALID_PREFIX"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (!ILP_SEGMENT_PATTERN.test(segment)) {
|
|
63
|
+
throw new ToonError(
|
|
64
|
+
`Invalid ILP address: segment "${segment}" contains invalid characters`,
|
|
65
|
+
"ADDRESS_INVALID_PREFIX"
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/address/derive-child-address.ts
|
|
72
|
+
var HEX_PATTERN = /^[0-9a-fA-F]+$/;
|
|
73
|
+
var PUBKEY_TRUNCATION_LENGTH = 8;
|
|
74
|
+
var MAX_PUBKEY_LENGTH = 128;
|
|
75
|
+
var MAX_ILP_ADDRESS_LENGTH2 = 1023;
|
|
76
|
+
function deriveChildAddress(parentPrefix, childPubkey) {
|
|
77
|
+
if (!parentPrefix) {
|
|
78
|
+
throw new ToonError(
|
|
79
|
+
"Parent prefix must not be empty",
|
|
80
|
+
"ADDRESS_INVALID_PREFIX"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
validateIlpAddress(parentPrefix);
|
|
84
|
+
if (childPubkey.length < PUBKEY_TRUNCATION_LENGTH) {
|
|
85
|
+
throw new ToonError(
|
|
86
|
+
`Invalid pubkey: must be at least ${PUBKEY_TRUNCATION_LENGTH} hex characters, got ${childPubkey.length}`,
|
|
87
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (childPubkey.length > MAX_PUBKEY_LENGTH) {
|
|
91
|
+
throw new ToonError(
|
|
92
|
+
`Invalid pubkey: exceeds maximum length of ${MAX_PUBKEY_LENGTH} characters`,
|
|
93
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (!HEX_PATTERN.test(childPubkey)) {
|
|
97
|
+
throw new ToonError(
|
|
98
|
+
`Invalid pubkey: contains non-hex characters`,
|
|
99
|
+
"ADDRESS_INVALID_PUBKEY"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const childSegment = childPubkey.slice(0, PUBKEY_TRUNCATION_LENGTH).toLowerCase();
|
|
103
|
+
const derivedAddress = `${parentPrefix}.${childSegment}`;
|
|
104
|
+
if (derivedAddress.length > MAX_ILP_ADDRESS_LENGTH2) {
|
|
105
|
+
throw new ToonError(
|
|
106
|
+
`Derived ILP address exceeds maximum length of ${MAX_ILP_ADDRESS_LENGTH2}`,
|
|
107
|
+
"ADDRESS_TOO_LONG"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return derivedAddress;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/address/btp-prefix-exchange.ts
|
|
114
|
+
var MAX_PREFIX_LENGTH = 1023;
|
|
115
|
+
function extractPrefixFromHandshake(handshakeData) {
|
|
116
|
+
const prefix = handshakeData["prefix"];
|
|
117
|
+
if (prefix === void 0 || prefix === null || prefix === "") {
|
|
118
|
+
throw new ToonError(
|
|
119
|
+
"BTP handshake response missing required prefix field",
|
|
120
|
+
"ADDRESS_MISSING_PREFIX"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (typeof prefix !== "string") {
|
|
124
|
+
throw new ToonError(
|
|
125
|
+
"BTP handshake response missing required prefix field",
|
|
126
|
+
"ADDRESS_MISSING_PREFIX"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (prefix.length > MAX_PREFIX_LENGTH) {
|
|
130
|
+
throw new ToonError(
|
|
131
|
+
`BTP handshake prefix exceeds maximum length of ${MAX_PREFIX_LENGTH}`,
|
|
132
|
+
"ADDRESS_INVALID_PREFIX"
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
if (!isValidIlpAddressStructure(prefix)) {
|
|
136
|
+
throw new ToonError(
|
|
137
|
+
`BTP handshake prefix is not a valid ILP address: "${prefix}"`,
|
|
138
|
+
"ADDRESS_INVALID_PREFIX"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return prefix;
|
|
142
|
+
}
|
|
143
|
+
function buildPrefixHandshakeData(ownIlpAddress) {
|
|
144
|
+
if (ownIlpAddress.length > MAX_PREFIX_LENGTH) {
|
|
145
|
+
throw new ToonError(
|
|
146
|
+
`Cannot build handshake data: address exceeds maximum length of ${MAX_PREFIX_LENGTH}`,
|
|
147
|
+
"ADDRESS_INVALID_PREFIX"
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!isValidIlpAddressStructure(ownIlpAddress)) {
|
|
151
|
+
throw new ToonError(
|
|
152
|
+
`Cannot build handshake data: "${ownIlpAddress}" is not a valid ILP address`,
|
|
153
|
+
"ADDRESS_INVALID_PREFIX"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return { prefix: ownIlpAddress };
|
|
157
|
+
}
|
|
158
|
+
function validatePrefixConsistency(handshakePrefix, advertisedPrefix) {
|
|
159
|
+
if (advertisedPrefix === void 0) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (handshakePrefix !== advertisedPrefix) {
|
|
163
|
+
throw new ToonError(
|
|
164
|
+
`BTP handshake prefix "${handshakePrefix}" does not match upstream kind:10032 advertised address "${advertisedPrefix}"`,
|
|
165
|
+
"ADDRESS_PREFIX_MISMATCH"
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function checkAddressCollision(derivedAddress, knownPeerAddresses) {
|
|
170
|
+
if (knownPeerAddresses.includes(derivedAddress)) {
|
|
171
|
+
throw new ToonError(
|
|
172
|
+
`Derived ILP address "${derivedAddress}" collides with an existing peer address`,
|
|
173
|
+
"ADDRESS_COLLISION"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/address/address-assignment.ts
|
|
179
|
+
function assignAddressFromHandshake(handshakeData, ownPubkey) {
|
|
180
|
+
const prefix = extractPrefixFromHandshake(handshakeData);
|
|
181
|
+
return deriveChildAddress(prefix, ownPubkey);
|
|
182
|
+
}
|
|
183
|
+
function isGenesisNode(config) {
|
|
184
|
+
return config.ilpAddress === ILP_ROOT_PREFIX;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/address/address-registry.ts
|
|
188
|
+
var AddressRegistry = class {
|
|
189
|
+
prefixToAddress = /* @__PURE__ */ new Map();
|
|
190
|
+
/**
|
|
191
|
+
* Registers a new upstream prefix -> derived address mapping.
|
|
192
|
+
*
|
|
193
|
+
* @param upstreamPrefix - The upstream peer's ILP address prefix
|
|
194
|
+
* @param derivedAddress - The derived ILP address for this node under that prefix
|
|
195
|
+
*/
|
|
196
|
+
addAddress(upstreamPrefix, derivedAddress) {
|
|
197
|
+
this.prefixToAddress.set(upstreamPrefix, derivedAddress);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Removes the mapping for the given upstream prefix.
|
|
201
|
+
*
|
|
202
|
+
* @param upstreamPrefix - The upstream prefix to remove
|
|
203
|
+
* @returns The removed derived address, or `undefined` if the prefix was not found
|
|
204
|
+
*/
|
|
205
|
+
removeAddress(upstreamPrefix) {
|
|
206
|
+
const address = this.prefixToAddress.get(upstreamPrefix);
|
|
207
|
+
if (address !== void 0) {
|
|
208
|
+
this.prefixToAddress.delete(upstreamPrefix);
|
|
209
|
+
}
|
|
210
|
+
return address;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Returns all derived addresses in insertion order.
|
|
214
|
+
*/
|
|
215
|
+
getAddresses() {
|
|
216
|
+
return [...this.prefixToAddress.values()];
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns `true` if the given upstream prefix is registered.
|
|
220
|
+
*/
|
|
221
|
+
hasPrefix(upstreamPrefix) {
|
|
222
|
+
return this.prefixToAddress.has(upstreamPrefix);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Returns the number of registered addresses.
|
|
226
|
+
*/
|
|
227
|
+
get size() {
|
|
228
|
+
return this.prefixToAddress.size;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Returns the primary (first inserted) address.
|
|
232
|
+
*
|
|
233
|
+
* @returns The first address, or `undefined` if the registry is empty
|
|
234
|
+
*/
|
|
235
|
+
getPrimaryAddress() {
|
|
236
|
+
const first = this.prefixToAddress.values().next();
|
|
237
|
+
return first.done ? void 0 : first.value;
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// src/address/prefix-validation.ts
|
|
242
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set(["toon", "ilp", "local", "peer", "test"]);
|
|
243
|
+
var PREFIX_PATTERN = /^[a-z0-9]+$/;
|
|
244
|
+
var MIN_LENGTH = 2;
|
|
245
|
+
var MAX_LENGTH = 16;
|
|
246
|
+
function validatePrefix(prefix) {
|
|
247
|
+
if (typeof prefix !== "string" || prefix.length === 0) {
|
|
248
|
+
return { valid: false, reason: "Prefix must be a non-empty string" };
|
|
249
|
+
}
|
|
250
|
+
if (prefix.length < MIN_LENGTH) {
|
|
251
|
+
return {
|
|
252
|
+
valid: false,
|
|
253
|
+
reason: `Prefix must have a minimum of ${MIN_LENGTH} characters, got ${prefix.length}`
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (prefix.length > MAX_LENGTH) {
|
|
257
|
+
return {
|
|
258
|
+
valid: false,
|
|
259
|
+
reason: `Prefix must have a maximum of ${MAX_LENGTH} characters, got ${prefix.length}`
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (!PREFIX_PATTERN.test(prefix)) {
|
|
263
|
+
return {
|
|
264
|
+
valid: false,
|
|
265
|
+
reason: "Prefix must contain only lowercase alphanumeric characters [a-z0-9]"
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (RESERVED_WORDS.has(prefix)) {
|
|
269
|
+
return {
|
|
270
|
+
valid: false,
|
|
271
|
+
reason: `Prefix "${prefix}" is a reserved word`
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return { valid: true };
|
|
275
|
+
}
|
|
25
276
|
|
|
26
277
|
// src/events/parsers.ts
|
|
27
278
|
function validateChainId(chainId) {
|
|
@@ -57,7 +308,8 @@ function parseIlpPeerInfo(event) {
|
|
|
57
308
|
blsHttpEndpoint,
|
|
58
309
|
settlementEngine,
|
|
59
310
|
assetCode,
|
|
60
|
-
assetScale
|
|
311
|
+
assetScale,
|
|
312
|
+
ilpAddresses: rawIlpAddresses
|
|
61
313
|
} = parsed;
|
|
62
314
|
if (typeof ilpAddress !== "string" || ilpAddress.length === 0) {
|
|
63
315
|
throw new InvalidEventError(
|
|
@@ -142,6 +394,52 @@ function parseIlpPeerInfo(event) {
|
|
|
142
394
|
throw new InvalidEventError("tokenNetworks must be an object");
|
|
143
395
|
}
|
|
144
396
|
}
|
|
397
|
+
const { feePerByte: rawFeePerByte } = parsed;
|
|
398
|
+
let feePerByte;
|
|
399
|
+
if (rawFeePerByte === void 0) {
|
|
400
|
+
feePerByte = "0";
|
|
401
|
+
} else if (typeof rawFeePerByte !== "string" || !/^\d+$/.test(rawFeePerByte)) {
|
|
402
|
+
throw new InvalidEventError(
|
|
403
|
+
`Invalid feePerByte: "${String(rawFeePerByte)}" must be a non-negative integer string`
|
|
404
|
+
);
|
|
405
|
+
} else {
|
|
406
|
+
feePerByte = rawFeePerByte;
|
|
407
|
+
}
|
|
408
|
+
const { prefixPricing: rawPrefixPricing } = parsed;
|
|
409
|
+
let prefixPricing;
|
|
410
|
+
if (rawPrefixPricing !== void 0) {
|
|
411
|
+
if (!isObject(rawPrefixPricing)) {
|
|
412
|
+
throw new InvalidEventError("prefixPricing must be an object");
|
|
413
|
+
}
|
|
414
|
+
const { basePrice } = rawPrefixPricing;
|
|
415
|
+
if (typeof basePrice !== "string" || !/^\d+$/.test(basePrice)) {
|
|
416
|
+
throw new InvalidEventError(
|
|
417
|
+
`Invalid prefixPricing.basePrice: "${String(basePrice)}" must be a non-negative integer string`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
prefixPricing = { basePrice };
|
|
421
|
+
}
|
|
422
|
+
let ilpAddresses;
|
|
423
|
+
if (rawIlpAddresses !== void 0) {
|
|
424
|
+
if (!Array.isArray(rawIlpAddresses)) {
|
|
425
|
+
throw new InvalidEventError("ilpAddresses must be an array");
|
|
426
|
+
}
|
|
427
|
+
for (const addr of rawIlpAddresses) {
|
|
428
|
+
if (typeof addr !== "string" || addr.length === 0) {
|
|
429
|
+
throw new InvalidEventError(
|
|
430
|
+
"ilpAddresses elements must be non-empty strings"
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (!isValidIlpAddressStructure(addr)) {
|
|
434
|
+
throw new InvalidEventError(
|
|
435
|
+
`Invalid ILP address in ilpAddresses: "${addr}"`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
ilpAddresses = rawIlpAddresses;
|
|
440
|
+
} else {
|
|
441
|
+
ilpAddresses = [ilpAddress];
|
|
442
|
+
}
|
|
145
443
|
return {
|
|
146
444
|
ilpAddress,
|
|
147
445
|
btpEndpoint,
|
|
@@ -156,17 +454,51 @@ function parseIlpPeerInfo(event) {
|
|
|
156
454
|
},
|
|
157
455
|
...tokenNetworks !== void 0 && {
|
|
158
456
|
tokenNetworks
|
|
159
|
-
}
|
|
457
|
+
},
|
|
458
|
+
ilpAddresses,
|
|
459
|
+
feePerByte,
|
|
460
|
+
...prefixPricing !== void 0 && { prefixPricing }
|
|
160
461
|
};
|
|
161
462
|
}
|
|
162
463
|
|
|
163
464
|
// src/events/builders.ts
|
|
164
465
|
import { finalizeEvent } from "nostr-tools/pure";
|
|
165
466
|
function buildIlpPeerInfoEvent(info, secretKey) {
|
|
467
|
+
if (info.feePerByte !== void 0) {
|
|
468
|
+
if (typeof info.feePerByte !== "string" || !/^\d+$/.test(info.feePerByte)) {
|
|
469
|
+
throw new ToonError(
|
|
470
|
+
`Invalid feePerByte: "${String(info.feePerByte)}" must be a non-negative integer string`,
|
|
471
|
+
"INVALID_FEE"
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
let effectiveInfo = info;
|
|
476
|
+
if (info.ilpAddresses !== void 0) {
|
|
477
|
+
const addresses = info.ilpAddresses;
|
|
478
|
+
if (addresses.length === 0) {
|
|
479
|
+
throw new ToonError(
|
|
480
|
+
"ilpAddresses must be a non-empty array: a node must have at least one address",
|
|
481
|
+
"ADDRESS_EMPTY_ADDRESSES"
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
for (const addr of addresses) {
|
|
485
|
+
if (!isValidIlpAddressStructure(addr)) {
|
|
486
|
+
throw new ToonError(
|
|
487
|
+
`Invalid ILP address in ilpAddresses: "${addr}"`,
|
|
488
|
+
"ADDRESS_INVALID_PREFIX"
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
const primaryAddress = addresses[0];
|
|
493
|
+
effectiveInfo = {
|
|
494
|
+
...info,
|
|
495
|
+
ilpAddress: primaryAddress
|
|
496
|
+
};
|
|
497
|
+
}
|
|
166
498
|
return finalizeEvent(
|
|
167
499
|
{
|
|
168
500
|
kind: ILP_PEER_INFO_KIND,
|
|
169
|
-
content: JSON.stringify(
|
|
501
|
+
content: JSON.stringify(effectiveInfo),
|
|
170
502
|
tags: [],
|
|
171
503
|
created_at: Math.floor(Date.now() / 1e3)
|
|
172
504
|
},
|
|
@@ -376,6 +708,32 @@ function parseServiceDiscovery(event) {
|
|
|
376
708
|
return null;
|
|
377
709
|
skillResult.attestation = attestation;
|
|
378
710
|
}
|
|
711
|
+
const reputation = skillRecord["reputation"];
|
|
712
|
+
if (reputation !== void 0) {
|
|
713
|
+
if (typeof reputation !== "object" || reputation === null || Array.isArray(reputation))
|
|
714
|
+
return null;
|
|
715
|
+
const repRecord = reputation;
|
|
716
|
+
const repScore = repRecord["score"];
|
|
717
|
+
if (typeof repScore !== "number" || !isFinite(repScore)) return null;
|
|
718
|
+
const signals = repRecord["signals"];
|
|
719
|
+
if (typeof signals !== "object" || signals === null || Array.isArray(signals))
|
|
720
|
+
return null;
|
|
721
|
+
const sigRecord = signals;
|
|
722
|
+
const trustedBy = sigRecord["trustedBy"];
|
|
723
|
+
if (typeof trustedBy !== "number" || !isFinite(trustedBy)) return null;
|
|
724
|
+
const channelVolumeUsdc = sigRecord["channelVolumeUsdc"];
|
|
725
|
+
if (typeof channelVolumeUsdc !== "number" || !isFinite(channelVolumeUsdc))
|
|
726
|
+
return null;
|
|
727
|
+
const jobsCompleted = sigRecord["jobsCompleted"];
|
|
728
|
+
if (typeof jobsCompleted !== "number" || !isFinite(jobsCompleted))
|
|
729
|
+
return null;
|
|
730
|
+
const avgRating = sigRecord["avgRating"];
|
|
731
|
+
if (typeof avgRating !== "number" || !isFinite(avgRating)) return null;
|
|
732
|
+
skillResult.reputation = {
|
|
733
|
+
score: repScore,
|
|
734
|
+
signals: { trustedBy, channelVolumeUsdc, jobsCompleted, avgRating }
|
|
735
|
+
};
|
|
736
|
+
}
|
|
379
737
|
result.skill = skillResult;
|
|
380
738
|
}
|
|
381
739
|
return result;
|
|
@@ -577,11 +935,22 @@ function buildJobResultEvent(params, secretKey) {
|
|
|
577
935
|
"DVM_MISSING_CONTENT"
|
|
578
936
|
);
|
|
579
937
|
}
|
|
938
|
+
if (params.attestationEventId !== void 0) {
|
|
939
|
+
if (!HEX_64_REGEX.test(params.attestationEventId)) {
|
|
940
|
+
throw new ToonError(
|
|
941
|
+
"Job result attestationEventId must be a 64-character lowercase hex string",
|
|
942
|
+
"DVM_INVALID_ATTESTATION_EVENT_ID"
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
580
946
|
const tags = [
|
|
581
947
|
["e", params.requestEventId],
|
|
582
948
|
["p", params.customerPubkey],
|
|
583
949
|
["amount", params.amount, "usdc"]
|
|
584
950
|
];
|
|
951
|
+
if (params.attestationEventId !== void 0) {
|
|
952
|
+
tags.push(["attestation", params.attestationEventId]);
|
|
953
|
+
}
|
|
585
954
|
return finalizeEvent5(
|
|
586
955
|
{
|
|
587
956
|
kind: params.kind,
|
|
@@ -706,46 +1075,839 @@ function parseJobResult(event) {
|
|
|
706
1075
|
const customerPubkey = pTag[1];
|
|
707
1076
|
if (customerPubkey === void 0 || !HEX_64_REGEX.test(customerPubkey))
|
|
708
1077
|
return null;
|
|
709
|
-
const amountTag = event.tags.find((t) => t[0] === "amount");
|
|
710
|
-
if (!amountTag) return null;
|
|
711
|
-
const amount = amountTag[1];
|
|
712
|
-
if (amount === void 0 || amount === "") return null;
|
|
1078
|
+
const amountTag = event.tags.find((t) => t[0] === "amount");
|
|
1079
|
+
if (!amountTag) return null;
|
|
1080
|
+
const amount = amountTag[1];
|
|
1081
|
+
if (amount === void 0 || amount === "") return null;
|
|
1082
|
+
if (!/^\d+$/.test(amount)) return null;
|
|
1083
|
+
const attestationTag = event.tags.find(
|
|
1084
|
+
(t) => t[0] === "attestation"
|
|
1085
|
+
);
|
|
1086
|
+
const attestationEventId = attestationTag?.[1];
|
|
1087
|
+
const result = {
|
|
1088
|
+
kind: event.kind,
|
|
1089
|
+
requestEventId,
|
|
1090
|
+
customerPubkey,
|
|
1091
|
+
amount,
|
|
1092
|
+
content: event.content
|
|
1093
|
+
};
|
|
1094
|
+
if (attestationEventId !== void 0 && HEX_64_REGEX.test(attestationEventId)) {
|
|
1095
|
+
result.attestationEventId = attestationEventId;
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
function parseJobFeedback(event) {
|
|
1100
|
+
if (event.kind !== JOB_FEEDBACK_KIND) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
1104
|
+
if (!eTag) return null;
|
|
1105
|
+
const requestEventId = eTag[1];
|
|
1106
|
+
if (requestEventId === void 0 || !HEX_64_REGEX.test(requestEventId))
|
|
1107
|
+
return null;
|
|
1108
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
1109
|
+
if (!pTag) return null;
|
|
1110
|
+
const customerPubkey = pTag[1];
|
|
1111
|
+
if (customerPubkey === void 0 || !HEX_64_REGEX.test(customerPubkey))
|
|
1112
|
+
return null;
|
|
1113
|
+
const statusTag = event.tags.find((t) => t[0] === "status");
|
|
1114
|
+
if (!statusTag) return null;
|
|
1115
|
+
const statusValue = statusTag[1];
|
|
1116
|
+
if (statusValue === void 0) return null;
|
|
1117
|
+
if (!VALID_STATUSES.has(statusValue)) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
return {
|
|
1121
|
+
requestEventId,
|
|
1122
|
+
customerPubkey,
|
|
1123
|
+
status: statusValue,
|
|
1124
|
+
content: event.content
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/events/workflow.ts
|
|
1129
|
+
import { finalizeEvent as finalizeEvent6 } from "nostr-tools/pure";
|
|
1130
|
+
var HEX_64_REGEX2 = /^[0-9a-f]{64}$/;
|
|
1131
|
+
function buildWorkflowDefinitionEvent(params, secretKey) {
|
|
1132
|
+
if (!params.steps || params.steps.length === 0) {
|
|
1133
|
+
throw new ToonError(
|
|
1134
|
+
"Workflow definition must have at least one step",
|
|
1135
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
for (const step of params.steps) {
|
|
1139
|
+
if (step.kind < 5e3 || step.kind > 5999) {
|
|
1140
|
+
throw new ToonError(
|
|
1141
|
+
`Workflow step kind must be in range 5000-5999, got ${step.kind}`,
|
|
1142
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
if (typeof step.description !== "string" || step.description === "") {
|
|
1146
|
+
throw new ToonError(
|
|
1147
|
+
"Workflow step description must be a non-empty string",
|
|
1148
|
+
"DVM_WORKFLOW_INVALID_STEPS"
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
if (step.targetProvider !== void 0 && !HEX_64_REGEX2.test(step.targetProvider)) {
|
|
1152
|
+
throw new ToonError(
|
|
1153
|
+
"Workflow step targetProvider must be a 64-character lowercase hex string",
|
|
1154
|
+
"DVM_WORKFLOW_INVALID_PROVIDER"
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof params.initialInput.data !== "string") {
|
|
1159
|
+
throw new ToonError(
|
|
1160
|
+
"Workflow definition initial input data must be a string",
|
|
1161
|
+
"DVM_WORKFLOW_MISSING_INPUT"
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
if (!params.initialInput.type) {
|
|
1165
|
+
throw new ToonError(
|
|
1166
|
+
"Workflow definition initial input type is required",
|
|
1167
|
+
"DVM_WORKFLOW_MISSING_INPUT"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof params.totalBid !== "string" || params.totalBid === "") {
|
|
1171
|
+
throw new ToonError(
|
|
1172
|
+
"Workflow definition totalBid must be a non-empty string (USDC micro-units)",
|
|
1173
|
+
"DVM_WORKFLOW_INVALID_BID"
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
const stepsWithAllocation = params.steps.filter(
|
|
1177
|
+
(s) => s.bidAllocation !== void 0
|
|
1178
|
+
);
|
|
1179
|
+
if (stepsWithAllocation.length > 0) {
|
|
1180
|
+
let allocationSum;
|
|
1181
|
+
let totalBidBigInt;
|
|
1182
|
+
try {
|
|
1183
|
+
allocationSum = 0n;
|
|
1184
|
+
for (const step of stepsWithAllocation) {
|
|
1185
|
+
allocationSum += BigInt(step.bidAllocation);
|
|
1186
|
+
}
|
|
1187
|
+
totalBidBigInt = BigInt(params.totalBid);
|
|
1188
|
+
} catch {
|
|
1189
|
+
throw new ToonError(
|
|
1190
|
+
"Workflow bid amounts must be valid numeric strings (USDC micro-units)",
|
|
1191
|
+
"DVM_WORKFLOW_INVALID_BID"
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (allocationSum > totalBidBigInt) {
|
|
1195
|
+
throw new ToonError(
|
|
1196
|
+
`Sum of step bid allocations (${allocationSum}) exceeds total bid (${totalBidBigInt})`,
|
|
1197
|
+
"DVM_WORKFLOW_BID_OVERFLOW"
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const contentBody = {
|
|
1202
|
+
steps: params.steps.map((step) => {
|
|
1203
|
+
const s = {
|
|
1204
|
+
kind: step.kind,
|
|
1205
|
+
description: step.description
|
|
1206
|
+
};
|
|
1207
|
+
if (step.targetProvider !== void 0) {
|
|
1208
|
+
s["targetProvider"] = step.targetProvider;
|
|
1209
|
+
}
|
|
1210
|
+
if (step.bidAllocation !== void 0) {
|
|
1211
|
+
s["bidAllocation"] = step.bidAllocation;
|
|
1212
|
+
}
|
|
1213
|
+
return s;
|
|
1214
|
+
}),
|
|
1215
|
+
initialInput: params.initialInput,
|
|
1216
|
+
totalBid: params.totalBid
|
|
1217
|
+
};
|
|
1218
|
+
const contentJson = JSON.stringify(contentBody);
|
|
1219
|
+
const tags = [];
|
|
1220
|
+
const dTag = params.workflowId ?? `wf-${Date.now()}-${params.steps.length}`;
|
|
1221
|
+
tags.push(["d", dTag]);
|
|
1222
|
+
tags.push(["bid", params.totalBid, "usdc"]);
|
|
1223
|
+
return finalizeEvent6(
|
|
1224
|
+
{
|
|
1225
|
+
kind: WORKFLOW_CHAIN_KIND,
|
|
1226
|
+
content: contentJson,
|
|
1227
|
+
tags,
|
|
1228
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1229
|
+
},
|
|
1230
|
+
secretKey
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
function parseWorkflowDefinition(event) {
|
|
1234
|
+
if (event.kind !== WORKFLOW_CHAIN_KIND) {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
let body;
|
|
1238
|
+
try {
|
|
1239
|
+
body = JSON.parse(event.content);
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
if (typeof body !== "object" || body === null) {
|
|
1244
|
+
return null;
|
|
1245
|
+
}
|
|
1246
|
+
const obj = body;
|
|
1247
|
+
const rawSteps = obj["steps"];
|
|
1248
|
+
if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
const steps = [];
|
|
1252
|
+
for (const rawStep of rawSteps) {
|
|
1253
|
+
if (typeof rawStep !== "object" || rawStep === null) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
const stepObj = rawStep;
|
|
1257
|
+
const kind = stepObj["kind"];
|
|
1258
|
+
const description = stepObj["description"];
|
|
1259
|
+
if (typeof kind !== "number" || typeof description !== "string") {
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
if (kind < 5e3 || kind > 5999) {
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
const step = { kind, description };
|
|
1266
|
+
const targetProvider = stepObj["targetProvider"];
|
|
1267
|
+
if (typeof targetProvider === "string" && targetProvider.length > 0) {
|
|
1268
|
+
step.targetProvider = targetProvider;
|
|
1269
|
+
}
|
|
1270
|
+
const bidAllocation = stepObj["bidAllocation"];
|
|
1271
|
+
if (typeof bidAllocation === "string" && bidAllocation.length > 0) {
|
|
1272
|
+
step.bidAllocation = bidAllocation;
|
|
1273
|
+
}
|
|
1274
|
+
steps.push(step);
|
|
1275
|
+
}
|
|
1276
|
+
const rawInput = obj["initialInput"];
|
|
1277
|
+
if (typeof rawInput !== "object" || rawInput === null) {
|
|
1278
|
+
return null;
|
|
1279
|
+
}
|
|
1280
|
+
const inputObj = rawInput;
|
|
1281
|
+
const inputData = inputObj["data"];
|
|
1282
|
+
const inputType = inputObj["type"];
|
|
1283
|
+
if (typeof inputData !== "string" || typeof inputType !== "string") {
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1286
|
+
const totalBid = obj["totalBid"];
|
|
1287
|
+
if (typeof totalBid !== "string" || totalBid === "") {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
steps,
|
|
1292
|
+
initialInput: { data: inputData, type: inputType },
|
|
1293
|
+
totalBid,
|
|
1294
|
+
content: event.content
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// src/events/swarm.ts
|
|
1299
|
+
import { finalizeEvent as finalizeEvent7 } from "nostr-tools/pure";
|
|
1300
|
+
var HEX_64_REGEX3 = /^[0-9a-f]{64}$/;
|
|
1301
|
+
function buildSwarmRequestEvent(params, secretKey) {
|
|
1302
|
+
if (!Number.isInteger(params.maxProviders) || params.maxProviders < 1) {
|
|
1303
|
+
throw new ToonError(
|
|
1304
|
+
`Swarm maxProviders must be a positive integer >= 1, got ${params.maxProviders}`,
|
|
1305
|
+
"DVM_SWARM_INVALID_MAX_PROVIDERS"
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
const baseEvent = buildJobRequestEvent(params, secretKey);
|
|
1309
|
+
const judge = params.judge ?? "customer";
|
|
1310
|
+
const tags = [
|
|
1311
|
+
...baseEvent.tags,
|
|
1312
|
+
["swarm", params.maxProviders.toString()],
|
|
1313
|
+
["judge", judge]
|
|
1314
|
+
];
|
|
1315
|
+
return finalizeEvent7(
|
|
1316
|
+
{
|
|
1317
|
+
kind: baseEvent.kind,
|
|
1318
|
+
content: baseEvent.content,
|
|
1319
|
+
tags,
|
|
1320
|
+
created_at: baseEvent.created_at
|
|
1321
|
+
},
|
|
1322
|
+
secretKey
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
function buildSwarmSelectionEvent(params, secretKey) {
|
|
1326
|
+
if (!HEX_64_REGEX3.test(params.swarmRequestEventId)) {
|
|
1327
|
+
throw new ToonError(
|
|
1328
|
+
"Swarm selection swarmRequestEventId must be a 64-character lowercase hex string",
|
|
1329
|
+
"DVM_INVALID_EVENT_ID"
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
if (!HEX_64_REGEX3.test(params.winnerResultEventId)) {
|
|
1333
|
+
throw new ToonError(
|
|
1334
|
+
"Swarm selection winnerResultEventId must be a 64-character lowercase hex string",
|
|
1335
|
+
"DVM_INVALID_EVENT_ID"
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
if (!HEX_64_REGEX3.test(params.customerPubkey)) {
|
|
1339
|
+
throw new ToonError(
|
|
1340
|
+
"Swarm selection customerPubkey must be a 64-character lowercase hex string",
|
|
1341
|
+
"DVM_INVALID_PUBKEY"
|
|
1342
|
+
);
|
|
1343
|
+
}
|
|
1344
|
+
const tags = [
|
|
1345
|
+
["e", params.swarmRequestEventId],
|
|
1346
|
+
["p", params.customerPubkey],
|
|
1347
|
+
["status", "success"],
|
|
1348
|
+
["winner", params.winnerResultEventId]
|
|
1349
|
+
];
|
|
1350
|
+
return finalizeEvent7(
|
|
1351
|
+
{
|
|
1352
|
+
kind: JOB_FEEDBACK_KIND,
|
|
1353
|
+
content: "",
|
|
1354
|
+
tags,
|
|
1355
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1356
|
+
},
|
|
1357
|
+
secretKey
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
function parseSwarmRequest(event) {
|
|
1361
|
+
const base = parseJobRequest(event);
|
|
1362
|
+
if (!base) return null;
|
|
1363
|
+
const swarmTag = event.tags.find((t) => t[0] === "swarm");
|
|
1364
|
+
if (!swarmTag) return null;
|
|
1365
|
+
const maxProvidersStr = swarmTag[1];
|
|
1366
|
+
if (maxProvidersStr === void 0) return null;
|
|
1367
|
+
const maxProviders = parseInt(maxProvidersStr, 10);
|
|
1368
|
+
if (isNaN(maxProviders) || maxProviders < 1) return null;
|
|
1369
|
+
const judgeTag = event.tags.find((t) => t[0] === "judge");
|
|
1370
|
+
const judge = judgeTag?.[1] ?? "customer";
|
|
1371
|
+
return {
|
|
1372
|
+
...base,
|
|
1373
|
+
maxProviders,
|
|
1374
|
+
judge
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
function parseSwarmSelection(event) {
|
|
1378
|
+
if (event.kind !== JOB_FEEDBACK_KIND) return null;
|
|
1379
|
+
const winnerTag = event.tags.find((t) => t[0] === "winner");
|
|
1380
|
+
if (!winnerTag) return null;
|
|
1381
|
+
const winnerResultEventId = winnerTag[1];
|
|
1382
|
+
if (winnerResultEventId === void 0 || !HEX_64_REGEX3.test(winnerResultEventId))
|
|
1383
|
+
return null;
|
|
1384
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
1385
|
+
if (!eTag) return null;
|
|
1386
|
+
const swarmRequestEventId = eTag[1];
|
|
1387
|
+
if (swarmRequestEventId === void 0 || !HEX_64_REGEX3.test(swarmRequestEventId))
|
|
1388
|
+
return null;
|
|
1389
|
+
return {
|
|
1390
|
+
swarmRequestEventId,
|
|
1391
|
+
winnerResultEventId
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/events/prefix-claim.ts
|
|
1396
|
+
import { finalizeEvent as finalizeEvent8 } from "nostr-tools/pure";
|
|
1397
|
+
function buildPrefixClaimEvent(content, secretKey) {
|
|
1398
|
+
return finalizeEvent8(
|
|
1399
|
+
{
|
|
1400
|
+
kind: PREFIX_CLAIM_KIND,
|
|
1401
|
+
content: JSON.stringify(content),
|
|
1402
|
+
tags: [],
|
|
1403
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1404
|
+
},
|
|
1405
|
+
secretKey
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
function parsePrefixClaimEvent(event) {
|
|
1409
|
+
if (event.kind !== PREFIX_CLAIM_KIND) {
|
|
1410
|
+
return null;
|
|
1411
|
+
}
|
|
1412
|
+
try {
|
|
1413
|
+
const parsed = JSON.parse(event.content);
|
|
1414
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1415
|
+
return null;
|
|
1416
|
+
}
|
|
1417
|
+
const obj = parsed;
|
|
1418
|
+
const requestedPrefix = obj["requestedPrefix"];
|
|
1419
|
+
if (typeof requestedPrefix !== "string" || requestedPrefix.length === 0) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
return { requestedPrefix };
|
|
1423
|
+
} catch {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function buildPrefixGrantEvent(content, secretKey) {
|
|
1428
|
+
return finalizeEvent8(
|
|
1429
|
+
{
|
|
1430
|
+
kind: PREFIX_GRANT_KIND,
|
|
1431
|
+
content: JSON.stringify(content),
|
|
1432
|
+
tags: [],
|
|
1433
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1434
|
+
},
|
|
1435
|
+
secretKey
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
function parsePrefixGrantEvent(event) {
|
|
1439
|
+
if (event.kind !== PREFIX_GRANT_KIND) {
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
try {
|
|
1443
|
+
const parsed = JSON.parse(event.content);
|
|
1444
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
1445
|
+
return null;
|
|
1446
|
+
}
|
|
1447
|
+
const obj = parsed;
|
|
1448
|
+
const grantedPrefix = obj["grantedPrefix"];
|
|
1449
|
+
const claimerPubkey = obj["claimerPubkey"];
|
|
1450
|
+
const ilpAddress = obj["ilpAddress"];
|
|
1451
|
+
if (typeof grantedPrefix !== "string" || grantedPrefix.length === 0) {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
if (typeof claimerPubkey !== "string" || claimerPubkey.length === 0) {
|
|
1455
|
+
return null;
|
|
1456
|
+
}
|
|
1457
|
+
if (typeof ilpAddress !== "string" || ilpAddress.length === 0) {
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
grantedPrefix,
|
|
1462
|
+
claimerPubkey,
|
|
1463
|
+
ilpAddress
|
|
1464
|
+
};
|
|
1465
|
+
} catch {
|
|
1466
|
+
return null;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/bootstrap/AttestationVerifier.ts
|
|
1471
|
+
var AttestationState = /* @__PURE__ */ ((AttestationState2) => {
|
|
1472
|
+
AttestationState2["VALID"] = "valid";
|
|
1473
|
+
AttestationState2["STALE"] = "stale";
|
|
1474
|
+
AttestationState2["UNATTESTED"] = "unattested";
|
|
1475
|
+
return AttestationState2;
|
|
1476
|
+
})(AttestationState || {});
|
|
1477
|
+
var DEFAULT_VALIDITY_SECONDS = 300;
|
|
1478
|
+
var DEFAULT_GRACE_SECONDS = 30;
|
|
1479
|
+
var AttestationVerifier = class {
|
|
1480
|
+
knownGoodPcrs;
|
|
1481
|
+
validitySeconds;
|
|
1482
|
+
graceSeconds;
|
|
1483
|
+
constructor(config) {
|
|
1484
|
+
this.knownGoodPcrs = new Map(config.knownGoodPcrs);
|
|
1485
|
+
const validity = config.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
1486
|
+
const grace = config.graceSeconds ?? DEFAULT_GRACE_SECONDS;
|
|
1487
|
+
if (!Number.isFinite(validity) || validity < 0) {
|
|
1488
|
+
throw new Error(
|
|
1489
|
+
`validitySeconds must be a non-negative finite number, got ${String(validity)}`
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
if (!Number.isFinite(grace) || grace < 0) {
|
|
1493
|
+
throw new Error(
|
|
1494
|
+
`graceSeconds must be a non-negative finite number, got ${String(grace)}`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
this.validitySeconds = validity;
|
|
1498
|
+
this.graceSeconds = grace;
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Verifies a TEE attestation's PCR values against the known-good registry.
|
|
1502
|
+
*
|
|
1503
|
+
* All three PCR values (pcr0, pcr1, pcr2) must be present and truthy in
|
|
1504
|
+
* the registry for verification to pass.
|
|
1505
|
+
*
|
|
1506
|
+
* @param attestation - The TEE attestation to verify.
|
|
1507
|
+
* @returns Verification result with `valid: true` or `valid: false` with reason.
|
|
1508
|
+
*/
|
|
1509
|
+
verify(attestation) {
|
|
1510
|
+
const pcr0Valid = this.knownGoodPcrs.get(attestation.pcr0) === true;
|
|
1511
|
+
const pcr1Valid = this.knownGoodPcrs.get(attestation.pcr1) === true;
|
|
1512
|
+
const pcr2Valid = this.knownGoodPcrs.get(attestation.pcr2) === true;
|
|
1513
|
+
if (pcr0Valid && pcr1Valid && pcr2Valid) {
|
|
1514
|
+
return { valid: true };
|
|
1515
|
+
}
|
|
1516
|
+
return { valid: false, reason: "PCR mismatch" };
|
|
1517
|
+
}
|
|
1518
|
+
/**
|
|
1519
|
+
* Computes the attestation lifecycle state based on timing.
|
|
1520
|
+
*
|
|
1521
|
+
* Boundary behavior:
|
|
1522
|
+
* - At exactly `attestedAt + validitySeconds`: VALID (inclusive <=)
|
|
1523
|
+
* - At exactly `attestedAt + validitySeconds + graceSeconds`: STALE (inclusive <=)
|
|
1524
|
+
* - After grace expires: UNATTESTED
|
|
1525
|
+
*
|
|
1526
|
+
* @param _attestation - The TEE attestation (unused, reserved for future per-attestation logic).
|
|
1527
|
+
* @param attestedAt - Unix timestamp when the attestation was created.
|
|
1528
|
+
* @param now - Optional current unix timestamp (defaults to real clock).
|
|
1529
|
+
* @returns The current attestation state.
|
|
1530
|
+
*/
|
|
1531
|
+
getAttestationState(_attestation, attestedAt, now) {
|
|
1532
|
+
if (!Number.isFinite(attestedAt)) {
|
|
1533
|
+
return "unattested" /* UNATTESTED */;
|
|
1534
|
+
}
|
|
1535
|
+
const currentTime = now ?? Math.floor(Date.now() / 1e3);
|
|
1536
|
+
const validityEnd = attestedAt + this.validitySeconds;
|
|
1537
|
+
const graceEnd = validityEnd + this.graceSeconds;
|
|
1538
|
+
if (currentTime <= validityEnd) {
|
|
1539
|
+
return "valid" /* VALID */;
|
|
1540
|
+
}
|
|
1541
|
+
if (currentTime <= graceEnd) {
|
|
1542
|
+
return "stale" /* STALE */;
|
|
1543
|
+
}
|
|
1544
|
+
return "unattested" /* UNATTESTED */;
|
|
1545
|
+
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Ranks peers by attestation status: attested peers first, then non-attested.
|
|
1548
|
+
*
|
|
1549
|
+
* Preserves relative order within each group (stable sort via filter).
|
|
1550
|
+
* Does NOT mutate the input array -- returns a new sorted array.
|
|
1551
|
+
*
|
|
1552
|
+
* Attestation is a preference, not a requirement. Non-attested peers
|
|
1553
|
+
* remain in the result and are connectable.
|
|
1554
|
+
*
|
|
1555
|
+
* @param peers - Array of peer descriptors to rank.
|
|
1556
|
+
* @returns New array with attested peers first, preserving relative order.
|
|
1557
|
+
*/
|
|
1558
|
+
rankPeers(peers) {
|
|
1559
|
+
const attested = peers.filter((p) => p.attested);
|
|
1560
|
+
const nonAttested = peers.filter((p) => !p.attested);
|
|
1561
|
+
return [...attested, ...nonAttested];
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
// src/events/attested-result-verifier.ts
|
|
1566
|
+
var AttestedResultVerifier = class {
|
|
1567
|
+
attestationVerifier;
|
|
1568
|
+
constructor(options) {
|
|
1569
|
+
this.attestationVerifier = options.attestationVerifier;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Verifies that a Kind 6xxx result was computed in a valid TEE enclave.
|
|
1573
|
+
*
|
|
1574
|
+
* Performs three checks:
|
|
1575
|
+
* (a) Pubkey match: attestationEvent.pubkey === resultEvent.pubkey
|
|
1576
|
+
* (b) PCR validity: AttestationVerifier.verify(parsedAttestation.attestation)
|
|
1577
|
+
* (c) Time validity: attestation was VALID at resultEvent.created_at
|
|
1578
|
+
*
|
|
1579
|
+
* @param resultEvent - The Kind 6xxx result Nostr event.
|
|
1580
|
+
* @param _parsedResult - The parsed job result (reserved for future use).
|
|
1581
|
+
* @param attestationEvent - The kind:10033 attestation Nostr event.
|
|
1582
|
+
* @param parsedAttestation - The parsed attestation data.
|
|
1583
|
+
* @returns Verification result with valid flag, reason, and attestation state.
|
|
1584
|
+
*/
|
|
1585
|
+
verifyAttestedResult(resultEvent, _parsedResult, attestationEvent, parsedAttestation) {
|
|
1586
|
+
if (attestationEvent.pubkey !== resultEvent.pubkey) {
|
|
1587
|
+
return { valid: false, reason: "pubkey mismatch" };
|
|
1588
|
+
}
|
|
1589
|
+
const pcrResult = this.attestationVerifier.verify(
|
|
1590
|
+
parsedAttestation.attestation
|
|
1591
|
+
);
|
|
1592
|
+
if (!pcrResult.valid) {
|
|
1593
|
+
return { valid: false, reason: "PCR mismatch" };
|
|
1594
|
+
}
|
|
1595
|
+
const state = this.attestationVerifier.getAttestationState(
|
|
1596
|
+
parsedAttestation.attestation,
|
|
1597
|
+
attestationEvent.created_at,
|
|
1598
|
+
resultEvent.created_at
|
|
1599
|
+
);
|
|
1600
|
+
if (state !== "valid" /* VALID */) {
|
|
1601
|
+
return {
|
|
1602
|
+
valid: false,
|
|
1603
|
+
reason: "attestation expired at result creation time",
|
|
1604
|
+
attestationState: state
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
return { valid: true, attestationState: "valid" /* VALID */ };
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
function hasRequireAttestation(params) {
|
|
1611
|
+
return params.some(
|
|
1612
|
+
(p) => p.key === "require_attestation" && p.value === "true"
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/events/arweave-storage.ts
|
|
1617
|
+
import { finalizeEvent as finalizeEvent9 } from "nostr-tools/pure";
|
|
1618
|
+
var BASE64_REGEX2 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
1619
|
+
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;
|
|
1620
|
+
function isValidBase64(str) {
|
|
1621
|
+
if (str.length === 0) return false;
|
|
1622
|
+
if (!BASE64_REGEX2.test(str)) return false;
|
|
1623
|
+
try {
|
|
1624
|
+
Buffer.from(str, "base64");
|
|
1625
|
+
return true;
|
|
1626
|
+
} catch {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
function buildBlobStorageRequest(params, secretKey) {
|
|
1631
|
+
if (!params.blobData || params.blobData.length === 0) {
|
|
1632
|
+
throw new ToonError(
|
|
1633
|
+
"Blob storage request blobData is required and must not be empty",
|
|
1634
|
+
"DVM_MISSING_INPUT"
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
if (typeof params.bid !== "string" || params.bid === "") {
|
|
1638
|
+
throw new ToonError(
|
|
1639
|
+
"Blob storage request bid must be a non-empty string (USDC micro-units)",
|
|
1640
|
+
"DVM_INVALID_BID"
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
const contentType = params.contentType ?? "application/octet-stream";
|
|
1644
|
+
const base64Blob = params.blobData.toString("base64");
|
|
1645
|
+
const tags = [];
|
|
1646
|
+
tags.push(["i", base64Blob, "blob"]);
|
|
1647
|
+
tags.push(["bid", params.bid, "usdc"]);
|
|
1648
|
+
tags.push(["output", contentType]);
|
|
1649
|
+
if (params.params !== void 0) {
|
|
1650
|
+
for (const p of params.params) {
|
|
1651
|
+
tags.push(["param", p.key, p.value]);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return finalizeEvent9(
|
|
1655
|
+
{
|
|
1656
|
+
kind: BLOB_STORAGE_REQUEST_KIND,
|
|
1657
|
+
content: "",
|
|
1658
|
+
tags,
|
|
1659
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1660
|
+
},
|
|
1661
|
+
secretKey
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
function parseBlobStorageRequest(event) {
|
|
1665
|
+
if (event.kind !== BLOB_STORAGE_REQUEST_KIND) {
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
const iTag = event.tags.find((t) => t[0] === "i");
|
|
1669
|
+
if (!iTag) return null;
|
|
1670
|
+
const base64Data = iTag[1];
|
|
1671
|
+
const inputType = iTag[2];
|
|
1672
|
+
if (base64Data === void 0 || base64Data === "") return null;
|
|
1673
|
+
if (inputType !== "blob") return null;
|
|
1674
|
+
if (!isValidBase64(base64Data)) {
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
const bidTag = event.tags.find((t) => t[0] === "bid");
|
|
1678
|
+
if (!bidTag) return null;
|
|
1679
|
+
const bidAmount = bidTag[1];
|
|
1680
|
+
if (bidAmount === void 0 || bidAmount === "") return null;
|
|
1681
|
+
const outputTag = event.tags.find((t) => t[0] === "output");
|
|
1682
|
+
const contentType = outputTag?.[1] && outputTag[1] !== "" ? outputTag[1] : "application/octet-stream";
|
|
1683
|
+
const blobData = Buffer.from(base64Data, "base64");
|
|
1684
|
+
const paramTags = event.tags.filter((t) => t[0] === "param");
|
|
1685
|
+
const paramMap = /* @__PURE__ */ new Map();
|
|
1686
|
+
for (const pt of paramTags) {
|
|
1687
|
+
const key = pt[1];
|
|
1688
|
+
const value = pt[2];
|
|
1689
|
+
if (key !== void 0 && value !== void 0) {
|
|
1690
|
+
paramMap.set(key, value);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const result = {
|
|
1694
|
+
blobData,
|
|
1695
|
+
contentType
|
|
1696
|
+
};
|
|
1697
|
+
const uploadId = paramMap.get("uploadId");
|
|
1698
|
+
if (uploadId !== void 0 && UUID_REGEX.test(uploadId)) {
|
|
1699
|
+
result.uploadId = uploadId;
|
|
1700
|
+
}
|
|
1701
|
+
const chunkIndexStr = paramMap.get("chunkIndex");
|
|
1702
|
+
if (chunkIndexStr !== void 0) {
|
|
1703
|
+
const chunkIndex = parseInt(chunkIndexStr, 10);
|
|
1704
|
+
if (!isNaN(chunkIndex) && chunkIndex >= 0) {
|
|
1705
|
+
result.chunkIndex = chunkIndex;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const totalChunksStr = paramMap.get("totalChunks");
|
|
1709
|
+
if (totalChunksStr !== void 0) {
|
|
1710
|
+
const totalChunks = parseInt(totalChunksStr, 10);
|
|
1711
|
+
if (!isNaN(totalChunks) && totalChunks > 0) {
|
|
1712
|
+
result.totalChunks = totalChunks;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return result;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// src/events/reputation.ts
|
|
1719
|
+
import { finalizeEvent as finalizeEvent10 } from "nostr-tools/pure";
|
|
1720
|
+
var HEX_64_REGEX4 = /^[0-9a-f]{64}$/;
|
|
1721
|
+
function buildJobReviewEvent(params, secretKey) {
|
|
1722
|
+
if (!HEX_64_REGEX4.test(params.jobRequestEventId)) {
|
|
1723
|
+
throw new ToonError(
|
|
1724
|
+
"Job review jobRequestEventId must be a 64-character lowercase hex string",
|
|
1725
|
+
"REPUTATION_INVALID_JOB_REQUEST_EVENT_ID"
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
if (!HEX_64_REGEX4.test(params.targetPubkey)) {
|
|
1729
|
+
throw new ToonError(
|
|
1730
|
+
"Job review targetPubkey must be a 64-character lowercase hex string",
|
|
1731
|
+
"REPUTATION_INVALID_TARGET_PUBKEY"
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof params.rating !== "number" || !Number.isInteger(params.rating) || params.rating < 1 || params.rating > 5) {
|
|
1735
|
+
throw new ToonError(
|
|
1736
|
+
`Job review rating must be an integer 1-5, got ${String(params.rating)}`,
|
|
1737
|
+
"REPUTATION_INVALID_RATING"
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
if (params.role !== "customer" && params.role !== "provider") {
|
|
1741
|
+
throw new ToonError(
|
|
1742
|
+
`Job review role must be 'customer' or 'provider', got '${String(params.role)}'`,
|
|
1743
|
+
"REPUTATION_INVALID_ROLE"
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
const tags = [
|
|
1747
|
+
["d", params.jobRequestEventId],
|
|
1748
|
+
["p", params.targetPubkey],
|
|
1749
|
+
["rating", params.rating.toString()],
|
|
1750
|
+
["role", params.role]
|
|
1751
|
+
];
|
|
1752
|
+
return finalizeEvent10(
|
|
1753
|
+
{
|
|
1754
|
+
kind: JOB_REVIEW_KIND,
|
|
1755
|
+
content: params.content ?? "",
|
|
1756
|
+
tags,
|
|
1757
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1758
|
+
},
|
|
1759
|
+
secretKey
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
function parseJobReview(event) {
|
|
1763
|
+
if (event.kind !== JOB_REVIEW_KIND) {
|
|
1764
|
+
return null;
|
|
1765
|
+
}
|
|
1766
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
1767
|
+
if (!dTag) return null;
|
|
1768
|
+
const jobRequestEventId = dTag[1];
|
|
1769
|
+
if (jobRequestEventId === void 0 || !HEX_64_REGEX4.test(jobRequestEventId))
|
|
1770
|
+
return null;
|
|
1771
|
+
const pTag = event.tags.find((t) => t[0] === "p");
|
|
1772
|
+
if (!pTag) return null;
|
|
1773
|
+
const targetPubkey = pTag[1];
|
|
1774
|
+
if (targetPubkey === void 0 || !HEX_64_REGEX4.test(targetPubkey))
|
|
1775
|
+
return null;
|
|
1776
|
+
const ratingTag = event.tags.find((t) => t[0] === "rating");
|
|
1777
|
+
if (!ratingTag) return null;
|
|
1778
|
+
const ratingStr = ratingTag[1];
|
|
1779
|
+
if (ratingStr === void 0) return null;
|
|
1780
|
+
const rating = Number(ratingStr);
|
|
1781
|
+
if (!Number.isInteger(rating) || rating < 1 || rating > 5) return null;
|
|
1782
|
+
const roleTag = event.tags.find((t) => t[0] === "role");
|
|
1783
|
+
if (!roleTag) return null;
|
|
1784
|
+
const role = roleTag[1];
|
|
1785
|
+
if (role !== "customer" && role !== "provider") return null;
|
|
713
1786
|
return {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1787
|
+
jobRequestEventId,
|
|
1788
|
+
targetPubkey,
|
|
1789
|
+
rating,
|
|
1790
|
+
role,
|
|
718
1791
|
content: event.content
|
|
719
1792
|
};
|
|
720
1793
|
}
|
|
721
|
-
function
|
|
722
|
-
if (
|
|
723
|
-
|
|
1794
|
+
function buildWotDeclarationEvent(params, secretKey) {
|
|
1795
|
+
if (!HEX_64_REGEX4.test(params.targetPubkey)) {
|
|
1796
|
+
throw new ToonError(
|
|
1797
|
+
"WoT declaration targetPubkey must be a 64-character lowercase hex string",
|
|
1798
|
+
"REPUTATION_INVALID_TARGET_PUBKEY"
|
|
1799
|
+
);
|
|
724
1800
|
}
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1801
|
+
const tags = [
|
|
1802
|
+
["d", params.targetPubkey],
|
|
1803
|
+
["p", params.targetPubkey]
|
|
1804
|
+
];
|
|
1805
|
+
return finalizeEvent10(
|
|
1806
|
+
{
|
|
1807
|
+
kind: WEB_OF_TRUST_KIND,
|
|
1808
|
+
content: params.content ?? "",
|
|
1809
|
+
tags,
|
|
1810
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
1811
|
+
},
|
|
1812
|
+
secretKey
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
function parseWotDeclaration(event) {
|
|
1816
|
+
if (event.kind !== WEB_OF_TRUST_KIND) {
|
|
729
1817
|
return null;
|
|
1818
|
+
}
|
|
1819
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
1820
|
+
if (!dTag) return null;
|
|
1821
|
+
const dValue = dTag[1];
|
|
1822
|
+
if (dValue === void 0) return null;
|
|
730
1823
|
const pTag = event.tags.find((t) => t[0] === "p");
|
|
731
1824
|
if (!pTag) return null;
|
|
732
|
-
const
|
|
733
|
-
if (
|
|
734
|
-
return null;
|
|
735
|
-
const statusTag = event.tags.find((t) => t[0] === "status");
|
|
736
|
-
if (!statusTag) return null;
|
|
737
|
-
const statusValue = statusTag[1];
|
|
738
|
-
if (statusValue === void 0) return null;
|
|
739
|
-
if (!VALID_STATUSES.has(statusValue)) {
|
|
1825
|
+
const targetPubkey = pTag[1];
|
|
1826
|
+
if (targetPubkey === void 0 || !HEX_64_REGEX4.test(targetPubkey))
|
|
740
1827
|
return null;
|
|
741
|
-
|
|
1828
|
+
if (dValue !== targetPubkey) return null;
|
|
742
1829
|
return {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
status: statusValue,
|
|
1830
|
+
targetPubkey,
|
|
1831
|
+
declarerPubkey: event.pubkey,
|
|
746
1832
|
content: event.content
|
|
747
1833
|
};
|
|
748
1834
|
}
|
|
1835
|
+
var ReputationScoreCalculator = class {
|
|
1836
|
+
/**
|
|
1837
|
+
* Computes the composite reputation score from pre-computed signals.
|
|
1838
|
+
*
|
|
1839
|
+
* @param signals - The individual signal values.
|
|
1840
|
+
* @returns The composite score with individual signals.
|
|
1841
|
+
*/
|
|
1842
|
+
calculateScore(signals) {
|
|
1843
|
+
const score = signals.trustedBy * 100 + Math.log10(Math.max(1, signals.channelVolumeUsdc)) * 10 + signals.jobsCompleted * 5 + signals.avgRating * 20;
|
|
1844
|
+
if (!isFinite(score)) {
|
|
1845
|
+
return { score: 0, signals };
|
|
1846
|
+
}
|
|
1847
|
+
return { score, signals };
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* Computes the threshold-filtered trusted_by count from WoT declarations.
|
|
1851
|
+
*
|
|
1852
|
+
* Declarers with non-zero channel volume contribute 1 to the count.
|
|
1853
|
+
* Declarers with zero channel volume contribute 0 (sybil defense).
|
|
1854
|
+
*
|
|
1855
|
+
* @param wotDeclarations - Parsed WoT declarations targeting the provider.
|
|
1856
|
+
* @param getChannelVolume - Callback to look up a declarer's channel volume.
|
|
1857
|
+
* @returns The count of WoT declarations from non-zero-volume declarers.
|
|
1858
|
+
*/
|
|
1859
|
+
computeTrustedBy(wotDeclarations, getChannelVolume) {
|
|
1860
|
+
let count = 0;
|
|
1861
|
+
for (const declaration of wotDeclarations) {
|
|
1862
|
+
const volume = getChannelVolume(declaration.declarerPubkey);
|
|
1863
|
+
if (volume > 0) {
|
|
1864
|
+
count += 1;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return count;
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Computes the average rating from verified customer reviews only.
|
|
1871
|
+
*
|
|
1872
|
+
* Reviews are provided as tuples of `{ review, reviewerPubkey }` so the
|
|
1873
|
+
* calculator can filter by the verified customer set. Reviews from pubkeys
|
|
1874
|
+
* NOT in `verifiedCustomerPubkeys` are excluded entirely (customer-gate
|
|
1875
|
+
* sybil defense per E6-R013).
|
|
1876
|
+
*
|
|
1877
|
+
* @param reviews - Parsed job reviews with reviewer pubkeys.
|
|
1878
|
+
* @param verifiedCustomerPubkeys - Set of pubkeys that authored Kind 5xxx requests.
|
|
1879
|
+
* @returns The mean rating from verified reviews, or 0 when no verified reviews exist.
|
|
1880
|
+
*/
|
|
1881
|
+
computeAvgRating(reviews, verifiedCustomerPubkeys) {
|
|
1882
|
+
let sum = 0;
|
|
1883
|
+
let count = 0;
|
|
1884
|
+
for (const { review, reviewerPubkey } of reviews) {
|
|
1885
|
+
if (!verifiedCustomerPubkeys.has(reviewerPubkey)) {
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
if (Number.isInteger(review.rating) && review.rating >= 1 && review.rating <= 5) {
|
|
1889
|
+
sum += review.rating;
|
|
1890
|
+
count += 1;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
return count === 0 ? 0 : sum / count;
|
|
1894
|
+
}
|
|
1895
|
+
};
|
|
1896
|
+
function hasMinReputation(params) {
|
|
1897
|
+
const param = params.find((p) => p.key === "min_reputation");
|
|
1898
|
+
if (param === void 0) {
|
|
1899
|
+
return null;
|
|
1900
|
+
}
|
|
1901
|
+
const trimmed = param.value.trim();
|
|
1902
|
+
const numericValue = Number(trimmed);
|
|
1903
|
+
if (trimmed === "" || isNaN(numericValue) || !isFinite(numericValue)) {
|
|
1904
|
+
throw new ToonError(
|
|
1905
|
+
`min_reputation value must be numeric, got '${param.value}'`,
|
|
1906
|
+
"REPUTATION_INVALID_MIN_REPUTATION"
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
return numericValue;
|
|
1910
|
+
}
|
|
749
1911
|
|
|
750
1912
|
// src/discovery/NostrPeerDiscovery.ts
|
|
751
1913
|
import { SimplePool } from "nostr-tools/pool";
|
|
@@ -1455,6 +2617,98 @@ async function publishSeedRelayEntry(config) {
|
|
|
1455
2617
|
return { publishedTo, eventId: event.id };
|
|
1456
2618
|
}
|
|
1457
2619
|
|
|
2620
|
+
// src/fee/calculate-route-amount.ts
|
|
2621
|
+
function calculateRouteAmount(params) {
|
|
2622
|
+
const { basePricePerByte, packetByteLength, hopFees } = params;
|
|
2623
|
+
if (packetByteLength < 0) {
|
|
2624
|
+
return 0n;
|
|
2625
|
+
}
|
|
2626
|
+
const bytes = BigInt(packetByteLength);
|
|
2627
|
+
const baseAmount = basePricePerByte * bytes;
|
|
2628
|
+
const intermediaryFees = hopFees.reduce((sum, fee) => sum + fee * bytes, 0n);
|
|
2629
|
+
return baseAmount + intermediaryFees;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/fee/resolve-route-fees.ts
|
|
2633
|
+
var _cachedPeersRef = null;
|
|
2634
|
+
var _cachedPeerFingerprint = "";
|
|
2635
|
+
var _cachedPeerMap = null;
|
|
2636
|
+
function peerFingerprint(peers) {
|
|
2637
|
+
let fp = `${peers.length}:`;
|
|
2638
|
+
for (const p of peers) {
|
|
2639
|
+
fp += `${p.peerInfo.ilpAddress}=${p.peerInfo.feePerByte};`;
|
|
2640
|
+
}
|
|
2641
|
+
return fp;
|
|
2642
|
+
}
|
|
2643
|
+
function getPeerByAddressMap(discoveredPeers) {
|
|
2644
|
+
const fp = peerFingerprint(discoveredPeers);
|
|
2645
|
+
if (_cachedPeersRef === discoveredPeers && _cachedPeerFingerprint === fp && _cachedPeerMap) {
|
|
2646
|
+
return _cachedPeerMap;
|
|
2647
|
+
}
|
|
2648
|
+
const peerByAddress = /* @__PURE__ */ new Map();
|
|
2649
|
+
for (const peer of discoveredPeers) {
|
|
2650
|
+
peerByAddress.set(peer.peerInfo.ilpAddress, peer);
|
|
2651
|
+
if (peer.peerInfo.ilpAddresses) {
|
|
2652
|
+
for (const addr of peer.peerInfo.ilpAddresses) {
|
|
2653
|
+
peerByAddress.set(addr, peer);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
_cachedPeersRef = discoveredPeers;
|
|
2658
|
+
_cachedPeerFingerprint = fp;
|
|
2659
|
+
_cachedPeerMap = peerByAddress;
|
|
2660
|
+
return peerByAddress;
|
|
2661
|
+
}
|
|
2662
|
+
function resolveRouteFees(params) {
|
|
2663
|
+
const { destination, ownIlpAddress, discoveredPeers } = params;
|
|
2664
|
+
if (!destination || !destination.trim() || !ownIlpAddress || !ownIlpAddress.trim()) {
|
|
2665
|
+
return { hopFees: [], warnings: [] };
|
|
2666
|
+
}
|
|
2667
|
+
const senderSegments = ownIlpAddress.split(".");
|
|
2668
|
+
const destSegments = destination.split(".");
|
|
2669
|
+
let lcaLength = 0;
|
|
2670
|
+
const minLength = Math.min(senderSegments.length, destSegments.length);
|
|
2671
|
+
for (let i = 0; i < minLength; i++) {
|
|
2672
|
+
if (senderSegments[i] === destSegments[i]) {
|
|
2673
|
+
lcaLength = i + 1;
|
|
2674
|
+
} else {
|
|
2675
|
+
break;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const intermediaryPrefixes = [];
|
|
2679
|
+
for (let i = lcaLength; i < destSegments.length - 1; i++) {
|
|
2680
|
+
const prefix = destSegments.slice(0, i + 1).join(".");
|
|
2681
|
+
intermediaryPrefixes.push(prefix);
|
|
2682
|
+
}
|
|
2683
|
+
if (intermediaryPrefixes.length === 0) {
|
|
2684
|
+
return { hopFees: [], warnings: [] };
|
|
2685
|
+
}
|
|
2686
|
+
const peerByAddress = getPeerByAddressMap(discoveredPeers);
|
|
2687
|
+
const hopFees = [];
|
|
2688
|
+
const warnings = [];
|
|
2689
|
+
for (const prefix of intermediaryPrefixes) {
|
|
2690
|
+
const peer = peerByAddress.get(prefix);
|
|
2691
|
+
if (peer) {
|
|
2692
|
+
let fee;
|
|
2693
|
+
try {
|
|
2694
|
+
fee = BigInt(peer.peerInfo.feePerByte ?? "0");
|
|
2695
|
+
} catch {
|
|
2696
|
+
fee = 0n;
|
|
2697
|
+
warnings.push(
|
|
2698
|
+
`Invalid feePerByte "${peer.peerInfo.feePerByte}" at ${prefix}: defaulting to 0`
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
hopFees.push(fee < 0n ? 0n : fee);
|
|
2702
|
+
} else {
|
|
2703
|
+
hopFees.push(0n);
|
|
2704
|
+
warnings.push(
|
|
2705
|
+
`Unknown intermediary at ${prefix}: defaulting feePerByte to 0`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return { hopFees, warnings };
|
|
2710
|
+
}
|
|
2711
|
+
|
|
1458
2712
|
// src/settlement/settlement.ts
|
|
1459
2713
|
function negotiateSettlementChain(requesterChains, responderChains, requesterPreferredTokens, responderPreferredTokens) {
|
|
1460
2714
|
const responderSet = new Set(responderChains);
|
|
@@ -1868,14 +3122,23 @@ var BootstrapService = class {
|
|
|
1868
3122
|
const toonBytes = this.toonEncoder(ilpInfoEvent);
|
|
1869
3123
|
const base64Toon = Buffer.from(toonBytes).toString("base64");
|
|
1870
3124
|
const amount = String(BigInt(toonBytes.length) * this.basePricePerByte);
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
amount
|
|
1874
|
-
|
|
1875
|
-
|
|
3125
|
+
let ilpResult;
|
|
3126
|
+
if (this.claimSigner && result.channelId && this.ilpClient.sendIlpPacketWithClaim) {
|
|
3127
|
+
const claim = await this.claimSigner(result.channelId, BigInt(amount));
|
|
3128
|
+
ilpResult = await this.ilpClient.sendIlpPacketWithClaim(
|
|
3129
|
+
{ destination: result.peerInfo.ilpAddress, amount, data: base64Toon },
|
|
3130
|
+
claim
|
|
3131
|
+
);
|
|
3132
|
+
} else {
|
|
3133
|
+
ilpResult = await this.ilpClient.sendIlpPacket({
|
|
3134
|
+
destination: result.peerInfo.ilpAddress,
|
|
3135
|
+
amount,
|
|
3136
|
+
data: base64Toon
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
1876
3139
|
if (ilpResult.accepted) {
|
|
1877
3140
|
console.log(
|
|
1878
|
-
`[Bootstrap] Announced to ${result.registeredPeerId} via ILP (
|
|
3141
|
+
`[Bootstrap] Announced to ${result.registeredPeerId} via ILP (eventId: ${ilpInfoEvent.id})`
|
|
1879
3142
|
);
|
|
1880
3143
|
this.emit({
|
|
1881
3144
|
type: "bootstrap:announced",
|
|
@@ -2138,6 +3401,56 @@ var BootstrapService = class {
|
|
|
2138
3401
|
);
|
|
2139
3402
|
}
|
|
2140
3403
|
}
|
|
3404
|
+
/**
|
|
3405
|
+
* Re-advertise own kind:10032 to all previously bootstrapped peers.
|
|
3406
|
+
*
|
|
3407
|
+
* Called after topology changes (addUpstreamPeer/removeUpstreamPeer) to
|
|
3408
|
+
* propagate updated ILP address lists to the network. Uses the same
|
|
3409
|
+
* ILP-first announcement path as the initial Phase 2 bootstrap.
|
|
3410
|
+
*
|
|
3411
|
+
* For non-ILP mode, publishes directly to each peer's relay URL.
|
|
3412
|
+
*
|
|
3413
|
+
* @param results - The bootstrap results from the initial bootstrap() call
|
|
3414
|
+
* @returns Number of peers successfully re-announced to
|
|
3415
|
+
*/
|
|
3416
|
+
async republish(results) {
|
|
3417
|
+
if (results.length === 0) {
|
|
3418
|
+
return 0;
|
|
3419
|
+
}
|
|
3420
|
+
let successCount = 0;
|
|
3421
|
+
if (this.ilpClient && this.toonEncoder) {
|
|
3422
|
+
for (const result of results) {
|
|
3423
|
+
try {
|
|
3424
|
+
await this.announceViaIlp(result);
|
|
3425
|
+
successCount++;
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
3428
|
+
this.emit({
|
|
3429
|
+
type: "bootstrap:announce-failed",
|
|
3430
|
+
peerId: result.registeredPeerId,
|
|
3431
|
+
reason: `republish: ${reason}`
|
|
3432
|
+
});
|
|
3433
|
+
console.warn(
|
|
3434
|
+
`[Bootstrap] Republish failed for ${result.registeredPeerId}:`,
|
|
3435
|
+
reason
|
|
3436
|
+
);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
} else {
|
|
3440
|
+
for (const result of results) {
|
|
3441
|
+
try {
|
|
3442
|
+
await this.publishOurInfo(result.knownPeer.relayUrl);
|
|
3443
|
+
successCount++;
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
console.warn(
|
|
3446
|
+
`[Bootstrap] Republish to ${result.knownPeer.relayUrl} failed:`,
|
|
3447
|
+
error instanceof Error ? error.message : "Unknown error"
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
return successCount;
|
|
3453
|
+
}
|
|
2141
3454
|
/**
|
|
2142
3455
|
* Get our pubkey.
|
|
2143
3456
|
*/
|
|
@@ -2187,7 +3500,8 @@ function createDiscoveryTracker(config) {
|
|
|
2187
3500
|
});
|
|
2188
3501
|
}).catch((error) => {
|
|
2189
3502
|
console.warn(
|
|
2190
|
-
|
|
3503
|
+
"[DiscoveryTracker] Failed to deregister %s: %s",
|
|
3504
|
+
peerId,
|
|
2191
3505
|
error instanceof Error ? error.message : "Unknown error"
|
|
2192
3506
|
);
|
|
2193
3507
|
});
|
|
@@ -2268,7 +3582,8 @@ function createDiscoveryTracker(config) {
|
|
|
2268
3582
|
peeredPubkeys.delete(targetPubkey);
|
|
2269
3583
|
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2270
3584
|
console.warn(
|
|
2271
|
-
|
|
3585
|
+
"[DiscoveryTracker] Failed to register %s: %s",
|
|
3586
|
+
peerId,
|
|
2272
3587
|
reason
|
|
2273
3588
|
);
|
|
2274
3589
|
emit({
|
|
@@ -2332,7 +3647,8 @@ function createDiscoveryTracker(config) {
|
|
|
2332
3647
|
} catch (error) {
|
|
2333
3648
|
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
2334
3649
|
console.warn(
|
|
2335
|
-
|
|
3650
|
+
"[DiscoveryTracker] Settlement failed for %s: %s",
|
|
3651
|
+
peerId,
|
|
2336
3652
|
reason
|
|
2337
3653
|
);
|
|
2338
3654
|
emit({
|
|
@@ -2348,6 +3664,9 @@ function createDiscoveryTracker(config) {
|
|
|
2348
3664
|
(p) => !peeredPubkeys.has(p.pubkey)
|
|
2349
3665
|
);
|
|
2350
3666
|
},
|
|
3667
|
+
getAllDiscoveredPeers() {
|
|
3668
|
+
return [...discoveredPeers.values()];
|
|
3669
|
+
},
|
|
2351
3670
|
isPeered(targetPubkey) {
|
|
2352
3671
|
return peeredPubkeys.has(targetPubkey);
|
|
2353
3672
|
},
|
|
@@ -2415,7 +3734,6 @@ function createHttpIlpClient(baseUrl) {
|
|
|
2415
3734
|
const data = await response.json();
|
|
2416
3735
|
return {
|
|
2417
3736
|
accepted: data["accepted"] ?? data["fulfilled"] ?? false,
|
|
2418
|
-
fulfillment: data["fulfillment"],
|
|
2419
3737
|
data: data["data"],
|
|
2420
3738
|
code: data["code"],
|
|
2421
3739
|
message: data["message"]
|
|
@@ -2427,30 +3745,21 @@ var createHttpRuntimeClient = createHttpIlpClient;
|
|
|
2427
3745
|
var createAgentRuntimeClient = createHttpIlpClient;
|
|
2428
3746
|
|
|
2429
3747
|
// src/bootstrap/direct-ilp-client.ts
|
|
2430
|
-
|
|
2431
|
-
function createDirectIlpClient(connector, config) {
|
|
3748
|
+
function createDirectIlpClient(connector, _config) {
|
|
2432
3749
|
return {
|
|
2433
3750
|
async sendIlpPacket(params) {
|
|
2434
3751
|
try {
|
|
2435
3752
|
const amount = BigInt(params.amount);
|
|
2436
3753
|
const data = Uint8Array.from(Buffer.from(params.data, "base64"));
|
|
2437
|
-
let executionCondition;
|
|
2438
|
-
if (config?.toonDecoder && data.length > 0) {
|
|
2439
|
-
const decoded = config.toonDecoder(data);
|
|
2440
|
-
const fulfillment = createHash("sha256").update(decoded.id).digest();
|
|
2441
|
-
executionCondition = createHash("sha256").update(fulfillment).digest();
|
|
2442
|
-
}
|
|
2443
3754
|
const result = await connector.sendPacket({
|
|
2444
3755
|
destination: params.destination,
|
|
2445
3756
|
amount,
|
|
2446
3757
|
data,
|
|
2447
|
-
executionCondition,
|
|
2448
3758
|
expiresAt: new Date(Date.now() + 3e4)
|
|
2449
3759
|
});
|
|
2450
3760
|
if (result.type === "fulfill" || result.type === 13) {
|
|
2451
3761
|
return {
|
|
2452
3762
|
accepted: true,
|
|
2453
|
-
fulfillment: Buffer.from(result.fulfillment).toString("base64"),
|
|
2454
3763
|
data: result.data ? Buffer.from(result.data).toString("base64") : void 0
|
|
2455
3764
|
};
|
|
2456
3765
|
}
|
|
@@ -2629,7 +3938,6 @@ function createHttpIlpClient2(connectorUrl) {
|
|
|
2629
3938
|
if (result["accept"] || result["accepted"]) {
|
|
2630
3939
|
return {
|
|
2631
3940
|
accepted: true,
|
|
2632
|
-
fulfillment: result["fulfillment"],
|
|
2633
3941
|
data: result["data"]
|
|
2634
3942
|
};
|
|
2635
3943
|
} else {
|
|
@@ -2717,101 +4025,6 @@ function createHttpChannelClient(adminUrl) {
|
|
|
2717
4025
|
};
|
|
2718
4026
|
}
|
|
2719
4027
|
|
|
2720
|
-
// src/bootstrap/AttestationVerifier.ts
|
|
2721
|
-
var AttestationState = /* @__PURE__ */ ((AttestationState2) => {
|
|
2722
|
-
AttestationState2["VALID"] = "valid";
|
|
2723
|
-
AttestationState2["STALE"] = "stale";
|
|
2724
|
-
AttestationState2["UNATTESTED"] = "unattested";
|
|
2725
|
-
return AttestationState2;
|
|
2726
|
-
})(AttestationState || {});
|
|
2727
|
-
var DEFAULT_VALIDITY_SECONDS = 300;
|
|
2728
|
-
var DEFAULT_GRACE_SECONDS = 30;
|
|
2729
|
-
var AttestationVerifier = class {
|
|
2730
|
-
knownGoodPcrs;
|
|
2731
|
-
validitySeconds;
|
|
2732
|
-
graceSeconds;
|
|
2733
|
-
constructor(config) {
|
|
2734
|
-
this.knownGoodPcrs = new Map(config.knownGoodPcrs);
|
|
2735
|
-
const validity = config.validitySeconds ?? DEFAULT_VALIDITY_SECONDS;
|
|
2736
|
-
const grace = config.graceSeconds ?? DEFAULT_GRACE_SECONDS;
|
|
2737
|
-
if (!Number.isFinite(validity) || validity < 0) {
|
|
2738
|
-
throw new Error(
|
|
2739
|
-
`validitySeconds must be a non-negative finite number, got ${String(validity)}`
|
|
2740
|
-
);
|
|
2741
|
-
}
|
|
2742
|
-
if (!Number.isFinite(grace) || grace < 0) {
|
|
2743
|
-
throw new Error(
|
|
2744
|
-
`graceSeconds must be a non-negative finite number, got ${String(grace)}`
|
|
2745
|
-
);
|
|
2746
|
-
}
|
|
2747
|
-
this.validitySeconds = validity;
|
|
2748
|
-
this.graceSeconds = grace;
|
|
2749
|
-
}
|
|
2750
|
-
/**
|
|
2751
|
-
* Verifies a TEE attestation's PCR values against the known-good registry.
|
|
2752
|
-
*
|
|
2753
|
-
* All three PCR values (pcr0, pcr1, pcr2) must be present and truthy in
|
|
2754
|
-
* the registry for verification to pass.
|
|
2755
|
-
*
|
|
2756
|
-
* @param attestation - The TEE attestation to verify.
|
|
2757
|
-
* @returns Verification result with `valid: true` or `valid: false` with reason.
|
|
2758
|
-
*/
|
|
2759
|
-
verify(attestation) {
|
|
2760
|
-
const pcr0Valid = this.knownGoodPcrs.get(attestation.pcr0) === true;
|
|
2761
|
-
const pcr1Valid = this.knownGoodPcrs.get(attestation.pcr1) === true;
|
|
2762
|
-
const pcr2Valid = this.knownGoodPcrs.get(attestation.pcr2) === true;
|
|
2763
|
-
if (pcr0Valid && pcr1Valid && pcr2Valid) {
|
|
2764
|
-
return { valid: true };
|
|
2765
|
-
}
|
|
2766
|
-
return { valid: false, reason: "PCR mismatch" };
|
|
2767
|
-
}
|
|
2768
|
-
/**
|
|
2769
|
-
* Computes the attestation lifecycle state based on timing.
|
|
2770
|
-
*
|
|
2771
|
-
* Boundary behavior:
|
|
2772
|
-
* - At exactly `attestedAt + validitySeconds`: VALID (inclusive <=)
|
|
2773
|
-
* - At exactly `attestedAt + validitySeconds + graceSeconds`: STALE (inclusive <=)
|
|
2774
|
-
* - After grace expires: UNATTESTED
|
|
2775
|
-
*
|
|
2776
|
-
* @param _attestation - The TEE attestation (unused, reserved for future per-attestation logic).
|
|
2777
|
-
* @param attestedAt - Unix timestamp when the attestation was created.
|
|
2778
|
-
* @param now - Optional current unix timestamp (defaults to real clock).
|
|
2779
|
-
* @returns The current attestation state.
|
|
2780
|
-
*/
|
|
2781
|
-
getAttestationState(_attestation, attestedAt, now) {
|
|
2782
|
-
if (!Number.isFinite(attestedAt)) {
|
|
2783
|
-
return "unattested" /* UNATTESTED */;
|
|
2784
|
-
}
|
|
2785
|
-
const currentTime = now ?? Math.floor(Date.now() / 1e3);
|
|
2786
|
-
const validityEnd = attestedAt + this.validitySeconds;
|
|
2787
|
-
const graceEnd = validityEnd + this.graceSeconds;
|
|
2788
|
-
if (currentTime <= validityEnd) {
|
|
2789
|
-
return "valid" /* VALID */;
|
|
2790
|
-
}
|
|
2791
|
-
if (currentTime <= graceEnd) {
|
|
2792
|
-
return "stale" /* STALE */;
|
|
2793
|
-
}
|
|
2794
|
-
return "unattested" /* UNATTESTED */;
|
|
2795
|
-
}
|
|
2796
|
-
/**
|
|
2797
|
-
* Ranks peers by attestation status: attested peers first, then non-attested.
|
|
2798
|
-
*
|
|
2799
|
-
* Preserves relative order within each group (stable sort via filter).
|
|
2800
|
-
* Does NOT mutate the input array -- returns a new sorted array.
|
|
2801
|
-
*
|
|
2802
|
-
* Attestation is a preference, not a requirement. Non-attested peers
|
|
2803
|
-
* remain in the result and are connectable.
|
|
2804
|
-
*
|
|
2805
|
-
* @param peers - Array of peer descriptors to rank.
|
|
2806
|
-
* @returns New array with attested peers first, preserving relative order.
|
|
2807
|
-
*/
|
|
2808
|
-
rankPeers(peers) {
|
|
2809
|
-
const attested = peers.filter((p) => p.attested);
|
|
2810
|
-
const nonAttested = peers.filter((p) => !p.attested);
|
|
2811
|
-
return [...attested, ...nonAttested];
|
|
2812
|
-
}
|
|
2813
|
-
};
|
|
2814
|
-
|
|
2815
4028
|
// src/bootstrap/AttestationBootstrap.ts
|
|
2816
4029
|
var AttestationBootstrap = class {
|
|
2817
4030
|
config;
|
|
@@ -3017,21 +4230,24 @@ var CHAIN_PRESETS = {
|
|
|
3017
4230
|
chainId: 31337,
|
|
3018
4231
|
rpcUrl: "http://localhost:8545",
|
|
3019
4232
|
usdcAddress: MOCK_USDC_ADDRESS,
|
|
3020
|
-
tokenNetworkAddress: "0xCafac3dD18aC6c6e92c921884f9E4176737C052c"
|
|
4233
|
+
tokenNetworkAddress: "0xCafac3dD18aC6c6e92c921884f9E4176737C052c",
|
|
4234
|
+
registryAddress: "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"
|
|
3021
4235
|
},
|
|
3022
4236
|
"arbitrum-sepolia": {
|
|
3023
4237
|
name: "arbitrum-sepolia",
|
|
3024
4238
|
chainId: 421614,
|
|
3025
4239
|
rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc",
|
|
3026
4240
|
usdcAddress: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
|
|
3027
|
-
tokenNetworkAddress: ""
|
|
4241
|
+
tokenNetworkAddress: "0x91d62b1F7C5d1129A64EE3915c480DBF288B1cBa",
|
|
4242
|
+
registryAddress: ""
|
|
3028
4243
|
},
|
|
3029
4244
|
"arbitrum-one": {
|
|
3030
4245
|
name: "arbitrum-one",
|
|
3031
4246
|
chainId: 42161,
|
|
3032
4247
|
rpcUrl: "https://arb1.arbitrum.io/rpc",
|
|
3033
4248
|
usdcAddress: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
3034
|
-
tokenNetworkAddress: ""
|
|
4249
|
+
tokenNetworkAddress: "",
|
|
4250
|
+
registryAddress: ""
|
|
3035
4251
|
}
|
|
3036
4252
|
};
|
|
3037
4253
|
function resolveChainConfig(chain) {
|
|
@@ -3053,6 +4269,10 @@ function resolveChainConfig(chain) {
|
|
|
3053
4269
|
if (envTokenNetwork) {
|
|
3054
4270
|
resolved.tokenNetworkAddress = envTokenNetwork;
|
|
3055
4271
|
}
|
|
4272
|
+
const envRegistryAddress = process.env["TOON_REGISTRY_ADDRESS"];
|
|
4273
|
+
if (envRegistryAddress) {
|
|
4274
|
+
resolved.registryAddress = envRegistryAddress;
|
|
4275
|
+
}
|
|
3056
4276
|
return resolved;
|
|
3057
4277
|
}
|
|
3058
4278
|
function buildEip712Domain(config) {
|
|
@@ -3063,6 +4283,97 @@ function buildEip712Domain(config) {
|
|
|
3063
4283
|
verifyingContract: config.tokenNetworkAddress
|
|
3064
4284
|
};
|
|
3065
4285
|
}
|
|
4286
|
+
var SOLANA_CHAIN_PRESETS = {
|
|
4287
|
+
"solana-devnet": {
|
|
4288
|
+
name: "solana-devnet",
|
|
4289
|
+
chainType: "solana",
|
|
4290
|
+
rpcUrl: "http://localhost:19899",
|
|
4291
|
+
programId: "",
|
|
4292
|
+
// TBD: set after program deployment
|
|
4293
|
+
cluster: "devnet"
|
|
4294
|
+
}
|
|
4295
|
+
};
|
|
4296
|
+
var MINA_CHAIN_PRESETS = {
|
|
4297
|
+
"mina-devnet": {
|
|
4298
|
+
name: "mina-devnet",
|
|
4299
|
+
chainType: "mina",
|
|
4300
|
+
graphqlUrl: "http://localhost:19085/graphql",
|
|
4301
|
+
zkAppAddress: "",
|
|
4302
|
+
// TBD: set after zkApp deployment
|
|
4303
|
+
network: "devnet"
|
|
4304
|
+
}
|
|
4305
|
+
};
|
|
4306
|
+
function resolveSolanaChainConfig(name) {
|
|
4307
|
+
const preset = SOLANA_CHAIN_PRESETS[name];
|
|
4308
|
+
if (!preset) {
|
|
4309
|
+
const validNames = Object.keys(SOLANA_CHAIN_PRESETS).join(", ");
|
|
4310
|
+
throw new ToonError(
|
|
4311
|
+
`Unknown Solana chain "${name}". Valid Solana chains: ${validNames}`,
|
|
4312
|
+
"INVALID_CHAIN"
|
|
4313
|
+
);
|
|
4314
|
+
}
|
|
4315
|
+
const resolved = { ...preset };
|
|
4316
|
+
const envRpcUrl = process.env["SOLANA_RPC_URL"];
|
|
4317
|
+
if (envRpcUrl) {
|
|
4318
|
+
resolved.rpcUrl = envRpcUrl;
|
|
4319
|
+
}
|
|
4320
|
+
const envProgramId = process.env["SOLANA_PROGRAM_ID"];
|
|
4321
|
+
if (envProgramId) {
|
|
4322
|
+
resolved.programId = envProgramId;
|
|
4323
|
+
}
|
|
4324
|
+
return resolved;
|
|
4325
|
+
}
|
|
4326
|
+
function resolveMinaChainConfig(name) {
|
|
4327
|
+
const preset = MINA_CHAIN_PRESETS[name];
|
|
4328
|
+
if (!preset) {
|
|
4329
|
+
const validNames = Object.keys(MINA_CHAIN_PRESETS).join(", ");
|
|
4330
|
+
throw new ToonError(
|
|
4331
|
+
`Unknown Mina chain "${name}". Valid Mina chains: ${validNames}`,
|
|
4332
|
+
"INVALID_CHAIN"
|
|
4333
|
+
);
|
|
4334
|
+
}
|
|
4335
|
+
const resolved = { ...preset };
|
|
4336
|
+
const envGraphqlUrl = process.env["MINA_GRAPHQL_URL"];
|
|
4337
|
+
if (envGraphqlUrl) {
|
|
4338
|
+
resolved.graphqlUrl = envGraphqlUrl;
|
|
4339
|
+
}
|
|
4340
|
+
const envZkAppAddress = process.env["MINA_ZKAPP_ADDRESS"];
|
|
4341
|
+
if (envZkAppAddress) {
|
|
4342
|
+
resolved.zkAppAddress = envZkAppAddress;
|
|
4343
|
+
}
|
|
4344
|
+
return resolved;
|
|
4345
|
+
}
|
|
4346
|
+
function buildEvmProviderEntry(config, keyId) {
|
|
4347
|
+
return {
|
|
4348
|
+
chainType: "evm",
|
|
4349
|
+
chainId: `evm:${config.chainId}`,
|
|
4350
|
+
rpcUrl: config.rpcUrl,
|
|
4351
|
+
registryAddress: config.registryAddress,
|
|
4352
|
+
keyId
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
function buildSolanaProviderEntry(config, keyId) {
|
|
4356
|
+
return {
|
|
4357
|
+
chainType: "solana",
|
|
4358
|
+
chainId: `solana:${config.cluster}`,
|
|
4359
|
+
rpcUrl: config.rpcUrl,
|
|
4360
|
+
programId: config.programId,
|
|
4361
|
+
keyId,
|
|
4362
|
+
cluster: config.cluster,
|
|
4363
|
+
...config.tokenMint && { tokenMint: config.tokenMint }
|
|
4364
|
+
};
|
|
4365
|
+
}
|
|
4366
|
+
function buildMinaProviderEntry(config, keyId) {
|
|
4367
|
+
return {
|
|
4368
|
+
chainType: "mina",
|
|
4369
|
+
chainId: `mina:${config.network}`,
|
|
4370
|
+
graphqlUrl: config.graphqlUrl,
|
|
4371
|
+
zkAppAddress: config.zkAppAddress,
|
|
4372
|
+
...keyId && { keyId },
|
|
4373
|
+
...config.tokenId && { tokenId: config.tokenId },
|
|
4374
|
+
network: config.network
|
|
4375
|
+
};
|
|
4376
|
+
}
|
|
3066
4377
|
|
|
3067
4378
|
// src/x402/build-ilp-prepare.ts
|
|
3068
4379
|
function buildIlpPrepare(params) {
|
|
@@ -3149,7 +4460,7 @@ import { execFile as execFileCb } from "child_process";
|
|
|
3149
4460
|
import { readFile, mkdtemp, cp, writeFile, rm } from "fs/promises";
|
|
3150
4461
|
import { tmpdir } from "os";
|
|
3151
4462
|
import path from "path";
|
|
3152
|
-
import { createHash
|
|
4463
|
+
import { createHash } from "crypto";
|
|
3153
4464
|
function execFileAsync(cmd, args, options) {
|
|
3154
4465
|
return new Promise((resolve, reject) => {
|
|
3155
4466
|
execFileCb(cmd, args, options, (error, stdout, stderr) => {
|
|
@@ -3210,19 +4521,19 @@ var NixBuilder = class {
|
|
|
3210
4521
|
);
|
|
3211
4522
|
}
|
|
3212
4523
|
const imageData = await readFile(imagePath);
|
|
3213
|
-
const sha256 =
|
|
4524
|
+
const sha256 = createHash("sha256").update(imageData).digest("hex");
|
|
3214
4525
|
const imageHash = `sha256:${sha256}`;
|
|
3215
|
-
const pcr0 =
|
|
4526
|
+
const pcr0 = createHash("sha384").update(imageData).digest("hex");
|
|
3216
4527
|
const KERNEL_REGION_SIZE = 1024 * 1024;
|
|
3217
4528
|
const kernelRegion = imageData.subarray(
|
|
3218
4529
|
0,
|
|
3219
4530
|
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3220
4531
|
);
|
|
3221
|
-
const pcr1 =
|
|
4532
|
+
const pcr1 = createHash("sha384").update(kernelRegion).digest("hex");
|
|
3222
4533
|
const appRegion = imageData.subarray(
|
|
3223
4534
|
Math.min(KERNEL_REGION_SIZE, imageData.length)
|
|
3224
4535
|
);
|
|
3225
|
-
const pcr2 = appRegion.length > 0 ?
|
|
4536
|
+
const pcr2 = appRegion.length > 0 ? createHash("sha384").update(appRegion).digest("hex") : createHash("sha384").update("pcr2:").update(imageData).digest("hex");
|
|
3226
4537
|
return {
|
|
3227
4538
|
imageHash,
|
|
3228
4539
|
pcr0,
|
|
@@ -3413,29 +4724,40 @@ function createLogger(config) {
|
|
|
3413
4724
|
// src/index.ts
|
|
3414
4725
|
var VERSION = "0.1.0";
|
|
3415
4726
|
export {
|
|
4727
|
+
AddressRegistry,
|
|
3416
4728
|
ArDrivePeerRegistry,
|
|
3417
4729
|
AttestationBootstrap,
|
|
3418
4730
|
AttestationState,
|
|
3419
4731
|
AttestationVerifier,
|
|
4732
|
+
AttestedResultVerifier,
|
|
4733
|
+
BLOB_STORAGE_REQUEST_KIND,
|
|
4734
|
+
BLOB_STORAGE_RESULT_KIND,
|
|
3420
4735
|
BootstrapError,
|
|
3421
4736
|
BootstrapService,
|
|
3422
4737
|
CHAIN_PRESETS,
|
|
3423
4738
|
GenesisPeerLoader,
|
|
3424
4739
|
ILP_PEER_INFO_KIND,
|
|
4740
|
+
ILP_ROOT_PREFIX,
|
|
3425
4741
|
IMAGE_GENERATION_KIND,
|
|
3426
4742
|
InvalidEventError,
|
|
3427
4743
|
JOB_FEEDBACK_KIND,
|
|
3428
4744
|
JOB_REQUEST_KIND_BASE,
|
|
3429
4745
|
JOB_RESULT_KIND_BASE,
|
|
4746
|
+
JOB_REVIEW_KIND,
|
|
3430
4747
|
KmsIdentityError,
|
|
4748
|
+
MINA_CHAIN_PRESETS,
|
|
3431
4749
|
MOCK_USDC_ADDRESS,
|
|
3432
4750
|
MOCK_USDC_CONFIG,
|
|
3433
4751
|
NixBuilder,
|
|
3434
4752
|
NostrPeerDiscovery,
|
|
4753
|
+
PREFIX_CLAIM_KIND,
|
|
4754
|
+
PREFIX_GRANT_KIND,
|
|
3435
4755
|
PcrReproducibilityError,
|
|
3436
4756
|
PeerDiscoveryError,
|
|
4757
|
+
ReputationScoreCalculator,
|
|
3437
4758
|
SEED_RELAY_LIST_KIND,
|
|
3438
4759
|
SERVICE_DISCOVERY_KIND,
|
|
4760
|
+
SOLANA_CHAIN_PRESETS,
|
|
3439
4761
|
SeedRelayDiscovery,
|
|
3440
4762
|
SocialPeerDiscovery,
|
|
3441
4763
|
TEE_ATTESTATION_KIND,
|
|
@@ -3449,16 +4771,33 @@ export {
|
|
|
3449
4771
|
USDC_NAME,
|
|
3450
4772
|
USDC_SYMBOL,
|
|
3451
4773
|
VERSION,
|
|
4774
|
+
WEB_OF_TRUST_KIND,
|
|
4775
|
+
WORKFLOW_CHAIN_KIND,
|
|
3452
4776
|
analyzeDockerfileForNonDeterminism,
|
|
4777
|
+
assignAddressFromHandshake,
|
|
3453
4778
|
buildAttestationEvent,
|
|
4779
|
+
buildBlobStorageRequest,
|
|
3454
4780
|
buildEip712Domain,
|
|
4781
|
+
buildEvmProviderEntry,
|
|
3455
4782
|
buildIlpPeerInfoEvent,
|
|
3456
4783
|
buildIlpPrepare,
|
|
3457
4784
|
buildJobFeedbackEvent,
|
|
3458
4785
|
buildJobRequestEvent,
|
|
3459
4786
|
buildJobResultEvent,
|
|
4787
|
+
buildJobReviewEvent,
|
|
4788
|
+
buildMinaProviderEntry,
|
|
4789
|
+
buildPrefixClaimEvent,
|
|
4790
|
+
buildPrefixGrantEvent,
|
|
4791
|
+
buildPrefixHandshakeData,
|
|
3460
4792
|
buildSeedRelayListEvent,
|
|
3461
4793
|
buildServiceDiscoveryEvent,
|
|
4794
|
+
buildSolanaProviderEntry,
|
|
4795
|
+
buildSwarmRequestEvent,
|
|
4796
|
+
buildSwarmSelectionEvent,
|
|
4797
|
+
buildWorkflowDefinitionEvent,
|
|
4798
|
+
buildWotDeclarationEvent,
|
|
4799
|
+
calculateRouteAmount,
|
|
4800
|
+
checkAddressCollision,
|
|
3462
4801
|
createAgentRuntimeClient,
|
|
3463
4802
|
createDirectChannelClient,
|
|
3464
4803
|
createDirectConnectorAdmin,
|
|
@@ -3473,23 +4812,43 @@ export {
|
|
|
3473
4812
|
createLogger,
|
|
3474
4813
|
createToonNode,
|
|
3475
4814
|
decodeEventFromToon,
|
|
4815
|
+
deriveChildAddress,
|
|
3476
4816
|
deriveFromKmsSeed,
|
|
3477
4817
|
encodeEventToToon,
|
|
3478
4818
|
encodeEventToToonString,
|
|
4819
|
+
extractPrefixFromHandshake,
|
|
4820
|
+
hasMinReputation,
|
|
4821
|
+
hasRequireAttestation,
|
|
4822
|
+
isGenesisNode,
|
|
4823
|
+
isValidIlpAddressStructure,
|
|
3479
4824
|
negotiateSettlementChain,
|
|
3480
4825
|
parseAttestation,
|
|
4826
|
+
parseBlobStorageRequest,
|
|
3481
4827
|
parseIlpPeerInfo,
|
|
3482
4828
|
parseJobFeedback,
|
|
3483
4829
|
parseJobRequest,
|
|
3484
4830
|
parseJobResult,
|
|
4831
|
+
parseJobReview,
|
|
4832
|
+
parsePrefixClaimEvent,
|
|
4833
|
+
parsePrefixGrantEvent,
|
|
3485
4834
|
parseSeedRelayList,
|
|
3486
4835
|
parseServiceDiscovery,
|
|
4836
|
+
parseSwarmRequest,
|
|
4837
|
+
parseSwarmSelection,
|
|
4838
|
+
parseWorkflowDefinition,
|
|
4839
|
+
parseWotDeclaration,
|
|
3487
4840
|
publishSeedRelayEntry,
|
|
3488
4841
|
readDockerfileNix,
|
|
3489
4842
|
resolveChainConfig,
|
|
4843
|
+
resolveMinaChainConfig,
|
|
4844
|
+
resolveRouteFees,
|
|
4845
|
+
resolveSolanaChainConfig,
|
|
3490
4846
|
resolveTokenForChain,
|
|
3491
4847
|
shallowParseToon,
|
|
3492
4848
|
validateChainId,
|
|
4849
|
+
validateIlpAddress,
|
|
4850
|
+
validatePrefix,
|
|
4851
|
+
validatePrefixConsistency,
|
|
3493
4852
|
verifyPcrReproducibility
|
|
3494
4853
|
};
|
|
3495
4854
|
//# sourceMappingURL=index.js.map
|