@provenonce/sdk 0.5.0 → 0.8.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 +28 -3
- package/dist/index.d.mts +151 -8
- package/dist/index.d.ts +151 -8
- package/dist/index.js +372 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +370 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,19 +22,256 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
BeatAgent: () => BeatAgent,
|
|
24
24
|
computeBeat: () => computeBeat,
|
|
25
|
-
computeBeatsLite: () => computeBeatsLite
|
|
25
|
+
computeBeatsLite: () => computeBeatsLite,
|
|
26
|
+
generateWalletKeypair: () => generateWalletKeypair,
|
|
27
|
+
register: () => register
|
|
26
28
|
});
|
|
27
29
|
module.exports = __toCommonJS(index_exports);
|
|
28
30
|
|
|
29
31
|
// src/beat-sdk.ts
|
|
30
32
|
var import_crypto = require("crypto");
|
|
31
|
-
function computeBeat(prevHash, beatIndex, difficulty, nonce) {
|
|
33
|
+
function computeBeat(prevHash, beatIndex, difficulty, nonce, anchorHash) {
|
|
32
34
|
const timestamp = Date.now();
|
|
33
|
-
|
|
35
|
+
const seed = anchorHash ? `${prevHash}:${beatIndex}:${nonce || ""}:${anchorHash}` : `${prevHash}:${beatIndex}:${nonce || ""}`;
|
|
36
|
+
let current = (0, import_crypto.createHash)("sha256").update(seed).digest("hex");
|
|
34
37
|
for (let i = 0; i < difficulty; i++) {
|
|
35
38
|
current = (0, import_crypto.createHash)("sha256").update(current).digest("hex");
|
|
36
39
|
}
|
|
37
|
-
return { index: beatIndex, hash: current, prev: prevHash, timestamp, nonce };
|
|
40
|
+
return { index: beatIndex, hash: current, prev: prevHash, timestamp, nonce, anchor_hash: anchorHash };
|
|
41
|
+
}
|
|
42
|
+
var ED25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
43
|
+
function generateWalletKeypair() {
|
|
44
|
+
const { publicKey, privateKey } = (0, import_crypto.generateKeyPairSync)("ed25519");
|
|
45
|
+
const pubRaw = publicKey.export({ type: "spki", format: "der" }).subarray(12);
|
|
46
|
+
const privRaw = privateKey.export({ type: "pkcs8", format: "der" }).subarray(16);
|
|
47
|
+
return {
|
|
48
|
+
publicKey: Buffer.from(pubRaw).toString("hex"),
|
|
49
|
+
secretKey: Buffer.from(privRaw).toString("hex")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function signMessage(secretKeyHex, message) {
|
|
53
|
+
const privRaw = Buffer.from(secretKeyHex, "hex");
|
|
54
|
+
const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
|
|
55
|
+
const keyObject = (0, import_crypto.createPrivateKey)({ key: privKeyDer, format: "der", type: "pkcs8" });
|
|
56
|
+
const sig = (0, import_crypto.sign)(null, Buffer.from(message), keyObject);
|
|
57
|
+
return Buffer.from(sig).toString("hex");
|
|
58
|
+
}
|
|
59
|
+
async function register(name, options) {
|
|
60
|
+
if (!name || typeof name !== "string" || name.trim().length === 0) {
|
|
61
|
+
throw new Error("name is required (must be a non-empty string)");
|
|
62
|
+
}
|
|
63
|
+
if (name.length > 64) {
|
|
64
|
+
throw new Error("name must be 64 characters or fewer");
|
|
65
|
+
}
|
|
66
|
+
const url = options?.registryUrl || "https://provenonce.io";
|
|
67
|
+
try {
|
|
68
|
+
new URL(url);
|
|
69
|
+
} catch {
|
|
70
|
+
throw new Error("registryUrl is not a valid URL");
|
|
71
|
+
}
|
|
72
|
+
const headers = { "Content-Type": "application/json" };
|
|
73
|
+
if (options?.registrationSecret) {
|
|
74
|
+
headers["x-registration-secret"] = options.registrationSecret;
|
|
75
|
+
}
|
|
76
|
+
if (options?.parentHash) {
|
|
77
|
+
if (options.parentApiKey) {
|
|
78
|
+
headers["Authorization"] = `Bearer ${options.parentApiKey}`;
|
|
79
|
+
}
|
|
80
|
+
const res2 = await fetch(`${url}/api/v1/register`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify({ name, parent: options.parentHash, ...options.metadata && { metadata: options.metadata } })
|
|
84
|
+
});
|
|
85
|
+
let data2;
|
|
86
|
+
try {
|
|
87
|
+
data2 = await res2.json();
|
|
88
|
+
} catch {
|
|
89
|
+
throw new Error(`Registration failed: ${res2.status} ${res2.statusText} (non-JSON response)`);
|
|
90
|
+
}
|
|
91
|
+
if (!res2.ok) throw new Error(data2.error || "Registration failed");
|
|
92
|
+
return data2;
|
|
93
|
+
}
|
|
94
|
+
if (options?.walletChain === "ethereum") {
|
|
95
|
+
if (!options.walletAddress || !options.walletSignFn) {
|
|
96
|
+
throw new Error("Ethereum registration requires walletAddress and walletSignFn");
|
|
97
|
+
}
|
|
98
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(options.walletAddress)) {
|
|
99
|
+
throw new Error("walletAddress must be a valid Ethereum address (0x + 40 hex chars)");
|
|
100
|
+
}
|
|
101
|
+
const challengeRes = await fetch(`${url}/api/v1/register`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers,
|
|
104
|
+
body: JSON.stringify({ name, action: "challenge", wallet_chain: "ethereum" })
|
|
105
|
+
});
|
|
106
|
+
let challengeData;
|
|
107
|
+
try {
|
|
108
|
+
challengeData = await challengeRes.json();
|
|
109
|
+
} catch {
|
|
110
|
+
throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
111
|
+
}
|
|
112
|
+
if (!challengeRes.ok || !challengeData.nonce) {
|
|
113
|
+
throw new Error(challengeData.error || "Failed to get registration challenge");
|
|
114
|
+
}
|
|
115
|
+
const nonce = challengeData.nonce;
|
|
116
|
+
const message = `provenonce-register-ethereum:${nonce}:${options.walletAddress}:${name}`;
|
|
117
|
+
const walletSignature = await options.walletSignFn(message);
|
|
118
|
+
const registerRes = await fetch(`${url}/api/v1/register`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
name,
|
|
123
|
+
wallet_chain: "ethereum",
|
|
124
|
+
wallet_address: options.walletAddress,
|
|
125
|
+
wallet_signature: walletSignature,
|
|
126
|
+
wallet_nonce: nonce,
|
|
127
|
+
...options.metadata && { metadata: options.metadata }
|
|
128
|
+
})
|
|
129
|
+
});
|
|
130
|
+
let data2;
|
|
131
|
+
try {
|
|
132
|
+
data2 = await registerRes.json();
|
|
133
|
+
} catch {
|
|
134
|
+
throw new Error(`Ethereum registration failed: ${registerRes.status} (non-JSON response)`);
|
|
135
|
+
}
|
|
136
|
+
if (!registerRes.ok) throw new Error(data2.error || "Registration failed");
|
|
137
|
+
data2.wallet = {
|
|
138
|
+
public_key: "",
|
|
139
|
+
secret_key: "",
|
|
140
|
+
address: data2.wallet?.address || options.walletAddress,
|
|
141
|
+
chain: "ethereum"
|
|
142
|
+
};
|
|
143
|
+
return data2;
|
|
144
|
+
}
|
|
145
|
+
if (options?.walletModel === "operator") {
|
|
146
|
+
if (!options.operatorWalletAddress || !options.operatorSignFn) {
|
|
147
|
+
throw new Error("Operator registration requires operatorWalletAddress and operatorSignFn");
|
|
148
|
+
}
|
|
149
|
+
const challengeRes = await fetch(`${url}/api/v1/register`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers,
|
|
152
|
+
body: JSON.stringify({ name, action: "challenge", wallet_model: "operator" })
|
|
153
|
+
});
|
|
154
|
+
let challengeData;
|
|
155
|
+
try {
|
|
156
|
+
challengeData = await challengeRes.json();
|
|
157
|
+
} catch {
|
|
158
|
+
throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
159
|
+
}
|
|
160
|
+
if (!challengeRes.ok || !challengeData.nonce) {
|
|
161
|
+
throw new Error(challengeData.error || "Failed to get registration challenge");
|
|
162
|
+
}
|
|
163
|
+
const nonce = challengeData.nonce;
|
|
164
|
+
const message = `provenonce-register-operator:${nonce}:${options.operatorWalletAddress}:${name}`;
|
|
165
|
+
const walletSignature = await options.operatorSignFn(message);
|
|
166
|
+
const registerRes = await fetch(`${url}/api/v1/register`, {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers,
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
name,
|
|
171
|
+
wallet_model: "operator",
|
|
172
|
+
operator_wallet_address: options.operatorWalletAddress,
|
|
173
|
+
wallet_signature: walletSignature,
|
|
174
|
+
wallet_nonce: nonce,
|
|
175
|
+
...options.metadata && { metadata: options.metadata }
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
let data2;
|
|
179
|
+
try {
|
|
180
|
+
data2 = await registerRes.json();
|
|
181
|
+
} catch {
|
|
182
|
+
throw new Error(`Operator registration failed: ${registerRes.status} (non-JSON response)`);
|
|
183
|
+
}
|
|
184
|
+
if (!registerRes.ok) throw new Error(data2.error || "Registration failed");
|
|
185
|
+
const addr = data2.wallet?.address || data2.wallet?.solana_address || options.operatorWalletAddress;
|
|
186
|
+
data2.wallet = {
|
|
187
|
+
public_key: "",
|
|
188
|
+
secret_key: "",
|
|
189
|
+
solana_address: addr,
|
|
190
|
+
address: addr,
|
|
191
|
+
chain: "solana"
|
|
192
|
+
};
|
|
193
|
+
return data2;
|
|
194
|
+
}
|
|
195
|
+
if (options?.walletModel === "self-custody" || options?.walletSecretKey) {
|
|
196
|
+
let walletKeys;
|
|
197
|
+
if (options?.walletSecretKey) {
|
|
198
|
+
const privRaw = Buffer.from(options.walletSecretKey, "hex");
|
|
199
|
+
const privKeyDer = Buffer.concat([ED25519_PKCS8_PREFIX, privRaw]);
|
|
200
|
+
const keyObject = (0, import_crypto.createPrivateKey)({ key: privKeyDer, format: "der", type: "pkcs8" });
|
|
201
|
+
const pubRaw = keyObject.export({ type: "spki", format: "der" }).subarray(12);
|
|
202
|
+
walletKeys = {
|
|
203
|
+
publicKey: Buffer.from(pubRaw).toString("hex"),
|
|
204
|
+
secretKey: options.walletSecretKey
|
|
205
|
+
};
|
|
206
|
+
} else {
|
|
207
|
+
walletKeys = generateWalletKeypair();
|
|
208
|
+
}
|
|
209
|
+
const challengeRes = await fetch(`${url}/api/v1/register`, {
|
|
210
|
+
method: "POST",
|
|
211
|
+
headers,
|
|
212
|
+
body: JSON.stringify({ name, action: "challenge" })
|
|
213
|
+
});
|
|
214
|
+
let challengeData;
|
|
215
|
+
try {
|
|
216
|
+
challengeData = await challengeRes.json();
|
|
217
|
+
} catch {
|
|
218
|
+
throw new Error(`Registration challenge failed: ${challengeRes.status} (non-JSON response)`);
|
|
219
|
+
}
|
|
220
|
+
if (!challengeRes.ok || !challengeData.nonce) {
|
|
221
|
+
const err = new Error(challengeData.error || "Failed to get registration challenge");
|
|
222
|
+
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
const nonce = challengeData.nonce;
|
|
226
|
+
const message = `provenonce-register:${nonce}:${walletKeys.publicKey}:${name}`;
|
|
227
|
+
const walletSignature = signMessage(walletKeys.secretKey, message);
|
|
228
|
+
const registerRes = await fetch(`${url}/api/v1/register`, {
|
|
229
|
+
method: "POST",
|
|
230
|
+
headers,
|
|
231
|
+
body: JSON.stringify({
|
|
232
|
+
name,
|
|
233
|
+
wallet_public_key: walletKeys.publicKey,
|
|
234
|
+
wallet_signature: walletSignature,
|
|
235
|
+
wallet_nonce: nonce,
|
|
236
|
+
...options?.metadata && { metadata: options.metadata }
|
|
237
|
+
})
|
|
238
|
+
});
|
|
239
|
+
let data2;
|
|
240
|
+
try {
|
|
241
|
+
data2 = await registerRes.json();
|
|
242
|
+
} catch {
|
|
243
|
+
const err = new Error(`Registration failed: ${registerRes.status} (non-JSON response)`);
|
|
244
|
+
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
if (!registerRes.ok) {
|
|
248
|
+
const err = new Error(data2.error || "Registration failed");
|
|
249
|
+
err.walletKeys = { publicKey: walletKeys.publicKey, secretKey: walletKeys.secretKey };
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
const addr = data2.wallet?.address || data2.wallet?.solana_address || "";
|
|
253
|
+
data2.wallet = {
|
|
254
|
+
public_key: walletKeys.publicKey,
|
|
255
|
+
secret_key: walletKeys.secretKey,
|
|
256
|
+
solana_address: addr,
|
|
257
|
+
address: addr,
|
|
258
|
+
chain: "solana"
|
|
259
|
+
};
|
|
260
|
+
return data2;
|
|
261
|
+
}
|
|
262
|
+
const res = await fetch(`${url}/api/v1/register`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers,
|
|
265
|
+
body: JSON.stringify({ name, ...options?.metadata && { metadata: options.metadata } })
|
|
266
|
+
});
|
|
267
|
+
let data;
|
|
268
|
+
try {
|
|
269
|
+
data = await res.json();
|
|
270
|
+
} catch {
|
|
271
|
+
throw new Error(`Registration failed: ${res.status} ${res.statusText} (non-JSON response)`);
|
|
272
|
+
}
|
|
273
|
+
if (!res.ok) throw new Error(data.error || "Registration failed");
|
|
274
|
+
return data;
|
|
38
275
|
}
|
|
39
276
|
var BeatAgent = class {
|
|
40
277
|
constructor(config) {
|
|
@@ -47,6 +284,24 @@ var BeatAgent = class {
|
|
|
47
284
|
this.status = "uninitialized";
|
|
48
285
|
this.heartbeatInterval = null;
|
|
49
286
|
this.globalBeat = 0;
|
|
287
|
+
this.globalAnchorHash = "";
|
|
288
|
+
if (!config.apiKey || typeof config.apiKey !== "string") {
|
|
289
|
+
throw new Error("BeatAgentConfig.apiKey is required (must be a non-empty string)");
|
|
290
|
+
}
|
|
291
|
+
if (!config.registryUrl || typeof config.registryUrl !== "string") {
|
|
292
|
+
throw new Error("BeatAgentConfig.registryUrl is required (must be a non-empty string)");
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
new URL(config.registryUrl);
|
|
296
|
+
} catch {
|
|
297
|
+
throw new Error("BeatAgentConfig.registryUrl is not a valid URL");
|
|
298
|
+
}
|
|
299
|
+
if (config.beatsPerPulse !== void 0 && (!Number.isInteger(config.beatsPerPulse) || config.beatsPerPulse < 1 || config.beatsPerPulse > 1e4)) {
|
|
300
|
+
throw new Error("BeatAgentConfig.beatsPerPulse must be an integer between 1 and 10000");
|
|
301
|
+
}
|
|
302
|
+
if (config.checkinIntervalSec !== void 0 && (!Number.isFinite(config.checkinIntervalSec) || config.checkinIntervalSec < 10 || config.checkinIntervalSec > 86400)) {
|
|
303
|
+
throw new Error("BeatAgentConfig.checkinIntervalSec must be between 10 and 86400");
|
|
304
|
+
}
|
|
50
305
|
this.config = {
|
|
51
306
|
beatsPerPulse: 10,
|
|
52
307
|
checkinIntervalSec: 300,
|
|
@@ -107,21 +362,32 @@ var BeatAgent = class {
|
|
|
107
362
|
* through a specific window of computational time.
|
|
108
363
|
*/
|
|
109
364
|
pulse(count) {
|
|
365
|
+
if (this.status !== "active") {
|
|
366
|
+
throw new Error(`Cannot pulse: agent is ${this.status}. Use resync() if frozen.`);
|
|
367
|
+
}
|
|
368
|
+
if (count !== void 0 && (!Number.isInteger(count) || count < 1 || count > 1e4)) {
|
|
369
|
+
throw new Error("pulse count must be an integer between 1 and 10000");
|
|
370
|
+
}
|
|
371
|
+
return this.computeBeats(count);
|
|
372
|
+
}
|
|
373
|
+
/** Internal beat computation — no status check. Used by both pulse() and resync(). */
|
|
374
|
+
computeBeats(count, onProgress) {
|
|
110
375
|
const n = count || this.config.beatsPerPulse;
|
|
111
376
|
if (!this.latestBeat) {
|
|
112
377
|
throw new Error("Beat chain not initialized. Call init() first.");
|
|
113
378
|
}
|
|
114
|
-
if (this.status !== "active") {
|
|
115
|
-
throw new Error(`Cannot pulse in status '${this.status}'. Use resync() if frozen.`);
|
|
116
|
-
}
|
|
117
379
|
const newBeats = [];
|
|
118
380
|
let prevHash = this.latestBeat.hash;
|
|
119
381
|
let startIndex = this.latestBeat.index + 1;
|
|
120
382
|
const t0 = Date.now();
|
|
383
|
+
const progressInterval = Math.max(1, Math.floor(n / 10));
|
|
121
384
|
for (let i = 0; i < n; i++) {
|
|
122
|
-
const beat = computeBeat(prevHash, startIndex + i, this.difficulty);
|
|
385
|
+
const beat = computeBeat(prevHash, startIndex + i, this.difficulty, void 0, this.globalAnchorHash || void 0);
|
|
123
386
|
newBeats.push(beat);
|
|
124
387
|
prevHash = beat.hash;
|
|
388
|
+
if (onProgress && (i + 1) % progressInterval === 0) {
|
|
389
|
+
onProgress(i + 1, n);
|
|
390
|
+
}
|
|
125
391
|
}
|
|
126
392
|
const elapsed = Date.now() - t0;
|
|
127
393
|
this.chain.push(...newBeats);
|
|
@@ -142,21 +408,26 @@ var BeatAgent = class {
|
|
|
142
408
|
* submit a proof of its Local Beats to the Registry."
|
|
143
409
|
*/
|
|
144
410
|
async checkin() {
|
|
145
|
-
if (!this.latestBeat || this.
|
|
411
|
+
if (!this.latestBeat || this.latestBeat.index <= this.lastCheckinBeat) {
|
|
412
|
+
this.log("No new beats since last check-in. Call pulse() first.");
|
|
146
413
|
return { ok: true, total_beats: this.totalBeats };
|
|
147
414
|
}
|
|
148
415
|
try {
|
|
416
|
+
const fromBeat = this.lastCheckinBeat;
|
|
417
|
+
const toBeat = this.latestBeat.index;
|
|
149
418
|
const spotChecks = [];
|
|
150
|
-
const
|
|
151
|
-
|
|
419
|
+
const toBeatEntry = this.chain.find((b) => b.index === toBeat);
|
|
420
|
+
if (toBeatEntry) {
|
|
421
|
+
spotChecks.push({ index: toBeatEntry.index, hash: toBeatEntry.hash, prev: toBeatEntry.prev, nonce: toBeatEntry.nonce });
|
|
422
|
+
}
|
|
423
|
+
const available = this.chain.filter((b) => b.index > this.lastCheckinBeat && b.index !== toBeat);
|
|
424
|
+
const sampleCount = Math.min(4, available.length);
|
|
152
425
|
for (let i = 0; i < sampleCount; i++) {
|
|
153
426
|
const idx = Math.floor(Math.random() * available.length);
|
|
154
427
|
const beat = available[idx];
|
|
155
428
|
spotChecks.push({ index: beat.index, hash: beat.hash, prev: beat.prev, nonce: beat.nonce });
|
|
156
429
|
available.splice(idx, 1);
|
|
157
430
|
}
|
|
158
|
-
const fromBeat = this.lastCheckinBeat;
|
|
159
|
-
const toBeat = this.latestBeat.index;
|
|
160
431
|
const fromHash = this.chain.find((b) => b.index === fromBeat)?.hash || this.genesisHash;
|
|
161
432
|
const toHash = this.latestBeat.hash;
|
|
162
433
|
const res = await this.api("POST", "/api/v1/agent/checkin", {
|
|
@@ -167,6 +438,7 @@ var BeatAgent = class {
|
|
|
167
438
|
to_hash: toHash,
|
|
168
439
|
beats_computed: toBeat - fromBeat,
|
|
169
440
|
global_anchor: this.globalBeat,
|
|
441
|
+
anchor_hash: this.globalAnchorHash || void 0,
|
|
170
442
|
spot_checks: spotChecks
|
|
171
443
|
}
|
|
172
444
|
});
|
|
@@ -201,7 +473,13 @@ var BeatAgent = class {
|
|
|
201
473
|
throw new Error(`Cannot start heartbeat in status '${this.status}'.`);
|
|
202
474
|
}
|
|
203
475
|
this.log("\u2661 Starting heartbeat...");
|
|
476
|
+
let consecutiveErrors = 0;
|
|
477
|
+
let skipCount = 0;
|
|
204
478
|
this.heartbeatInterval = setInterval(async () => {
|
|
479
|
+
if (skipCount > 0) {
|
|
480
|
+
skipCount--;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
205
483
|
try {
|
|
206
484
|
this.pulse();
|
|
207
485
|
const beatsSinceCheckin = this.latestBeat.index - this.lastCheckinBeat;
|
|
@@ -210,8 +488,12 @@ var BeatAgent = class {
|
|
|
210
488
|
await this.checkin();
|
|
211
489
|
await this.syncGlobal();
|
|
212
490
|
}
|
|
491
|
+
consecutiveErrors = 0;
|
|
213
492
|
} catch (err) {
|
|
493
|
+
consecutiveErrors++;
|
|
214
494
|
this.config.onError(err, "heartbeat");
|
|
495
|
+
skipCount = Math.min(32, Math.pow(2, consecutiveErrors - 1));
|
|
496
|
+
this.log(`Heartbeat error #${consecutiveErrors}, backing off ${skipCount} ticks`);
|
|
215
497
|
}
|
|
216
498
|
}, this.config.checkinIntervalSec * 1e3 / 10);
|
|
217
499
|
}
|
|
@@ -246,16 +528,18 @@ var BeatAgent = class {
|
|
|
246
528
|
const required = challenge.challenge.required_beats;
|
|
247
529
|
this.difficulty = challenge.challenge.difficulty;
|
|
248
530
|
this.log(`Re-sync challenge: compute ${required} beats at D=${this.difficulty}`);
|
|
531
|
+
await this.syncGlobal();
|
|
249
532
|
const startHash = challenge.challenge.start_from_hash;
|
|
250
533
|
const startBeat = challenge.challenge.start_from_beat;
|
|
251
534
|
this.latestBeat = { index: startBeat, hash: startHash, prev: "", timestamp: Date.now() };
|
|
252
535
|
this.chain = [this.latestBeat];
|
|
253
536
|
const t0 = Date.now();
|
|
254
|
-
this.
|
|
537
|
+
this.computeBeats(required);
|
|
255
538
|
const elapsed = Date.now() - t0;
|
|
256
539
|
this.log(`Re-sync beats computed in ${elapsed}ms`);
|
|
257
540
|
const proof = await this.api("POST", "/api/v1/agent/resync", {
|
|
258
541
|
action: "prove",
|
|
542
|
+
challenge_nonce: challenge.challenge.nonce,
|
|
259
543
|
proof: {
|
|
260
544
|
from_beat: startBeat,
|
|
261
545
|
to_beat: this.latestBeat.index,
|
|
@@ -263,7 +547,15 @@ var BeatAgent = class {
|
|
|
263
547
|
to_hash: this.latestBeat.hash,
|
|
264
548
|
beats_computed: required,
|
|
265
549
|
global_anchor: challenge.challenge.sync_to_global,
|
|
266
|
-
|
|
550
|
+
anchor_hash: this.globalAnchorHash || void 0,
|
|
551
|
+
spot_checks: (() => {
|
|
552
|
+
const toBeatEntry = this.chain.find((b) => b.index === this.latestBeat.index);
|
|
553
|
+
const available = this.chain.filter((b) => b.index !== this.latestBeat.index && b.index > startBeat);
|
|
554
|
+
const step = Math.max(1, Math.ceil(available.length / 5));
|
|
555
|
+
const others = available.filter((_, i) => i % step === 0).slice(0, 4);
|
|
556
|
+
const checks = toBeatEntry ? [toBeatEntry, ...others] : others;
|
|
557
|
+
return checks.map((b) => ({ index: b.index, hash: b.hash, prev: b.prev, nonce: b.nonce }));
|
|
558
|
+
})()
|
|
267
559
|
}
|
|
268
560
|
});
|
|
269
561
|
if (proof.ok) {
|
|
@@ -286,6 +578,14 @@ var BeatAgent = class {
|
|
|
286
578
|
*/
|
|
287
579
|
async requestSpawn(childName, childHash) {
|
|
288
580
|
try {
|
|
581
|
+
if (childName !== void 0) {
|
|
582
|
+
if (typeof childName !== "string" || childName.trim().length === 0) {
|
|
583
|
+
throw new Error("childName must be a non-empty string");
|
|
584
|
+
}
|
|
585
|
+
if (childName.length > 64) {
|
|
586
|
+
throw new Error("childName must be 64 characters or fewer");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
289
589
|
const res = await this.api("POST", "/api/v1/agent/spawn", {
|
|
290
590
|
child_name: childName,
|
|
291
591
|
child_hash: childHash
|
|
@@ -330,10 +630,14 @@ var BeatAgent = class {
|
|
|
330
630
|
// ── INTERNALS ──
|
|
331
631
|
async syncGlobal() {
|
|
332
632
|
try {
|
|
333
|
-
const
|
|
633
|
+
const controller = new AbortController();
|
|
634
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
635
|
+
const res = await fetch(`${this.config.registryUrl}/api/v1/beat/anchor`, { signal: controller.signal });
|
|
636
|
+
clearTimeout(timeout);
|
|
334
637
|
const data = await res.json();
|
|
335
638
|
if (data.anchor) {
|
|
336
639
|
this.globalBeat = data.anchor.beat_index;
|
|
640
|
+
this.globalAnchorHash = data.anchor.hash || "";
|
|
337
641
|
if (data.anchor.difficulty) this.difficulty = data.anchor.difficulty;
|
|
338
642
|
this.log(`Synced to global beat ${this.globalBeat} (D=${this.difficulty})`);
|
|
339
643
|
}
|
|
@@ -347,23 +651,52 @@ var BeatAgent = class {
|
|
|
347
651
|
this.totalBeats = res.total_beats;
|
|
348
652
|
this.genesisHash = res.genesis_hash;
|
|
349
653
|
this.status = res.status;
|
|
654
|
+
this.difficulty = res.difficulty || this.difficulty;
|
|
655
|
+
this.lastCheckinBeat = res.last_checkin_beat || 0;
|
|
656
|
+
if (!this.latestBeat && this.genesisHash) {
|
|
657
|
+
this.latestBeat = {
|
|
658
|
+
index: res.latest_beat || this.totalBeats,
|
|
659
|
+
hash: res.latest_hash || this.genesisHash,
|
|
660
|
+
prev: "0".repeat(64),
|
|
661
|
+
timestamp: Date.now()
|
|
662
|
+
};
|
|
663
|
+
this.chain = [this.latestBeat];
|
|
664
|
+
}
|
|
350
665
|
}
|
|
351
666
|
return res;
|
|
352
667
|
}
|
|
353
668
|
async api(method, path, body) {
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
669
|
+
const controller = new AbortController();
|
|
670
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
671
|
+
try {
|
|
672
|
+
const res = await fetch(`${this.config.registryUrl}${path}`, {
|
|
673
|
+
method,
|
|
674
|
+
headers: {
|
|
675
|
+
"Content-Type": "application/json",
|
|
676
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
677
|
+
},
|
|
678
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
679
|
+
signal: controller.signal
|
|
680
|
+
});
|
|
681
|
+
let data;
|
|
682
|
+
try {
|
|
683
|
+
data = await res.json();
|
|
684
|
+
} catch {
|
|
685
|
+
throw new Error(`API error: ${res.status} non-JSON response from ${path}`);
|
|
686
|
+
}
|
|
687
|
+
if (!res.ok && !data.ok && !data.already_initialized && !data.eligible) {
|
|
688
|
+
const serverMsg = typeof data.error === "string" ? data.error : `API error ${res.status}`;
|
|
689
|
+
throw new Error(serverMsg);
|
|
690
|
+
}
|
|
691
|
+
return data;
|
|
692
|
+
} catch (err) {
|
|
693
|
+
if (err.name === "AbortError") {
|
|
694
|
+
throw new Error(`Request timeout: ${method} ${path}`);
|
|
695
|
+
}
|
|
696
|
+
throw err;
|
|
697
|
+
} finally {
|
|
698
|
+
clearTimeout(timeout);
|
|
365
699
|
}
|
|
366
|
-
return data;
|
|
367
700
|
}
|
|
368
701
|
log(msg) {
|
|
369
702
|
if (this.config.verbose) {
|
|
@@ -371,12 +704,18 @@ var BeatAgent = class {
|
|
|
371
704
|
}
|
|
372
705
|
}
|
|
373
706
|
};
|
|
374
|
-
function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3) {
|
|
707
|
+
function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3, anchorHash) {
|
|
708
|
+
if (!startHash || typeof startHash !== "string") {
|
|
709
|
+
throw new Error("computeBeatsLite: startHash must be a non-empty string");
|
|
710
|
+
}
|
|
711
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
712
|
+
throw new Error("computeBeatsLite: count must be a positive integer");
|
|
713
|
+
}
|
|
375
714
|
const t0 = Date.now();
|
|
376
715
|
let prev = startHash;
|
|
377
716
|
let lastBeat = null;
|
|
378
717
|
for (let i = 0; i < count; i++) {
|
|
379
|
-
lastBeat = computeBeat(prev, startIndex + i, difficulty);
|
|
718
|
+
lastBeat = computeBeat(prev, startIndex + i, difficulty, void 0, anchorHash);
|
|
380
719
|
prev = lastBeat.hash;
|
|
381
720
|
}
|
|
382
721
|
return { lastBeat, elapsed: Date.now() - t0 };
|
|
@@ -385,6 +724,8 @@ function computeBeatsLite(startHash, startIndex, count, difficulty = 1e3) {
|
|
|
385
724
|
0 && (module.exports = {
|
|
386
725
|
BeatAgent,
|
|
387
726
|
computeBeat,
|
|
388
|
-
computeBeatsLite
|
|
727
|
+
computeBeatsLite,
|
|
728
|
+
generateWalletKeypair,
|
|
729
|
+
register
|
|
389
730
|
});
|
|
390
731
|
//# sourceMappingURL=index.js.map
|