@rodit/rodit-auth-be 9.11.14

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.
@@ -0,0 +1,1715 @@
1
+ /**
2
+ * Service for interacting with the blockchain network
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const config = require("../../services/configsdk");
8
+ const logger = require("../../services/logger");
9
+ const { createLogContext, logErrorWithMetrics } = logger;
10
+
11
+ const baseModuleContext = createLogContext("BlockchainService", "module", {
12
+ loadedAt: new Date().toISOString()
13
+ });
14
+
15
+ logger.debugWithContext("Loading blockchainservice.js module", baseModuleContext);
16
+
17
+ /**
18
+ * Constants and Configuration
19
+ */
20
+ const CONSTANTS = {
21
+ NEAR_CONTRACT_ID: config.get("NEAR_CONTRACT_ID"),
22
+ RODIT_ID_SZ: 128,
23
+ RODIT_ID_PK_SZ: 32,
24
+ RODIT_ID_SIGNATURE_SZ: 64,
25
+ ED25519_KEY_SZ: 64,
26
+ };
27
+
28
+ function getNearRpcUrl() {
29
+ return config.get("NEAR_RPC_URL");
30
+ }
31
+ // Simple in-memory TTL cache for RPC results
32
+ // Single TTL setting for all RPC caches (in milliseconds)
33
+ // Default value is defined centrally in configsdk.FALLBACK_DEFAULTS
34
+ const NEAR_RPC_CACHE_TTL = parseInt(config.get("NEAR_RPC_CACHE_TTL"));
35
+
36
+ const _rpcCache = new Map();
37
+ function _cacheGet(key) {
38
+ const entry = _rpcCache.get(key);
39
+ if (!entry) return undefined;
40
+ if (entry.expiresAt && entry.expiresAt <= Date.now()) {
41
+ _rpcCache.delete(key);
42
+ return undefined;
43
+ }
44
+ return entry.value;
45
+ }
46
+ function _cacheSet(key, value, ttlMs) {
47
+ const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
48
+ _rpcCache.set(key, { value, expiresAt });
49
+ }
50
+
51
+ /** Coalesce concurrent NEAR block timestamp RPCs (same cache key). Avoids divergent "chain now" values in parallel login_server calls and intermittent RODIT_NOT_LIVE at validity boundaries. */
52
+ let _nearTimestampInflightPromise = null;
53
+ /**
54
+ * Data models for RODiT Authentication
55
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
56
+ */
57
+
58
+ /**
59
+ * RODiT token class that represents a Resource Ownership and Digital Identity Token
60
+ */
61
+ class RODiT {
62
+ constructor() {
63
+ this.token_id = "";
64
+ this.owner_id = "";
65
+ this.metadata = {
66
+ openapijson_url: "",
67
+ not_after: "",
68
+ not_before: "",
69
+ max_requests: "",
70
+ maxrq_window: "",
71
+ webhook_url: "",
72
+ webhook_cidr: "",
73
+ userselected_dn: "",
74
+ allowed_cidr: "",
75
+ allowed_iso3166list: "",
76
+ jwt_duration: "",
77
+ permissioned_routes: "",
78
+ subjectuniqueidentifier_url: "",
79
+ serviceprovider_id: "",
80
+ serviceprovider_signature: "",
81
+ };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Payload class for NEP-413 standard
87
+ */
88
+ class PayloadNEP413 {
89
+ constructor(props) {
90
+ this.tag = props.tag || 2147484061;
91
+ this.message = props.message;
92
+ if (props.nonce instanceof Uint8Array) {
93
+ if (props.nonce.length !== 32) {
94
+ throw new Error("Nonce must be exactly 32 bytes");
95
+ }
96
+ this.nonce = props.nonce;
97
+ } else if (
98
+ Array.isArray(props.nonce) ||
99
+ (typeof props.nonce === "object" && props.nonce !== null)
100
+ ) {
101
+ const nonceArray = Array.isArray(props.nonce)
102
+ ? props.nonce
103
+ : Object.values(props.nonce);
104
+ if (nonceArray.length !== 32) {
105
+ throw new Error("Nonce must be exactly 32 bytes");
106
+ }
107
+ this.nonce = new Uint8Array(nonceArray);
108
+ } else {
109
+ throw new Error(
110
+ "Invalid nonce format - must be Uint8Array or convertible to Uint8Array"
111
+ );
112
+ }
113
+ this.recipient = props.recipient;
114
+ this.callbackUrl = props.callbackUrl;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Schema for NEP-413 payload in Borsh format
120
+ */
121
+ const PayloadNEP413Schema = {
122
+ struct: {
123
+ tag: "u32",
124
+ message: "string",
125
+ nonce: { array: { type: "u8", len: 32 } },
126
+ recipient: "string",
127
+ callbackUrl: { option: "string" },
128
+ },
129
+ };
130
+
131
+ /**
132
+ * Service for interacting with the blockchain network
133
+ */
134
+
135
+ async function nearorg_rpc_timestamp() {
136
+ const rpcUrl = getNearRpcUrl();
137
+ const cacheKey = `ts:${rpcUrl}`;
138
+ const cached = _cacheGet(cacheKey);
139
+ if (cached !== undefined) {
140
+ const requestId = ulid();
141
+ const baseContext = createLogContext(
142
+ "BlockchainService",
143
+ "nearorg_rpc_timestamp",
144
+ {
145
+ requestId,
146
+ rpcUrl
147
+ }
148
+ );
149
+ logger.debugWithContext("Cache hit for blockchain timestamp", baseContext);
150
+ return cached;
151
+ }
152
+
153
+ if (!_nearTimestampInflightPromise) {
154
+ _nearTimestampInflightPromise = nearorg_rpc_timestamp_fetchUncached(cacheKey, rpcUrl).finally(() => {
155
+ _nearTimestampInflightPromise = null;
156
+ });
157
+ }
158
+
159
+ return _nearTimestampInflightPromise;
160
+ }
161
+
162
+ async function nearorg_rpc_timestamp_fetchUncached(cacheKey, rpcUrl = getNearRpcUrl()) {
163
+ const requestId = ulid();
164
+ const startTime = Date.now();
165
+
166
+ const baseContext = createLogContext(
167
+ "BlockchainService",
168
+ "nearorg_rpc_timestamp",
169
+ {
170
+ requestId,
171
+ rpcUrl
172
+ }
173
+ );
174
+
175
+ logger.debugWithContext("Fetching blockchain timestamp", baseContext);
176
+
177
+ try {
178
+ const jsonData = {
179
+ jsonrpc: "2.0",
180
+ id: "dontcare",
181
+ method: "block",
182
+ params: {
183
+ finality: "final",
184
+ },
185
+ };
186
+
187
+ const fetchStartTime = Date.now();
188
+ const response = await fetch(rpcUrl, {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ },
193
+ body: JSON.stringify(jsonData),
194
+ });
195
+ const fetchDuration = Date.now() - fetchStartTime;
196
+
197
+ logger.debugWithContext("RPC response received", {
198
+ ...baseContext,
199
+ statusCode: response.status,
200
+ fetchDuration
201
+ });
202
+
203
+ if (!response.ok) {
204
+ const duration = Date.now() - startTime;
205
+
206
+ // Add metric for failed RPC calls
207
+ logger.metric("near_rpc_calls", fetchDuration, {
208
+ result: "http_error",
209
+ status_code: response.status,
210
+ method: "block",
211
+ });
212
+
213
+ logErrorWithMetrics(
214
+ "HTTP error from blockchain RPC",
215
+ {
216
+ ...baseContext,
217
+ statusCode: response.status,
218
+ statusText: response.statusText,
219
+ duration
220
+ },
221
+ new Error(`HTTP error! status: ${response.status}`),
222
+ "near_rpc_http_error",
223
+ {
224
+ result: "error",
225
+ status_code: response.status,
226
+ method: "block",
227
+ duration
228
+ }
229
+ );
230
+
231
+ throw new Error(`HTTP error! status: ${response.status}`);
232
+ }
233
+
234
+ const parseStartTime = Date.now();
235
+ const parsedJson = await response.json();
236
+ const parseDuration = Date.now() - parseStartTime;
237
+
238
+ logger.debugWithContext("RPC response parsed", {
239
+ ...baseContext,
240
+ parseDuration
241
+ });
242
+
243
+ if (parsedJson.error) {
244
+ const duration = Date.now() - startTime;
245
+
246
+ // Add metric for RPC errors
247
+ logger.metric("near_rpc_errors", 1, {
248
+ error_code: parsedJson.error.code || "unknown",
249
+ method: "block",
250
+ });
251
+
252
+ logErrorWithMetrics(
253
+ "RPC error response",
254
+ {
255
+ ...baseContext,
256
+ rpcError: parsedJson.error.message,
257
+ rpcErrorCode: parsedJson.error.code,
258
+ duration
259
+ },
260
+ new Error(`Error 017: ${parsedJson.error.message}`),
261
+ "near_rpc_error_response",
262
+ {
263
+ result: "error",
264
+ error_code: parsedJson.error.code || "unknown",
265
+ method: "block",
266
+ duration
267
+ }
268
+ );
269
+
270
+ throw new Error(`Error 017: ${parsedJson.error.message}`);
271
+ }
272
+
273
+ const timestamp = parsedJson.result?.header?.timestamp;
274
+ const totalDuration = Date.now() - startTime;
275
+
276
+ if (timestamp === undefined || timestamp === null || timestamp === "") {
277
+ const error = new Error("Missing blockchain timestamp in RPC response");
278
+ logErrorWithMetrics(
279
+ "RPC response missing timestamp",
280
+ {
281
+ ...baseContext,
282
+ duration: totalDuration,
283
+ fetchDuration,
284
+ parseDuration,
285
+ },
286
+ error,
287
+ "near_rpc_missing_timestamp",
288
+ {
289
+ result: "error",
290
+ method: "block",
291
+ duration: totalDuration,
292
+ }
293
+ );
294
+ throw error;
295
+ }
296
+
297
+ logger.infoWithContext("Blockchain timestamp fetched successfully", {
298
+ ...baseContext,
299
+ duration: totalDuration,
300
+ fetchDuration,
301
+ parseDuration,
302
+ timestamp: timestamp.toString()
303
+ });
304
+
305
+ // Add metric for successful RPC calls
306
+ logger.metric("near_rpc_calls", totalDuration, {
307
+ result: "success",
308
+ method: "block",
309
+ });
310
+ // Store in cache using unified TTL setting
311
+ const tsValue = timestamp.toString();
312
+ _cacheSet(cacheKey, tsValue, NEAR_RPC_CACHE_TTL);
313
+ return tsValue;
314
+ } catch (error) {
315
+ const duration = Date.now() - startTime;
316
+
317
+ // Add metrics for timestamp errors
318
+ logger.metric("near_rpc_timestamp_errors", 1, {
319
+ error_type: error.name || "Unknown",
320
+ });
321
+
322
+ logErrorWithMetrics(
323
+ "Error fetching blockchain timestamp",
324
+ {
325
+ ...baseContext,
326
+ duration,
327
+ rpcUrl
328
+ },
329
+ error,
330
+ "near_rpc_timestamp",
331
+ {
332
+ result: "error",
333
+ error_type: error.name || "Unknown",
334
+ duration
335
+ }
336
+ );
337
+
338
+ throw error;
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Fetches a RODiT token by ID from the NEAR blockchain
344
+ *
345
+ * @param {string} roditid - RODiT token ID to fetch
346
+ * @returns {Promise<RODiT>} RODiT token object
347
+ */
348
+ async function nearorg_rpc_tokenfromroditid(roditid) {
349
+ const requestId = ulid();
350
+ const startTime = Date.now();
351
+
352
+ const baseContext = createLogContext(
353
+ "BlockchainService",
354
+ "nearorg_rpc_tokenfromroditid",
355
+ {
356
+ requestId,
357
+ roditId: roditid,
358
+ nearContractId: CONSTANTS.NEAR_CONTRACT_ID,
359
+ nearRpcUrl: getNearRpcUrl()
360
+ }
361
+ );
362
+
363
+ // Security check: Handle null, undefined, or invalid roditid
364
+ // This is important for security tests that intentionally send invalid tokens
365
+ if (!roditid) {
366
+ logger.debugWithContext("Attempted to fetch RODiT with null/undefined ID", {
367
+ ...baseContext,
368
+ result: 'failure',
369
+ reason: 'Null or undefined RODiT'
370
+ });
371
+ // Return an empty RODiT object instead of throwing an error
372
+ // This allows the authentication flow to continue and properly reject the invalid token
373
+ return new RODiT();
374
+ }
375
+
376
+ logger.infoWithContext("Fetching RODiT token by ID", {
377
+ ...baseContext,
378
+ result: 'call',
379
+ reason: 'Fetch RODiT token by ID requested'
380
+ }); // Function call log
381
+
382
+ // Cache check
383
+ const cacheKey = `rodit_by_id:${CONSTANTS.NEAR_CONTRACT_ID}:${roditid}`;
384
+ const cachedRodit = _cacheGet(cacheKey);
385
+ if (cachedRodit) {
386
+ logger.debugWithContext("Cache hit for RODiT token by ID", {
387
+ ...baseContext,
388
+ cached: true
389
+ });
390
+ return cachedRodit;
391
+ }
392
+
393
+ try {
394
+ const args = { token_id: roditid };
395
+ const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");
396
+
397
+ const json_data = {
398
+ jsonrpc: "2.0",
399
+ id: CONSTANTS.NEAR_CONTRACT_ID,
400
+ method: "query",
401
+ params: {
402
+ request_type: "call_function",
403
+ finality: "final",
404
+ account_id: CONSTANTS.NEAR_CONTRACT_ID,
405
+ method_name: "rodit_token",
406
+ args_base64: argsBase64,
407
+ },
408
+ };
409
+
410
+ const fetchStartTime = Date.now();
411
+ const response = await fetch(getNearRpcUrl(), {
412
+ method: "POST",
413
+ headers: { "Content-Type": "application/json" },
414
+ body: JSON.stringify(json_data),
415
+ });
416
+ const fetchDuration = Date.now() - fetchStartTime;
417
+
418
+ logger.debugWithContext("RPC response received", {
419
+ ...baseContext,
420
+ statusCode: response.status,
421
+ fetchDuration
422
+ });
423
+
424
+ if (!response.ok) {
425
+ const duration = Date.now() - startTime;
426
+
427
+ // Add metric for failed RPC calls
428
+ logger.metric("near_rpc_calls", fetchDuration, {
429
+ result: "failure",
430
+ reason: `HTTP error from blockchain RPC: status ${response.status}`,
431
+ status_code: response.status,
432
+ method: "rodit_token",
433
+ });
434
+
435
+ logErrorWithMetrics(
436
+ "HTTP error from blockchain RPC",
437
+ {
438
+ ...baseContext,
439
+ statusCode: response.status,
440
+ duration,
441
+ result: 'failure',
442
+ reason: `HTTP error from blockchain RPC: status ${response.status}`
443
+ },
444
+ new Error(`HTTP error! status: ${response.status}`),
445
+ "near_rpc_http_error",
446
+ {
447
+ result: "failure",
448
+ reason: `HTTP error from blockchain RPC: status ${response.status}`,
449
+ status_code: response.status,
450
+ method: "rodit_token",
451
+ duration
452
+ }
453
+ );
454
+
455
+ return new RODiT();
456
+ }
457
+
458
+ const parseStartTime = Date.now();
459
+ const responseText = await response.text();
460
+ const parsedJson = JSON.parse(responseText);
461
+ const parseDuration = Date.now() - parseStartTime;
462
+
463
+ logger.debugWithContext("RPC response parsed", {
464
+ ...baseContext,
465
+ parseDuration,
466
+ hasResult: !!parsedJson.result
467
+ });
468
+
469
+ if (parsedJson.result && parsedJson.result.error) {
470
+ const duration = Date.now() - startTime;
471
+
472
+ // Add metric for WASM errors
473
+ logger.metric("near_rpc_wasm_errors", 1, {
474
+ method: "rodit_token",
475
+ rodit_id: roditid,
476
+ result: 'failure',
477
+ reason: `WASM execution error: ${parsedJson.result.error}`
478
+ });
479
+
480
+ logErrorWithMetrics(
481
+ "WASM execution error",
482
+ {
483
+ ...baseContext,
484
+ wasmError: parsedJson.result.error,
485
+ duration,
486
+ result: 'failure',
487
+ reason: `WASM execution error: ${parsedJson.result.error}`
488
+ },
489
+ new Error(`WASM execution error: ${parsedJson.result.error}`),
490
+ "near_rpc_wasm_error",
491
+ {
492
+ method: "rodit_token",
493
+ rodit_id: roditid,
494
+ duration,
495
+ result: 'failure',
496
+ reason: `WASM execution error: ${parsedJson.result.error}`
497
+ }
498
+ );
499
+
500
+ return new RODiT();
501
+ }
502
+
503
+ const resultArray = parsedJson.result.result;
504
+ if (!Array.isArray(resultArray)) {
505
+ const duration = Date.now() - startTime;
506
+
507
+ // Add metric for format errors
508
+ logger.metric("near_rpc_format_errors", 1, {
509
+ method: "rodit_token",
510
+ rodit_id: roditid,
511
+ error_type: "invalid_array",
512
+ result: 'failure',
513
+ reason: 'Invalid result format: not an array'
514
+ });
515
+
516
+ logErrorWithMetrics(
517
+ "Invalid result format",
518
+ {
519
+ ...baseContext,
520
+ resultType: typeof resultArray,
521
+ duration,
522
+ result: 'failure',
523
+ reason: 'Invalid result format: not an array'
524
+ },
525
+ new Error("Result is not an array"),
526
+ "near_rpc_format_error",
527
+ {
528
+ method: "rodit_token",
529
+ rodit_id: roditid,
530
+ error_type: "invalid_array",
531
+ duration,
532
+ result: 'failure',
533
+ reason: 'Invalid result format: not an array'
534
+ }
535
+ );
536
+
537
+ return new RODiT();
538
+ }
539
+
540
+ const decodeStartTime = Date.now();
541
+ const resultString = new TextDecoder().decode(new Uint8Array(resultArray));
542
+ let parsed;
543
+
544
+ try {
545
+ parsed = JSON.parse(resultString);
546
+ } catch (error) {
547
+ logErrorWithMetrics(
548
+ "Failed to parse RODiT data",
549
+ {
550
+ ...baseContext,
551
+ error: error.message,
552
+ result: 'failure',
553
+ reason: `Failed to parse RODiT data: ${error.message}`
554
+ },
555
+ error,
556
+ "rodit_parse_error",
557
+ {
558
+ error_type: error.name || "Unknown",
559
+ rodit_id: roditid,
560
+ result: 'failure',
561
+ reason: `Failed to parse RODiT data: ${error.message}`
562
+ }
563
+ );
564
+ return new RODiT();
565
+ }
566
+
567
+ const decodeDuration = Date.now() - decodeStartTime;
568
+
569
+ logger.debugWithContext("RODiT data decoded", {
570
+ ...baseContext,
571
+ decodeDuration,
572
+ isNull: parsed === null,
573
+ hasTokenId: parsed && !!parsed.token_id,
574
+ parsedType: typeof parsed
575
+ }); // Context-only debug log, does not expose secrets
576
+
577
+ const rodit = new RODiT();
578
+
579
+ // Handle Option<JsonToken> return type - null means token not found
580
+ if (parsed === null) {
581
+ logger.warnWithContext("RODiT token not found on blockchain", {
582
+ ...baseContext,
583
+ targetTokenId: roditid,
584
+ result: 'not_found',
585
+ reason: 'RODiT token does not exist on blockchain'
586
+ });
587
+ // Return empty RODiT object - this will cause authentication to fail properly
588
+ return rodit;
589
+ }
590
+
591
+ if (parsed && typeof parsed === 'object') {
592
+ // Debug logging for owner_id issue investigation
593
+ logger.debugWithContext("RAW RODiT data from blockchain", {
594
+ ...baseContext,
595
+ parsedData: parsed,
596
+ parsedOwnerIdType: typeof parsed.owner_id,
597
+ parsedKeys: Object.keys(parsed),
598
+ hasMetadata: !!parsed.metadata,
599
+ metadataServiceProviderId: parsed.metadata?.serviceprovider_id
600
+ });
601
+
602
+ Object.assign(rodit, parsed);
603
+
604
+ // Debug logging after assignment
605
+ logger.debugWithContext("RODiT object after assignment", {
606
+ ...baseContext,
607
+ roditOwnerId: rodit.owner_id,
608
+ roditOwnerIdType: typeof rodit.owner_id,
609
+ roditTokenId: rodit.token_id,
610
+ roditMetadata: rodit.metadata
611
+ });
612
+ } else {
613
+ logger.warnWithContext("Invalid RODiT data format", {
614
+ ...baseContext,
615
+ parsedType: typeof parsed,
616
+ parsedValue: parsed,
617
+ result: 'failure',
618
+ reason: 'Invalid RODiT data format from blockchain'
619
+ });
620
+ }
621
+
622
+ const totalDuration = Date.now() - startTime;
623
+ const hasValidData = !!rodit.token_id && !!rodit.owner_id;
624
+
625
+ logger.infoWithContext("RODiT token fetched", {
626
+ ...baseContext,
627
+ duration: totalDuration,
628
+ retrieved: hasValidData,
629
+ fetchDuration,
630
+ parseDuration,
631
+ decodeDuration,
632
+ result: 'success',
633
+ reason: hasValidData ? 'RODiT token successfully fetched' : 'RODiT token fetch returned empty or incomplete data'
634
+ });
635
+
636
+ // Add metrics for successful RPC calls
637
+ logger.metric("near_rpc_calls", totalDuration, {
638
+ result: "success",
639
+ reason: hasValidData ? 'RODiT token successfully fetched' : 'RODiT token fetch returned empty or incomplete data',
640
+ method: "rodit_token",
641
+ data_found: hasValidData ? "true" : "false"
642
+ });
643
+ // Cache successful lookups only
644
+ if (hasValidData) {
645
+ _cacheSet(cacheKey, rodit, NEAR_RPC_CACHE_TTL);
646
+ }
647
+ return rodit;
648
+ } catch (error) {
649
+ const duration = Date.now() - startTime;
650
+
651
+ // Add metrics for token fetch errors
652
+ logger.metric("near_rpc_token_errors", 1, {
653
+ error_type: error.name || "Unknown",
654
+ rodit_id: roditid,
655
+ result: 'failure',
656
+ reason: `Failed to fetch RODiT token: ${error.message}`
657
+ });
658
+
659
+ logErrorWithMetrics(
660
+ "Failed to fetch RODiT token",
661
+ {
662
+ ...baseContext,
663
+ duration,
664
+ result: 'failure',
665
+ reason: `Failed to fetch RODiT token: ${error.message}`
666
+ },
667
+ error,
668
+ "near_rpc_token_fetch",
669
+ {
670
+ result: 'failure',
671
+ reason: `Failed to fetch RODiT token: ${error.message}`,
672
+ error_type: error.name || "Unknown",
673
+ rodit_id: roditid,
674
+ duration
675
+ }
676
+ );
677
+
678
+ return new RODiT();
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Checks account state on the blockchain
684
+ *
685
+ * @param {string} accountId - Account ID to check
686
+ * @returns {Promise<boolean>} Whether the account exists
687
+ */
688
+ async function nearorg_rpc_state(accountId) {
689
+ const requestId = ulid();
690
+ const startTime = Date.now();
691
+
692
+ const baseContext = createLogContext(
693
+ "BlockchainService",
694
+ "nearorg_rpc_state",
695
+ {
696
+ requestId,
697
+ accountId,
698
+ contractId: CONSTANTS.NEAR_CONTRACT_ID
699
+ }
700
+ );
701
+
702
+ logger.infoWithContext("Checking account state on blockchain", {
703
+ ...baseContext,
704
+ result: 'call',
705
+ reason: 'Account state check requested'
706
+ }); // Function call log
707
+
708
+ // Cache check
709
+ const cacheKey = `state:${accountId}`;
710
+ const cachedState = _cacheGet(cacheKey);
711
+ if (cachedState !== undefined) {
712
+ logger.debugWithContext("Cache hit for account state", {
713
+ ...baseContext,
714
+ cachedState
715
+ });
716
+ return cachedState;
717
+ }
718
+
719
+ try {
720
+ const jsonData = {
721
+ jsonrpc: "2.0",
722
+ id: CONSTANTS.NEAR_CONTRACT_ID,
723
+ method: "query",
724
+ params: {
725
+ request_type: "view_account",
726
+ finality: "final",
727
+ account_id: accountId
728
+ }
729
+ };
730
+
731
+ const response = await fetch(getNearRpcUrl(), {
732
+ method: "POST",
733
+ headers: {
734
+ "Content-Type": "application/json",
735
+ },
736
+ body: JSON.stringify(jsonData),
737
+ });
738
+
739
+ const responseText = await response.json();
740
+
741
+ if (JSON.stringify(responseText).includes("does not exist while viewing")) {
742
+ const duration = Date.now() - startTime;
743
+
744
+ logger.warnWithContext("Account does not exist in blockchain", {
745
+ ...baseContext,
746
+ duration,
747
+ needsFunding: true,
748
+ result: 'failure',
749
+ reason: 'Account does not exist in blockchain'
750
+ });
751
+
752
+ // Emit metrics for dashboards
753
+ logger.metric("account_state_check_duration_ms", duration, {
754
+ component: "BlockchainService",
755
+ success: false,
756
+ accountExists: false,
757
+ result: 'failure',
758
+ reason: 'Account does not exist in blockchain'
759
+ });
760
+ logger.metric("non_existent_accounts_total", 1, {
761
+ component: "BlockchainService",
762
+ accountId,
763
+ result: 'failure',
764
+ reason: 'Account does not exist in blockchain'
765
+ });
766
+ _cacheSet(cacheKey, false, NEAR_RPC_CACHE_TTL);
767
+ return false;
768
+ }
769
+
770
+ // If account exists
771
+ const duration = Date.now() - startTime;
772
+ logger.infoWithContext("Account exists in blockchain", {
773
+ ...baseContext,
774
+ duration,
775
+ result: 'success',
776
+ reason: 'Account exists in blockchain'
777
+ });
778
+ logger.metric("account_state_check_duration_ms", duration, {
779
+ component: "BlockchainService",
780
+ success: true,
781
+ accountExists: true,
782
+ result: 'success',
783
+ reason: 'Account exists in blockchain'
784
+ });
785
+ _cacheSet(cacheKey, true, NEAR_RPC_CACHE_TTL);
786
+ return true;
787
+ } catch (error) {
788
+ const duration = Date.now() - startTime;
789
+ logger.metric("account_state_check_duration_ms", duration, {
790
+ component: "BlockchainService",
791
+ success: false,
792
+ accountExists: false,
793
+ result: 'failure',
794
+ reason: error.message || 'Unknown error during account state check'
795
+ });
796
+ logErrorWithMetrics(
797
+ "Error checking account state on blockchain",
798
+ {
799
+ ...baseContext,
800
+ duration,
801
+ result: 'failure',
802
+ reason: error.message || 'Unknown error during account state check'
803
+ },
804
+ error,
805
+ "account_state_check",
806
+ {
807
+ result: 'failure',
808
+ reason: error.message || 'Unknown error during account state check',
809
+ error_type: error.constructor?.name || 'Error',
810
+ duration
811
+ }
812
+ );
813
+ return false;
814
+ }
815
+ }
816
+
817
+ /**
818
+ * Fetches RODiT tokens for an account
819
+ *
820
+ * @param {string} account_id - Account ID to fetch tokens for
821
+ * @returns {Promise<RODiT>} First RODiT token for the account
822
+ */
823
+ async function nearorg_rpc_tokensfromaccountid(account_id) {
824
+ const requestId = ulid();
825
+ const startTime = Date.now();
826
+
827
+ const baseContext = createLogContext(
828
+ "BlockchainService",
829
+ "nearorg_rpc_tokensfromaccountid",
830
+ {
831
+ requestId,
832
+ accountId: account_id,
833
+ contractId: CONSTANTS.NEAR_CONTRACT_ID
834
+ }
835
+ );
836
+
837
+ logger.infoWithContext("Fetching RODiT tokens for account", {
838
+ ...baseContext,
839
+ result: 'call',
840
+ reason: 'Fetch RODiT tokens for account requested'
841
+ }); // Function call log
842
+
843
+ // Cache check
844
+ const cacheKey = `tokens_by_account:${CONSTANTS.NEAR_CONTRACT_ID}:${account_id}`;
845
+ const cachedTokens = _cacheGet(cacheKey);
846
+ if (cachedTokens) {
847
+ logger.debugWithContext("Cache hit for RODiT tokens by account", {
848
+ ...baseContext,
849
+ cached: true,
850
+ firstTokenId: cachedTokens.token_id
851
+ });
852
+ return cachedTokens;
853
+ }
854
+
855
+ try {
856
+ const args = JSON.stringify({
857
+ account_id: account_id,
858
+ from_index: "0", // String format for U128
859
+ limit: 1 // Only retrieve the first token
860
+ });
861
+
862
+ const jsonData = {
863
+ jsonrpc: "2.0",
864
+ id: CONSTANTS.NEAR_CONTRACT_ID,
865
+ method: "query",
866
+ params: {
867
+ request_type: "call_function",
868
+ finality: "final",
869
+ account_id: CONSTANTS.NEAR_CONTRACT_ID,
870
+ method_name: "rodit_tokens_for_owner",
871
+ args_base64: Buffer.from(args).toString("base64"),
872
+ },
873
+ };
874
+
875
+ logger.debugWithContext("Sending RPC request for account tokens", {
876
+ ...baseContext,
877
+ rpcMethod: "rodit_tokens_for_owner"
878
+ });
879
+
880
+ const response = await fetch(getNearRpcUrl(), {
881
+ method: "POST",
882
+ headers: { "Content-Type": "application/json" },
883
+ body: JSON.stringify(jsonData),
884
+ });
885
+
886
+ if (!response.ok) {
887
+ const duration = Date.now() - startTime;
888
+
889
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
890
+ component: "BlockchainService",
891
+ success: false,
892
+ result: 'failure',
893
+ reason: `HTTP error from blockchain RPC: status ${response.status}`
894
+ });
895
+ logger.metric("blockchain_rpc_errors_total", 1, {
896
+ component: "BlockchainService",
897
+ method: "tokens_from_account",
898
+ result: 'failure',
899
+ reason: `HTTP error from blockchain RPC: status ${response.status}`
900
+ });
901
+
902
+ logErrorWithMetrics(
903
+ "HTTP error from blockchain RPC",
904
+ {
905
+ ...baseContext,
906
+ statusCode: response.status,
907
+ duration,
908
+ result: 'failure',
909
+ reason: `HTTP error from blockchain RPC: status ${response.status}`
910
+ },
911
+ new Error(`HTTP error! status: ${response.status}`),
912
+ "account_tokens_fetch",
913
+ {
914
+ result: 'failure',
915
+ reason: `HTTP error from blockchain RPC: status ${response.status}`,
916
+ error_type: "HTTP_ERROR",
917
+ status_code: response.status,
918
+ duration
919
+ }
920
+ );
921
+
922
+ throw new Error(`HTTP error! status: ${response.status}`);
923
+ }
924
+
925
+ const responseText = await response.text();
926
+ const parsedJson = JSON.parse(responseText);
927
+
928
+ // Check for RPC-level errors
929
+ if (parsedJson.error) {
930
+ const duration = Date.now() - startTime;
931
+
932
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
933
+ component: "BlockchainService",
934
+ success: false,
935
+ result: 'failure',
936
+ reason: `RPC error: ${parsedJson.error.message || parsedJson.error}`
937
+ });
938
+ logger.metric("blockchain_rpc_errors_total", 1, {
939
+ component: "BlockchainService",
940
+ method: "tokens_from_account",
941
+ result: 'failure',
942
+ reason: `RPC error: ${parsedJson.error.message || parsedJson.error}`
943
+ });
944
+
945
+ logErrorWithMetrics(
946
+ "RPC error response",
947
+ {
948
+ ...baseContext,
949
+ duration,
950
+ rpcError: parsedJson.error,
951
+ result: 'failure',
952
+ reason: `RPC error: ${parsedJson.error.message || parsedJson.error}`
953
+ },
954
+ new Error(`RPC error: ${parsedJson.error.message || parsedJson.error}`),
955
+ "account_tokens_fetch",
956
+ {
957
+ result: 'failure',
958
+ reason: `RPC error: ${parsedJson.error.message || parsedJson.error}`,
959
+ error_type: "RPC_ERROR",
960
+ duration
961
+ }
962
+ );
963
+
964
+ throw new Error(`RPC error: ${parsedJson.error.message || parsedJson.error}`);
965
+ }
966
+
967
+ // Check if result exists before accessing nested properties
968
+ if (!parsedJson.result) {
969
+ const duration = Date.now() - startTime;
970
+
971
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
972
+ component: "BlockchainService",
973
+ success: false,
974
+ result: 'failure',
975
+ reason: 'Missing result field in RPC response'
976
+ });
977
+ logger.metric("blockchain_rpc_errors_total", 1, {
978
+ component: "BlockchainService",
979
+ method: "tokens_from_account",
980
+ result: 'failure',
981
+ reason: 'Missing result field in RPC response'
982
+ });
983
+
984
+ logErrorWithMetrics(
985
+ "Invalid RPC response structure",
986
+ {
987
+ ...baseContext,
988
+ duration,
989
+ responseKeys: Object.keys(parsedJson),
990
+ result: 'failure',
991
+ reason: 'Missing result field in RPC response'
992
+ },
993
+ new Error("Missing result field in RPC response"),
994
+ "account_tokens_fetch",
995
+ {
996
+ result: 'failure',
997
+ reason: 'Missing result field in RPC response',
998
+ error_type: "INVALID_RESPONSE",
999
+ duration
1000
+ }
1001
+ );
1002
+
1003
+ throw new Error("Missing result field in RPC response");
1004
+ }
1005
+
1006
+ if (parsedJson.result && parsedJson.result.error) {
1007
+ const duration = Date.now() - startTime;
1008
+
1009
+ // Emit metrics for dashboards
1010
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
1011
+ component: "BlockchainService",
1012
+ success: false,
1013
+ result: 'failure',
1014
+ reason: `WASM execution error: ${parsedJson.result.error}`
1015
+ });
1016
+ logger.metric("blockchain_rpc_errors_total", 1, {
1017
+ component: "BlockchainService",
1018
+ method: "tokens_from_account",
1019
+ result: 'failure',
1020
+ reason: `WASM execution error: ${parsedJson.result.error}`
1021
+ });
1022
+
1023
+ logErrorWithMetrics(
1024
+ "WASM execution error",
1025
+ {
1026
+ ...baseContext,
1027
+ duration,
1028
+ wasmError: parsedJson.result.error,
1029
+ result: 'failure',
1030
+ reason: `WASM execution error: ${parsedJson.result.error}`
1031
+ },
1032
+ new Error(`Smart contract execution failed: ${parsedJson.result.error}`),
1033
+ "account_tokens_fetch",
1034
+ {
1035
+ result: 'failure',
1036
+ reason: `WASM execution error: ${parsedJson.result.error}`,
1037
+ error_type: "WASM_ERROR",
1038
+ duration
1039
+ }
1040
+ );
1041
+
1042
+ throw new Error(
1043
+ `Smart contract execution failed: ${parsedJson.result.error}`
1044
+ );
1045
+ }
1046
+
1047
+ // Safe access to nested result property
1048
+ const resultArray = parsedJson.result?.result;
1049
+ if (!Array.isArray(resultArray)) {
1050
+ const duration = Date.now() - startTime;
1051
+
1052
+ // Emit metrics for dashboards
1053
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
1054
+ component: "BlockchainService",
1055
+ success: false,
1056
+ error: "INVALID_RESULT_FORMAT",
1057
+ });
1058
+ logger.metric("blockchain_rpc_errors_total", 1, {
1059
+ component: "BlockchainService",
1060
+ method: "tokens_from_account",
1061
+ error: "INVALID_RESULT_FORMAT",
1062
+ });
1063
+
1064
+ logErrorWithMetrics(
1065
+ "Invalid result format from blockchain",
1066
+ {
1067
+ ...baseContext,
1068
+ duration,
1069
+ resultType: typeof resultArray
1070
+ },
1071
+ new Error("Result is not an array"),
1072
+ "account_tokens_fetch",
1073
+ {
1074
+ result: "error",
1075
+ error_type: "INVALID_RESULT_FORMAT",
1076
+ duration
1077
+ }
1078
+ );
1079
+
1080
+ throw new Error("Result is not an array");
1081
+ }
1082
+
1083
+ const resultString = new TextDecoder().decode(new Uint8Array(resultArray));
1084
+ const resultStruct = JSON.parse(resultString);
1085
+
1086
+ if (!Array.isArray(resultStruct) || resultStruct.length === 0) {
1087
+ const duration = Date.now() - startTime;
1088
+
1089
+ logger.warnWithContext("No RODiT instances found for account", {
1090
+ ...baseContext,
1091
+ duration,
1092
+ tokenCount: 0
1093
+ });
1094
+
1095
+ // Emit metrics for dashboards
1096
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
1097
+ component: "BlockchainService",
1098
+ success: true,
1099
+ tokenCount: 0,
1100
+ });
1101
+ logger.metric("empty_account_tokens_total", 1, {
1102
+ component: "BlockchainService",
1103
+ accountId: account_id,
1104
+ });
1105
+
1106
+ const emptyRodit = new RODiT();
1107
+ return emptyRodit;
1108
+ }
1109
+
1110
+ const rodit = new RODiT();
1111
+ Object.assign(rodit, resultStruct[0]);
1112
+
1113
+ const duration = Date.now() - startTime;
1114
+ logger.debugWithContext("Successfully retrieved RODiT tokens", {
1115
+ ...baseContext,
1116
+ duration,
1117
+ tokenCount: resultStruct.length,
1118
+ firstTokenId: rodit.token_id
1119
+ });
1120
+
1121
+ // Emit metrics for dashboards
1122
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
1123
+ component: "BlockchainService",
1124
+ success: true,
1125
+ tokenCount: resultStruct.length,
1126
+ });
1127
+ // Cache successful lookups
1128
+ _cacheSet(cacheKey, rodit, NEAR_RPC_CACHE_TTL);
1129
+ return rodit;
1130
+ } catch (error) {
1131
+ const duration = Date.now() - startTime;
1132
+
1133
+ // Emit metrics for dashboards
1134
+ logger.metric("account_tokens_fetch_duration_ms", duration, {
1135
+ component: "BlockchainService",
1136
+ success: false,
1137
+ result: 'failure',
1138
+ reason: `Failed to fetch RODiT tokens: ${error.message}`
1139
+ });
1140
+ logger.metric("blockchain_rpc_errors_total", 1, {
1141
+ component: "BlockchainService",
1142
+ method: "tokens_from_account",
1143
+ result: 'failure',
1144
+ reason: `Failed to fetch RODiT tokens: ${error.message}`
1145
+ });
1146
+
1147
+ logErrorWithMetrics(
1148
+ "Failed to fetch RODiT tokens",
1149
+ {
1150
+ ...baseContext,
1151
+ duration,
1152
+ result: 'failure',
1153
+ reason: `Failed to fetch RODiT tokens: ${error.message}`
1154
+ },
1155
+ error,
1156
+ "account_tokens_fetch",
1157
+ {
1158
+ result: 'failure',
1159
+ reason: `Failed to fetch RODiT tokens: ${error.message}`,
1160
+ error_type: error.constructor.name,
1161
+ duration
1162
+ }
1163
+ );
1164
+
1165
+ throw error;
1166
+ }
1167
+ }
1168
+
1169
+ /**
1170
+ * Fetches a public key in bytes format for an account
1171
+ *
1172
+ * @param {string} accountId - Account ID
1173
+ * @returns {Promise<Uint8Array>} Public key bytes
1174
+ */
1175
+ async function nearorg_rpc_fetchpublickeybytes(accountId) {
1176
+ const requestId = ulid();
1177
+ const startTime = Date.now();
1178
+
1179
+ const baseContext = createLogContext(
1180
+ "BlockchainService",
1181
+ "nearorg_rpc_fetchpublickeybytes",
1182
+ {
1183
+ requestId,
1184
+ accountId
1185
+ }
1186
+ );
1187
+
1188
+ logger.debugWithContext("Fetching public key bytes", baseContext);
1189
+
1190
+ try {
1191
+ // Cache check
1192
+ const cacheKey = `pubkey_bytes:${accountId}`;
1193
+ const cached = _cacheGet(cacheKey);
1194
+ if (cached) {
1195
+ logger.debugWithContext("Cache hit for public key bytes", {
1196
+ ...baseContext,
1197
+ keyLength: cached.length
1198
+ });
1199
+ return cached;
1200
+ }
1201
+ const isImplicitAccount = /^[0-9a-f]{64}$/.test(accountId);
1202
+
1203
+ if (isImplicitAccount) {
1204
+ logger.debugWithContext("Account is implicit, using direct hex encoding", baseContext);
1205
+
1206
+ const result = new Uint8Array(Buffer.from(accountId, "hex"));
1207
+
1208
+ const duration = Date.now() - startTime;
1209
+ logger.debugWithContext(
1210
+ "Successfully retrieved public key bytes from implicit account",
1211
+ {
1212
+ ...baseContext,
1213
+ duration,
1214
+ keyLength: result.length
1215
+ }
1216
+ );
1217
+
1218
+ // Emit metrics for dashboards
1219
+ logger.metric("public_key_fetch_duration_ms", duration, {
1220
+ method: "direct_hex",
1221
+ component: "BlockchainService",
1222
+ success: true,
1223
+ });
1224
+ // Cache result
1225
+ _cacheSet(cacheKey, result, NEAR_RPC_CACHE_TTL);
1226
+ return result;
1227
+ }
1228
+
1229
+ logger.debugWithContext("Account is named, fetching RODiT token", baseContext);
1230
+
1231
+ const rodit = await nearorg_rpc_tokensfromaccountid(accountId);
1232
+
1233
+ if (!rodit || !rodit.owner_id) {
1234
+ const duration = Date.now() - startTime;
1235
+
1236
+ // Emit metrics for dashboards
1237
+ logger.metric("public_key_fetch_duration_ms", duration, {
1238
+ method: "rodit_lookup",
1239
+ component: "BlockchainService",
1240
+ success: false,
1241
+ error: "NO_VALID_RODIT",
1242
+ });
1243
+ logger.metric("public_key_fetch_errors_total", 1, {
1244
+ method: "rodit_lookup",
1245
+ component: "BlockchainService",
1246
+ error: "NO_VALID_RODIT",
1247
+ });
1248
+
1249
+ logErrorWithMetrics(
1250
+ "No valid RODiT found for account",
1251
+ {
1252
+ ...baseContext,
1253
+ duration,
1254
+ error: "NO_VALID_RODIT"
1255
+ },
1256
+ new Error(`No valid RODiT found for account: ${accountId}`),
1257
+ "public_key_fetch",
1258
+ {
1259
+ result: "error",
1260
+ error_type: "NO_VALID_RODIT",
1261
+ method: "rodit_lookup",
1262
+ duration
1263
+ }
1264
+ );
1265
+
1266
+ throw new Error(`No valid RODiT found for account: ${accountId}`);
1267
+ }
1268
+
1269
+ const result = new Uint8Array(Buffer.from(rodit.owner_id, "hex"));
1270
+
1271
+ const duration = Date.now() - startTime;
1272
+ logger.debugWithContext("Successfully retrieved public key bytes from RODiT", {
1273
+ ...baseContext,
1274
+ duration,
1275
+ keyLength: result.length
1276
+ });
1277
+
1278
+ // Emit metrics for dashboards
1279
+ logger.metric("public_key_fetch_duration_ms", duration, {
1280
+ method: "rodit_lookup",
1281
+ component: "BlockchainService",
1282
+ success: true,
1283
+ });
1284
+ // Cache result using unified TTL setting
1285
+ _cacheSet(cacheKey, result, NEAR_RPC_CACHE_TTL);
1286
+ return result;
1287
+ } catch (error) {
1288
+ const duration = Date.now() - startTime;
1289
+
1290
+ // Emit metrics for dashboards
1291
+ logger.metric("public_key_fetch_duration_ms", duration, {
1292
+ component: "BlockchainService",
1293
+ success: false,
1294
+ error: error.constructor.name,
1295
+ });
1296
+ logger.metric("public_key_fetch_errors_total", 1, {
1297
+ component: "BlockchainService",
1298
+ error: error.constructor.name,
1299
+ });
1300
+
1301
+ logErrorWithMetrics(
1302
+ "Failed to fetch public key bytes",
1303
+ {
1304
+ ...baseContext,
1305
+ duration
1306
+ },
1307
+ error,
1308
+ "public_key_fetch",
1309
+ {
1310
+ result: "error",
1311
+ error_type: error.constructor.name,
1312
+ duration
1313
+ }
1314
+ );
1315
+
1316
+ throw new Error(`Error retrieving public key: ${error.message}`);
1317
+ }
1318
+ }
1319
+
1320
+ async function nearorg_rpc_listpublicagents(limitsandcursor = {}) {
1321
+ const { limit = 20, cursor } = limitsandcursor;
1322
+ // Convert cursor to from_index for rodit_tokens method
1323
+ const from_index = cursor ? cursor : null;
1324
+ const requestId = ulid();
1325
+ const startTime = Date.now();
1326
+ const baseContext = createLogContext(
1327
+ "BlockchainService",
1328
+ "nearorg_rpc_listpublicagents",
1329
+ {
1330
+ requestId,
1331
+ limit,
1332
+ cursor
1333
+ }
1334
+ );
1335
+ logger.debugWithContext("Fetching public agents list (using rodit_tokens)", baseContext);
1336
+ try {
1337
+ logger.debugWithContext("DEBUG: Entered try block", baseContext);
1338
+ const args = { from_index, limit };
1339
+ const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");
1340
+ const json_data = {
1341
+ jsonrpc: "2.0",
1342
+ id: CONSTANTS.NEAR_CONTRACT_ID,
1343
+ method: "query",
1344
+ params: {
1345
+ request_type: "call_function",
1346
+ finality: "final",
1347
+ account_id: CONSTANTS.NEAR_CONTRACT_ID,
1348
+ method_name: "rodit_tokens",
1349
+ args_base64: argsBase64
1350
+ }
1351
+ };
1352
+
1353
+ const rpcStart = Date.now();
1354
+ logger.debugWithContext("DEBUG: About to make fetch call", baseContext);
1355
+ const response = await fetch(getNearRpcUrl(), {
1356
+ method: "POST",
1357
+ headers: { "Content-Type": "application/json" },
1358
+ body: JSON.stringify(json_data)
1359
+ });
1360
+ const rpcDuration = Date.now() - rpcStart;
1361
+
1362
+ logger.debugWithContext("DEBUG: Fetch completed", { ...baseContext, status: response.status });
1363
+ if (!response.ok) {
1364
+ logger.metric("near_rpc_calls", rpcDuration, { result: "failure", method: "list_public_agents", status_code: response.status });
1365
+ throw new Error(`HTTP error ${response.status}`);
1366
+ }
1367
+
1368
+ logger.debugWithContext("DEBUG: About to parse JSON", baseContext);
1369
+ const parsed = await response.json();
1370
+ logger.debugWithContext("DEBUG: JSON parsed", baseContext);
1371
+ logger.debugWithContext("DEBUG: Inspecting parsed response", {
1372
+ ...baseContext,
1373
+ parsedKeys: Object.keys(parsed),
1374
+ hasResult: !!parsed.result,
1375
+ resultKeys: parsed.result ? Object.keys(parsed.result) : null,
1376
+ resultBase64Length: parsed.result?.result?.length || 0
1377
+ });
1378
+ const resultBase64 = parsed.result?.result;
1379
+ if (!resultBase64) {
1380
+ logger.debugWithContext("DEBUG: No resultBase64 found, returning empty list", {
1381
+ ...baseContext,
1382
+ fullParsedResponse: parsed
1383
+ });
1384
+ return { list_agents: [], nextCursor: null };
1385
+ }
1386
+
1387
+ logger.debugWithContext("DEBUG: About to decode base64", baseContext);
1388
+ const buf = Buffer.from(resultBase64, "base64");
1389
+ const decoded = new TextDecoder().decode(buf);
1390
+ logger.debugWithContext("DEBUG: About to parse payload JSON", baseContext);
1391
+ const payload = JSON.parse(decoded);
1392
+ logger.debugWithContext("DEBUG: Payload parsed successfully", baseContext);
1393
+
1394
+ const totalDuration = Date.now() - startTime;
1395
+ logger.metric("near_rpc_calls", totalDuration, { result: "success", method: "list_public_agents" });
1396
+
1397
+ // Log the complete response data (rodit_tokens)
1398
+ logger.debugWithContext("Public agents list retrieved (rodit_tokens)", {
1399
+ ...baseContext,
1400
+ duration: totalDuration,
1401
+ tokenCount: payload?.length || 0,
1402
+ tokens: payload
1403
+ });
1404
+
1405
+ // Transform rodit_tokens response to match expected list_agents format
1406
+ const transformedResponse = {
1407
+ list_agents: payload || [],
1408
+ nextCursor: payload && payload.length === limit ? (from_index || 0) + limit : null
1409
+ };
1410
+
1411
+ // Cache successful response
1412
+ _cacheSet(cacheKey, transformedResponse, 30000); // 30 seconds for public agents list
1413
+ return transformedResponse;
1414
+ } catch (error) {
1415
+ const duration = Date.now() - startTime;
1416
+ logger.metric("near_rpc_errors", 1, { method: "list_public_agents" });
1417
+ logErrorWithMetrics(
1418
+ "Error listing public agents",
1419
+ { ...baseContext, duration },
1420
+ error,
1421
+ "near_rpc_listpublicagents",
1422
+ { result: "error", duration }
1423
+ );
1424
+ throw error;
1425
+ }
1426
+ }
1427
+
1428
+ /**
1429
+ * Fetches list of access keys for an account
1430
+ * @param {string} accountId
1431
+ */
1432
+ async function nearorg_rpc_accesskeys(accountId) {
1433
+ const requestId = ulid();
1434
+ const startTime = Date.now();
1435
+ const baseContext = createLogContext("BlockchainService","nearorg_rpc_accesskeys",{requestId,accountId});
1436
+ logger.debugWithContext("Fetching access keys", baseContext);
1437
+ // Cache check
1438
+ const cacheKey = `accesskeys:${accountId}`;
1439
+ const cached = _cacheGet(cacheKey);
1440
+ if (cached !== undefined) {
1441
+ logger.debugWithContext("Cache hit for access keys", { ...baseContext });
1442
+ return cached;
1443
+ }
1444
+ const json_data = {
1445
+ jsonrpc:"2.0", id:CONSTANTS.NEAR_CONTRACT_ID, method:"query", params:{request_type:"view_access_key_list", finality:"final", account_id:accountId}
1446
+ };
1447
+ const response = await fetch(getNearRpcUrl(),{method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(json_data)});
1448
+ const duration = Date.now() - startTime;
1449
+ if(!response.ok){ logger.metric("near_rpc_calls", duration,{result:"failure",method:"view_access_key_list",status_code:response.status}); throw new Error(`HTTP ${response.status}`);}
1450
+ const parsed = await response.json();
1451
+ logger.metric("near_rpc_calls", duration,{result:"success",method:"view_access_key_list"});
1452
+ // Cache successful result
1453
+ _cacheSet(cacheKey, parsed.result, NEAR_RPC_CACHE_TTL);
1454
+ return parsed.result;
1455
+ }
1456
+
1457
+ /**
1458
+ * Fetches owner (account ID) of a RODiT token
1459
+ * @param {string} token_id
1460
+ */
1461
+ async function nearorg_rpc_rodit_owner(token_id){
1462
+ const requestId = ulid();
1463
+ const startTime = Date.now();
1464
+ const baseContext = createLogContext("BlockchainService","nearorg_rpc_rodit_owner",{requestId,token_id});
1465
+ logger.debugWithContext("Fetching RODiT owner", baseContext);
1466
+ // Cache check
1467
+ const cacheKey = `rodit_owner:${token_id}`;
1468
+ const cached = _cacheGet(cacheKey);
1469
+ if (cached !== undefined) {
1470
+ logger.debugWithContext("Cache hit for RODiT owner", { ...baseContext });
1471
+ return cached;
1472
+ }
1473
+ const args = { token_id };
1474
+ const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");
1475
+ const json_data = {jsonrpc:"2.0", id:CONSTANTS.NEAR_CONTRACT_ID, method:"query", params:{request_type:"call_function", finality:"final", account_id:CONSTANTS.NEAR_CONTRACT_ID, method_name:"rodit_token_owner", args_base64:argsBase64 }};
1476
+ const response = await fetch(getNearRpcUrl(),{method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(json_data)});
1477
+ const duration = Date.now()-startTime;
1478
+ if(!response.ok){ logger.metric("near_rpc_calls",duration,{result:"failure",method:"rodit_token_owner",status_code:response.status}); throw new Error(`HTTP ${response.status}`);}
1479
+ const parsed = await response.json();
1480
+ logger.metric("near_rpc_calls",duration,{result:"success",method:"rodit_token_owner"});
1481
+ if(parsed.result && parsed.result.result){ const buf = Buffer.from(parsed.result.result,"base64"); const value = JSON.parse(new TextDecoder().decode(buf)); _cacheSet(cacheKey, value, NEAR_RPC_CACHE_TTL); return value; }
1482
+ return null;
1483
+ }
1484
+
1485
+ /**
1486
+ * Retrieves a nonce for a RODiT token from the agent-auth contract
1487
+ * @param {string} token_id
1488
+ * @returns {Promise<string>} nonce
1489
+ */
1490
+ async function nearorg_rpc_getnonce(token_id) {
1491
+ const requestId = ulid();
1492
+ const startTime = Date.now();
1493
+ const baseContext = createLogContext("BlockchainService","nearorg_rpc_getnonce",{requestId,token_id});
1494
+ const args = { token_id };
1495
+ const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");
1496
+ const json_data = {jsonrpc:"2.0", id:CONSTANTS.NEAR_CONTRACT_ID, method:"query", params:{request_type:"call_function", finality:"final", account_id:CONSTANTS.NEAR_CONTRACT_ID, method_name:"get_nonce", args_base64:argsBase64 }};
1497
+ const response = await fetch(getNearRpcUrl(),{method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(json_data)});
1498
+ const duration = Date.now()-startTime;
1499
+ if(!response.ok){ logger.metric("near_rpc_calls", duration,{result:"failure",method:"get_nonce",status_code:response.status}); throw new Error(`HTTP ${response.status}`);}
1500
+ const parsed = await response.json();
1501
+ logger.metric("near_rpc_calls", duration,{result:"success",method:"get_nonce"});
1502
+ if(parsed.result && parsed.result.result){ return Buffer.from(parsed.result.result,"base64").toString(); }
1503
+ return null;
1504
+ }
1505
+
1506
+ /**
1507
+ * Verifies a signature for RODiT Authentication
1508
+ * @param {string} token_id
1509
+ * @param {string} nonce
1510
+ * @param {string} sig - base58 or hex signature
1511
+ * @returns {Promise<boolean>} verification result
1512
+ */
1513
+ async function nearorg_rpc_verifysignature(token_id, nonce, sig) {
1514
+ const requestId = ulid();
1515
+ const startTime = Date.now();
1516
+ const baseContext = createLogContext("BlockchainService","nearorg_rpc_verifysignature",{requestId,token_id});
1517
+ const args = { token_id, nonce, sig };
1518
+ const argsBase64 = Buffer.from(JSON.stringify(args)).toString("base64");
1519
+ const json_data = {jsonrpc:"2.0", id:CONSTANTS.NEAR_CONTRACT_ID, method:"query", params:{request_type:"call_function", finality:"final", account_id:CONSTANTS.NEAR_CONTRACT_ID, method_name:"verify_signature", args_base64:argsBase64 }};
1520
+ const response = await fetch(getNearRpcUrl(),{method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(json_data)});
1521
+ const duration = Date.now()-startTime;
1522
+ if(!response.ok){ logger.metric("near_rpc_calls", duration,{result:"failure",method:"verify_signature",status_code:response.status}); throw new Error(`HTTP ${response.status}`);}
1523
+ const parsed = await response.json();
1524
+ logger.metric("near_rpc_calls", duration,{result:"success",method:"verify_signature"});
1525
+ if(parsed.result && parsed.result.result){ return Buffer.from(parsed.result.result,"base64").toString() === 'true'; }
1526
+ return false;
1527
+ }
1528
+
1529
+ /**
1530
+ * Health check for NEAR RPC endpoint
1531
+ * Tests connectivity and rate limits before accepting traffic
1532
+ * @param {string} rpcUrl - The RPC URL to check (defaults to configured URL)
1533
+ * @param {number} timeout - Timeout in milliseconds
1534
+ * @returns {Promise<boolean>} - True if healthy
1535
+ */
1536
+ async function healthCheckRPC(rpcUrl = getNearRpcUrl(), timeout = 5000) {
1537
+ const requestId = ulid();
1538
+ const baseContext = createLogContext("BlockchainService", "healthCheckRPC", {
1539
+ requestId,
1540
+ rpcUrl
1541
+ });
1542
+
1543
+ logger.info('Checking NEAR RPC health', baseContext);
1544
+
1545
+ try {
1546
+ const controller = new AbortController();
1547
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
1548
+
1549
+ const startTime = Date.now();
1550
+ const response = await fetch(rpcUrl, {
1551
+ method: 'POST',
1552
+ headers: { 'Content-Type': 'application/json' },
1553
+ body: JSON.stringify({
1554
+ jsonrpc: '2.0',
1555
+ id: 'health-check',
1556
+ method: 'status',
1557
+ params: []
1558
+ }),
1559
+ signal: controller.signal
1560
+ });
1561
+
1562
+ clearTimeout(timeoutId);
1563
+ const duration = Date.now() - startTime;
1564
+
1565
+ if (response.status === 429) {
1566
+ logger.error('RPC endpoint is already rate-limited', {
1567
+ ...baseContext,
1568
+ status: 429,
1569
+ message: 'Consider using a dedicated RPC provider'
1570
+ });
1571
+ throw new Error('NEAR RPC endpoint is rate-limited (429). Use a dedicated provider.');
1572
+ }
1573
+
1574
+ if (!response.ok) {
1575
+ logger.error('RPC health check failed', {
1576
+ ...baseContext,
1577
+ status: response.status,
1578
+ statusText: response.statusText
1579
+ });
1580
+ throw new Error(`RPC health check failed: ${response.status}`);
1581
+ }
1582
+
1583
+ const data = await response.json();
1584
+
1585
+ logger.info('NEAR RPC is healthy', {
1586
+ ...baseContext,
1587
+ duration,
1588
+ chainId: data.result?.chain_id,
1589
+ syncStatus: data.result?.sync_info?.syncing
1590
+ });
1591
+
1592
+ // Warn if response is slow
1593
+ if (duration > 2000) {
1594
+ logger.warn('RPC response is slow', {
1595
+ ...baseContext,
1596
+ duration,
1597
+ threshold: 2000,
1598
+ recommendation: 'Consider using a faster RPC endpoint'
1599
+ });
1600
+ }
1601
+
1602
+ return true;
1603
+ } catch (err) {
1604
+ if (err.name === 'AbortError') {
1605
+ logger.error('RPC health check timed out', { ...baseContext, timeout });
1606
+ throw new Error(`RPC health check timed out after ${timeout}ms`);
1607
+ }
1608
+ throw err;
1609
+ }
1610
+ }
1611
+
1612
+ /**
1613
+ * Resolve a healthy NEAR RPC URL by probing configured primary first, then SDK default fallback.
1614
+ * Throws when no candidate is healthy.
1615
+ */
1616
+ async function resolveHealthyNearRpcUrl(options = {}) {
1617
+ const primaryRpcUrl = options.primaryRpcUrl || config.get("NEAR_RPC_URL");
1618
+ const fallbackRpcUrl = options.fallbackRpcUrl || config?.FALLBACK_DEFAULTS?.NEAR_RPC_URL;
1619
+ const timeout = Number(options.timeout || config.get("NEAR_RPC_TIMEOUT") || 5000);
1620
+ const candidates = [...new Set([primaryRpcUrl, fallbackRpcUrl].filter(Boolean))];
1621
+ const failures = [];
1622
+
1623
+ for (const rpcUrl of candidates) {
1624
+ try {
1625
+ await healthCheckRPC(rpcUrl, timeout);
1626
+ if (rpcUrl !== primaryRpcUrl) {
1627
+ logger.warn("Primary NEAR RPC unavailable; using SDK default fallback RPC", {
1628
+ component: "BlockchainService",
1629
+ method: "resolveHealthyNearRpcUrl",
1630
+ primaryRpcUrl,
1631
+ selectedRpcUrl: rpcUrl
1632
+ });
1633
+ }
1634
+ return rpcUrl;
1635
+ } catch (err) {
1636
+ failures.push({ rpcUrl, error: err instanceof Error ? err.message : String(err) });
1637
+ logger.warn("NEAR RPC candidate failed health check", {
1638
+ component: "BlockchainService",
1639
+ method: "resolveHealthyNearRpcUrl",
1640
+ rpcUrl,
1641
+ error: err instanceof Error ? err.message : String(err)
1642
+ });
1643
+ }
1644
+ }
1645
+
1646
+ const details = failures.map((f) => `${f.rpcUrl} -> ${f.error}`).join("; ");
1647
+ throw new Error(`No healthy NEAR RPC endpoint available (${details})`);
1648
+ }
1649
+
1650
+ /**
1651
+ * Fetch with retry logic for handling rate limits and transient errors
1652
+ * @param {string} url - The URL to fetch
1653
+ * @param {object} options - Fetch options
1654
+ * @param {number} maxRetries - Maximum number of retries
1655
+ * @returns {Promise<Response>} - The response
1656
+ */
1657
+ async function fetchWithRetry(url, options, maxRetries = 3) {
1658
+ const requestId = ulid();
1659
+ const baseContext = createLogContext("BlockchainService", "fetchWithRetry", {
1660
+ requestId,
1661
+ maxRetries
1662
+ });
1663
+
1664
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
1665
+ try {
1666
+ const response = await fetch(url, options);
1667
+
1668
+ // If rate limited, retry with exponential backoff
1669
+ if (response.status === 429 && attempt < maxRetries) {
1670
+ const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000);
1671
+ logger.warn(`Rate limited (429), retrying in ${backoffMs}ms...`, {
1672
+ ...baseContext,
1673
+ attempt: attempt + 1,
1674
+ maxRetries,
1675
+ backoffMs
1676
+ });
1677
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
1678
+ continue;
1679
+ }
1680
+
1681
+ return response;
1682
+ } catch (err) {
1683
+ if (attempt === maxRetries) throw err;
1684
+
1685
+ const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000);
1686
+ logger.warn(`RPC call failed, retrying in ${backoffMs}ms...`, {
1687
+ ...baseContext,
1688
+ attempt: attempt + 1,
1689
+ maxRetries,
1690
+ error: err.message,
1691
+ backoffMs
1692
+ });
1693
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
1694
+ }
1695
+ }
1696
+ }
1697
+
1698
+ module.exports = {
1699
+ RODiT,
1700
+ PayloadNEP413,
1701
+ PayloadNEP413Schema,
1702
+ CONSTANTS,
1703
+ nearorg_rpc_timestamp,
1704
+ nearorg_rpc_tokenfromroditid,
1705
+ nearorg_rpc_state,
1706
+ nearorg_rpc_tokensfromaccountid,
1707
+ nearorg_rpc_fetchpublickeybytes,
1708
+ nearorg_rpc_accesskeys,
1709
+ nearorg_rpc_rodit_owner,
1710
+ nearorg_rpc_getnonce,
1711
+ nearorg_rpc_verifysignature,
1712
+ healthCheckRPC,
1713
+ resolveHealthyNearRpcUrl,
1714
+ fetchWithRetry
1715
+ };