@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.
- package/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -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
|
+
};
|