@openzeppelin/guardian-operator-client 0.14.3 → 0.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +319 -8
- package/dist/http.d.ts +158 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +909 -11
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/permissions.js +13 -0
- package/dist/permissions.js.map +1 -0
- package/dist/server-types.d.ts +35 -0
- package/dist/server-types.d.ts.map +1 -1
- package/dist/server-types.js +5 -1
- package/dist/server-types.js.map +1 -1
- package/dist/types.d.ts +403 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -1,3 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Set of stable error codes added by feature
|
|
3
|
+
* `005-operator-dashboard-metrics`. Used by {@link parseErrorBody} to
|
|
4
|
+
* narrow the unknown server `code` to the typed
|
|
5
|
+
* {@link DashboardErrorCode} union.
|
|
6
|
+
*/
|
|
7
|
+
const DASHBOARD_ERROR_CODES = new Set([
|
|
8
|
+
'authentication_failed',
|
|
9
|
+
'account_not_found',
|
|
10
|
+
'invalid_cursor',
|
|
11
|
+
'invalid_limit',
|
|
12
|
+
'invalid_status_filter',
|
|
13
|
+
'data_unavailable',
|
|
14
|
+
// Snapshot-specific codes (FR-045).
|
|
15
|
+
'unsupported_for_network',
|
|
16
|
+
'account_data_unavailable',
|
|
17
|
+
// Server emits SCREAMING_SNAKE_CASE; the TS union surfaces snake_case
|
|
18
|
+
// to match the rest of the DashboardErrorCode vocabulary.
|
|
19
|
+
// `parseErrorBody` does the mapping at the boundary.
|
|
20
|
+
'insufficient_operator_permission',
|
|
21
|
+
// Same wire-form / TS-form mapping as `insufficient_operator_permission`.
|
|
22
|
+
'account_paused',
|
|
23
|
+
]);
|
|
24
|
+
/** Server-emitted wire form for the permission-denial error code. */
|
|
25
|
+
const WIRE_INSUFFICIENT_OPERATOR_PERMISSION = 'GUARDIAN_INSUFFICIENT_OPERATOR_PERMISSION';
|
|
26
|
+
/** Server-emitted wire form for the account-paused error code. */
|
|
27
|
+
const WIRE_ACCOUNT_PAUSED = 'GUARDIAN_ACCOUNT_PAUSED';
|
|
28
|
+
/**
|
|
29
|
+
* Map a server-emitted error `code` to the typed
|
|
30
|
+
* {@link DashboardErrorCode} surface. The only mapping today is for
|
|
31
|
+
* the permission-denial code; every other code matches by string
|
|
32
|
+
* equality.
|
|
33
|
+
*/
|
|
34
|
+
function mapDashboardErrorCode(raw) {
|
|
35
|
+
if (raw === WIRE_INSUFFICIENT_OPERATOR_PERMISSION) {
|
|
36
|
+
return 'insufficient_operator_permission';
|
|
37
|
+
}
|
|
38
|
+
if (raw === WIRE_ACCOUNT_PAUSED) {
|
|
39
|
+
return 'account_paused';
|
|
40
|
+
}
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parse an error response body into a typed `{ code, message }` shape.
|
|
45
|
+
* Clients SHOULD branch on `code` rather than on the HTTP status alone
|
|
46
|
+
* — see FR-028 of `005-operator-dashboard-metrics`.
|
|
47
|
+
*
|
|
48
|
+
* Accepts either a `Response` (fetch result) or a pre-read JSON body.
|
|
49
|
+
* Returns `{ code: null, message: null }` when the body is absent or
|
|
50
|
+
* malformed; never throws.
|
|
51
|
+
*/
|
|
52
|
+
export async function parseErrorBody(response) {
|
|
53
|
+
let raw;
|
|
54
|
+
if (response &&
|
|
55
|
+
typeof response === 'object' &&
|
|
56
|
+
'text' in response &&
|
|
57
|
+
typeof response.text === 'function') {
|
|
58
|
+
// `response.text()` itself can reject when the body has already
|
|
59
|
+
// been consumed or the underlying stream errors. The function's
|
|
60
|
+
// contract says we never throw, so swallow that and treat it the
|
|
61
|
+
// same as an empty body.
|
|
62
|
+
let body;
|
|
63
|
+
try {
|
|
64
|
+
body = await response.text();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { code: null, message: null };
|
|
68
|
+
}
|
|
69
|
+
if (!body) {
|
|
70
|
+
return { code: null, message: null };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
raw = JSON.parse(body);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { code: null, message: null };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
raw = response;
|
|
81
|
+
}
|
|
82
|
+
if (raw === null || typeof raw !== 'object') {
|
|
83
|
+
return { code: null, message: null };
|
|
84
|
+
}
|
|
85
|
+
const record = raw;
|
|
86
|
+
const codeRaw = record['code'];
|
|
87
|
+
const code = mapDashboardErrorCode(typeof codeRaw === 'string' ? codeRaw : null);
|
|
88
|
+
const messageRaw = record['error'];
|
|
89
|
+
const message = typeof messageRaw === 'string' ? messageRaw : null;
|
|
90
|
+
const retryRaw = record['retry_after_secs'];
|
|
91
|
+
const retryAfterSecs = typeof retryRaw === 'number' && Number.isInteger(retryRaw)
|
|
92
|
+
? retryRaw
|
|
93
|
+
: undefined;
|
|
94
|
+
// Feature 006-operator-authz FR-016: populate `missingPermissions`
|
|
95
|
+
// and `retryable` only when the server emitted the permission-denial
|
|
96
|
+
// code. Every other code path leaves both fields undefined so the
|
|
97
|
+
// additive envelope extension is invisible to existing parsers. The
|
|
98
|
+
// contract pins `retryable=false` for this code; surface that
|
|
99
|
+
// unconditionally so a non-compliant server can't mislead retry
|
|
100
|
+
// policy code into retrying a permission denial.
|
|
101
|
+
let missingPermissions;
|
|
102
|
+
let retryable;
|
|
103
|
+
let pausedAt;
|
|
104
|
+
let pausedReason;
|
|
105
|
+
if (code === 'insufficient_operator_permission') {
|
|
106
|
+
const missingRaw = record['missing_permissions'];
|
|
107
|
+
if (Array.isArray(missingRaw) &&
|
|
108
|
+
missingRaw.every((v) => typeof v === 'string')) {
|
|
109
|
+
missingPermissions = missingRaw;
|
|
110
|
+
}
|
|
111
|
+
retryable = false;
|
|
112
|
+
}
|
|
113
|
+
else if (code === 'account_paused') {
|
|
114
|
+
const pausedAtRaw = record['paused_at'];
|
|
115
|
+
if (typeof pausedAtRaw === 'string') {
|
|
116
|
+
pausedAt = pausedAtRaw;
|
|
117
|
+
}
|
|
118
|
+
const reasonRaw = record['paused_reason'];
|
|
119
|
+
if (typeof reasonRaw === 'string') {
|
|
120
|
+
pausedReason = reasonRaw;
|
|
121
|
+
}
|
|
122
|
+
else if (reasonRaw === null) {
|
|
123
|
+
pausedReason = null;
|
|
124
|
+
}
|
|
125
|
+
retryable = false;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
code,
|
|
129
|
+
message,
|
|
130
|
+
retryAfterSecs,
|
|
131
|
+
missingPermissions,
|
|
132
|
+
retryable,
|
|
133
|
+
pausedAt,
|
|
134
|
+
pausedReason,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Type guard narrowing an arbitrary string to {@link DashboardErrorCode}.
|
|
139
|
+
*/
|
|
140
|
+
export function isDashboardErrorCode(value) {
|
|
141
|
+
return DASHBOARD_ERROR_CODES.has(value);
|
|
142
|
+
}
|
|
1
143
|
export class GuardianOperatorHttpError extends Error {
|
|
2
144
|
status;
|
|
3
145
|
statusText;
|
|
@@ -53,13 +195,166 @@ export class GuardianOperatorHttpClient {
|
|
|
53
195
|
async logout() {
|
|
54
196
|
return this.request(new URL('auth/logout', this.baseUrl), { method: 'POST' }, parseLogoutResponse);
|
|
55
197
|
}
|
|
56
|
-
|
|
57
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Paginated account list per feature
|
|
200
|
+
* `005-operator-dashboard-metrics` US1 / FR-001..FR-008.
|
|
201
|
+
*
|
|
202
|
+
* **Breaking change vs. `003-operator-account-apis`**: the response
|
|
203
|
+
* is now a {@link PagedResult} envelope. The previous
|
|
204
|
+
* unparameterized full-inventory mode and `total_count` field are
|
|
205
|
+
* removed; aggregate inventory totals are exposed via
|
|
206
|
+
* {@link GuardianOperatorHttpClient.getDashboardInfo}. Callers that
|
|
207
|
+
* relied on `listAccounts()` must migrate to this method.
|
|
208
|
+
*/
|
|
209
|
+
async listAccounts(options = {}) {
|
|
210
|
+
const url = new URL('dashboard/accounts', this.baseUrl);
|
|
211
|
+
applyPaginationParams(url, options);
|
|
212
|
+
if (options.paused !== undefined) {
|
|
213
|
+
url.searchParams.set('paused', String(options.paused));
|
|
214
|
+
}
|
|
215
|
+
return this.request(url, { method: 'GET' }, parseAccountListPage);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Return the authenticated operator's identity and effective
|
|
219
|
+
* permission set. Succeeds for any valid session, including
|
|
220
|
+
* operators with `permissions: []` (returns an empty array).
|
|
221
|
+
*/
|
|
222
|
+
async getSession() {
|
|
223
|
+
return this.request(new URL('dashboard/session', this.baseUrl), { method: 'GET' }, parseSessionInfo);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Inventory and lifecycle health snapshot per feature
|
|
227
|
+
* `005-operator-dashboard-metrics` US2 / FR-008..FR-012.
|
|
228
|
+
*/
|
|
229
|
+
async getDashboardInfo() {
|
|
230
|
+
return this.request(new URL('dashboard/info', this.baseUrl), { method: 'GET' }, parseDashboardInfo);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Pause mutating actions for the given account. Requires
|
|
234
|
+
* `accounts:pause`. Idempotent: re-pausing an already-paused account
|
|
235
|
+
* returns success with the original `pausedAt` preserved.
|
|
236
|
+
*/
|
|
237
|
+
async pauseAccount(accountId, reason) {
|
|
238
|
+
const encoded = encodeURIComponent(accountId);
|
|
239
|
+
return this.request(new URL(`dashboard/accounts/${encoded}/pause`, this.baseUrl), { method: 'POST', body: JSON.stringify({ reason }) }, parsePauseResponse);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Clear pause state for the given account. Requires `accounts:pause`.
|
|
243
|
+
* Idempotent: unpausing an already-active account returns success
|
|
244
|
+
* with no state change.
|
|
245
|
+
*/
|
|
246
|
+
async unpauseAccount(accountId, reason) {
|
|
247
|
+
const encoded = encodeURIComponent(accountId);
|
|
248
|
+
const init = { method: 'POST' };
|
|
249
|
+
if (reason !== undefined) {
|
|
250
|
+
init.body = JSON.stringify({ reason });
|
|
251
|
+
}
|
|
252
|
+
return this.request(new URL(`dashboard/accounts/${encoded}/unpause`, this.baseUrl), init, parseUnpauseResponse);
|
|
58
253
|
}
|
|
59
254
|
async getAccount(accountId) {
|
|
60
255
|
const encodedAccountId = encodeURIComponent(accountId);
|
|
61
256
|
return this.request(new URL(`dashboard/accounts/${encodedAccountId}`, this.baseUrl), { method: 'GET' }, parseAccountResponse);
|
|
62
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Return a decoded snapshot of `accountId`'s stored state at the
|
|
260
|
+
* commitment Guardian last canonicalized — v1 surface exposes the
|
|
261
|
+
* fungible/non-fungible vault. The endpoint distinguishes its
|
|
262
|
+
* failure modes via the FR-045 error taxonomy:
|
|
263
|
+
*
|
|
264
|
+
* - `400 unsupported_for_network` — the account's `network_config`
|
|
265
|
+
* is EVM. The snapshot endpoint is Miden-only by construction;
|
|
266
|
+
* this is a permanent condition for the account on this surface,
|
|
267
|
+
* not a transient failure.
|
|
268
|
+
* - `404 account_not_found` — no metadata exists.
|
|
269
|
+
* - `503 account_data_unavailable` — metadata exists but the state
|
|
270
|
+
* row cannot be loaded, or the stored blob fails to deserialize
|
|
271
|
+
* as a Miden `Account`. Transient/recoverable.
|
|
272
|
+
*
|
|
273
|
+
* Spec reference: follow-up addition to `005-operator-dashboard-metrics`
|
|
274
|
+
* FR-043..FR-046.
|
|
275
|
+
*/
|
|
276
|
+
async getAccountSnapshot(accountId) {
|
|
277
|
+
const encodedAccountId = encodeURIComponent(accountId);
|
|
278
|
+
return this.request(new URL(`dashboard/accounts/${encodedAccountId}/snapshot`, this.baseUrl), { method: 'GET' }, parseAccountSnapshot);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* List the per-account delta feed for `accountId`, paginated
|
|
282
|
+
* newest-first by `nonce DESC`. Spec reference:
|
|
283
|
+
* `005-operator-dashboard-metrics` US3.
|
|
284
|
+
*/
|
|
285
|
+
async listAccountDeltas(accountId, options = {}) {
|
|
286
|
+
const encodedAccountId = encodeURIComponent(accountId);
|
|
287
|
+
const url = new URL(`dashboard/accounts/${encodedAccountId}/deltas`, this.baseUrl);
|
|
288
|
+
applyPaginationParams(url, options);
|
|
289
|
+
return this.request(url, { method: 'GET' }, parseDeltaPage);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Fetch the full detail projection of one canonical delta. The
|
|
293
|
+
* nonce is serialized as a canonical base-10 `u64` URL segment;
|
|
294
|
+
* unknown account and unknown nonce both surface as
|
|
295
|
+
* `404 delta_not_found`.
|
|
296
|
+
*/
|
|
297
|
+
async getAccountDeltaDetail(accountId, nonce, options = {}) {
|
|
298
|
+
if (!Number.isSafeInteger(nonce) || nonce < 0) {
|
|
299
|
+
throw new GuardianOperatorContractError('getAccountDeltaDetail.nonce', `nonce must be a non-negative safe integer, got ${nonce}`);
|
|
300
|
+
}
|
|
301
|
+
const encodedAccountId = encodeURIComponent(accountId);
|
|
302
|
+
const url = new URL(`dashboard/accounts/${encodedAccountId}/deltas/${nonce.toString()}`, this.baseUrl);
|
|
303
|
+
if (options.includeRaw) {
|
|
304
|
+
url.searchParams.set('include', 'raw');
|
|
305
|
+
}
|
|
306
|
+
return this.request(url, { method: 'GET' }, parseDeltaDetail);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* List the per-account in-flight multisig proposal queue for
|
|
310
|
+
* `accountId`, paginated newest-first by `(nonce DESC, commitment
|
|
311
|
+
* DESC)`. Single-key Miden and EVM accounts always return an empty
|
|
312
|
+
* page per FR-017. Spec reference:
|
|
313
|
+
* `005-operator-dashboard-metrics` US4.
|
|
314
|
+
*/
|
|
315
|
+
async listAccountProposals(accountId, options = {}) {
|
|
316
|
+
const encodedAccountId = encodeURIComponent(accountId);
|
|
317
|
+
const url = new URL(`dashboard/accounts/${encodedAccountId}/proposals`, this.baseUrl);
|
|
318
|
+
applyPaginationParams(url, options);
|
|
319
|
+
return this.request(url, { method: 'GET' }, parseProposalPage);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Cross-account delta feed. Paginated newest-first by
|
|
323
|
+
* `status_timestamp DESC`. The optional `status` filter accepts a
|
|
324
|
+
* single status or an array; the wrapper serializes the array to
|
|
325
|
+
* comma-separated. Spec reference:
|
|
326
|
+
* `005-operator-dashboard-metrics` US6.
|
|
327
|
+
*/
|
|
328
|
+
async listGlobalDeltas(options = {}) {
|
|
329
|
+
const url = new URL('dashboard/deltas', this.baseUrl);
|
|
330
|
+
if (options.limit !== undefined) {
|
|
331
|
+
url.searchParams.set('limit', String(options.limit));
|
|
332
|
+
}
|
|
333
|
+
if (options.cursor !== undefined) {
|
|
334
|
+
url.searchParams.set('cursor', options.cursor);
|
|
335
|
+
}
|
|
336
|
+
if (options.status !== undefined) {
|
|
337
|
+
const statuses = Array.isArray(options.status)
|
|
338
|
+
? options.status
|
|
339
|
+
: [options.status];
|
|
340
|
+
if (statuses.length > 0) {
|
|
341
|
+
url.searchParams.set('status', statuses.join(','));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return this.request(url, { method: 'GET' }, parseGlobalDeltasPage);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Cross-account in-flight proposal feed. Paginated newest-first by
|
|
348
|
+
* `originating_timestamp DESC`. Takes no `status` filter — every
|
|
349
|
+
* entry is in-flight by definition. EVM accounts do not appear in
|
|
350
|
+
* v1 per FR-017. Spec reference:
|
|
351
|
+
* `005-operator-dashboard-metrics` US7.
|
|
352
|
+
*/
|
|
353
|
+
async listGlobalProposals(options = {}) {
|
|
354
|
+
const url = new URL('dashboard/proposals', this.baseUrl);
|
|
355
|
+
applyPaginationParams(url, options);
|
|
356
|
+
return this.request(url, { method: 'GET' }, parseGlobalProposalsPage);
|
|
357
|
+
}
|
|
63
358
|
async request(url, init, parse) {
|
|
64
359
|
const response = await this.fetchImpl(url.toString(), {
|
|
65
360
|
...init,
|
|
@@ -174,24 +469,138 @@ function parseLogoutResponse(value) {
|
|
|
174
469
|
success: requireSuccess(record, 'logout response'),
|
|
175
470
|
};
|
|
176
471
|
}
|
|
177
|
-
function
|
|
178
|
-
const record = asRecord(value, '
|
|
472
|
+
function parseSessionInfo(value) {
|
|
473
|
+
const record = asRecord(value, 'session response');
|
|
474
|
+
const permissions = requireStringArray(record, 'permissions', 'session response');
|
|
475
|
+
for (const [index, permission] of permissions.entries()) {
|
|
476
|
+
if (!KNOWN_OPERATOR_PERMISSIONS.has(permission)) {
|
|
477
|
+
throw new GuardianOperatorContractError('session response', `permissions[${index}] is not a known operator permission: ${JSON.stringify(permission)}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return {
|
|
481
|
+
operatorId: requireString(record, 'operator_id', 'session response'),
|
|
482
|
+
permissions: permissions,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Stable v1 operator permission vocabulary. Mirrors
|
|
487
|
+
* `crates/server/src/dashboard/permissions.rs::Permission::as_str`.
|
|
488
|
+
* Surfaces server/client drift as a contract failure instead of
|
|
489
|
+
* silently flowing unknown strings through.
|
|
490
|
+
*/
|
|
491
|
+
const KNOWN_OPERATOR_PERMISSIONS = new Set([
|
|
492
|
+
'dashboard:read',
|
|
493
|
+
'accounts:pause',
|
|
494
|
+
'policies:write',
|
|
495
|
+
]);
|
|
496
|
+
function parseAccountListPage(value) {
|
|
497
|
+
return parsePagedResult(value, parseAccountSummary, 'accounts page');
|
|
498
|
+
}
|
|
499
|
+
function parseDashboardInfo(value) {
|
|
500
|
+
const record = asRecord(value, 'dashboard info');
|
|
501
|
+
const serviceStatusRaw = requireString(record, 'service_status', 'dashboard info');
|
|
502
|
+
let serviceStatus;
|
|
503
|
+
if (serviceStatusRaw === 'healthy' || serviceStatusRaw === 'degraded') {
|
|
504
|
+
serviceStatus = serviceStatusRaw;
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
throw new GuardianOperatorContractError('dashboard info', `expected service_status to be "healthy" or "degraded", got ${JSON.stringify(serviceStatusRaw)}`);
|
|
508
|
+
}
|
|
509
|
+
const counts = asRecord(requireField(record, 'delta_status_counts', 'dashboard info'), 'dashboard info.delta_status_counts');
|
|
510
|
+
const buildRecord = asRecord(requireField(record, 'build', 'dashboard info'), 'dashboard info.build');
|
|
511
|
+
const profileRaw = requireString(buildRecord, 'profile', 'dashboard info.build');
|
|
512
|
+
if (profileRaw !== 'debug' && profileRaw !== 'release') {
|
|
513
|
+
throw new GuardianOperatorContractError('dashboard info.build', `expected profile to be "debug" or "release", got ${JSON.stringify(profileRaw)}`);
|
|
514
|
+
}
|
|
515
|
+
const build = {
|
|
516
|
+
version: requireString(buildRecord, 'version', 'dashboard info.build'),
|
|
517
|
+
gitCommit: requireString(buildRecord, 'git_commit', 'dashboard info.build'),
|
|
518
|
+
profile: profileRaw,
|
|
519
|
+
startedAt: requireString(buildRecord, 'started_at', 'dashboard info.build'),
|
|
520
|
+
};
|
|
521
|
+
const backendRecord = asRecord(requireField(record, 'backend', 'dashboard info'), 'dashboard info.backend');
|
|
522
|
+
const storageRaw = requireString(backendRecord, 'storage', 'dashboard info.backend');
|
|
523
|
+
if (storageRaw !== 'filesystem' && storageRaw !== 'postgres') {
|
|
524
|
+
throw new GuardianOperatorContractError('dashboard info.backend', `expected storage to be "filesystem" or "postgres", got ${JSON.stringify(storageRaw)}`);
|
|
525
|
+
}
|
|
526
|
+
const canonicalizationField = backendRecord['canonicalization'];
|
|
527
|
+
let canonicalization;
|
|
528
|
+
if (canonicalizationField === null || canonicalizationField === undefined) {
|
|
529
|
+
canonicalization = null;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
const c = asRecord(canonicalizationField, 'dashboard info.backend.canonicalization');
|
|
533
|
+
canonicalization = {
|
|
534
|
+
checkIntervalSeconds: requireInteger(c, 'check_interval_seconds', 'dashboard info.backend.canonicalization'),
|
|
535
|
+
maxRetries: requireInteger(c, 'max_retries', 'dashboard info.backend.canonicalization'),
|
|
536
|
+
submissionGracePeriodSeconds: requireInteger(c, 'submission_grace_period_seconds', 'dashboard info.backend.canonicalization'),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const backend = {
|
|
540
|
+
storage: storageRaw,
|
|
541
|
+
supportedAckSchemes: requireStringArray(backendRecord, 'supported_ack_schemes', 'dashboard info.backend'),
|
|
542
|
+
canonicalization,
|
|
543
|
+
};
|
|
544
|
+
const authMethodsRecord = asRecord(requireField(record, 'accounts_by_auth_method', 'dashboard info'), 'dashboard info.accounts_by_auth_method');
|
|
545
|
+
const accountsByAuthMethod = {};
|
|
546
|
+
for (const [key, raw] of Object.entries(authMethodsRecord)) {
|
|
547
|
+
if (typeof raw !== 'number' || !Number.isInteger(raw) || raw < 0) {
|
|
548
|
+
throw new GuardianOperatorContractError('dashboard info.accounts_by_auth_method', `expected non-negative integer count for "${key}", got ${JSON.stringify(raw)}`);
|
|
549
|
+
}
|
|
550
|
+
accountsByAuthMethod[key] = raw;
|
|
551
|
+
}
|
|
179
552
|
return {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
553
|
+
serviceStatus,
|
|
554
|
+
environment: requireString(record, 'environment', 'dashboard info'),
|
|
555
|
+
build,
|
|
556
|
+
backend,
|
|
557
|
+
totalAccountCount: requireInteger(record, 'total_account_count', 'dashboard info'),
|
|
558
|
+
accountsByAuthMethod,
|
|
559
|
+
latestActivity: requireNullableString(record, 'latest_activity', 'dashboard info'),
|
|
560
|
+
deltaStatusCounts: {
|
|
561
|
+
candidate: requireInteger(counts, 'candidate', 'dashboard info.delta_status_counts'),
|
|
562
|
+
canonical: requireInteger(counts, 'canonical', 'dashboard info.delta_status_counts'),
|
|
563
|
+
discarded: requireInteger(counts, 'discarded', 'dashboard info.delta_status_counts'),
|
|
564
|
+
},
|
|
565
|
+
inFlightProposalCount: requireInteger(record, 'in_flight_proposal_count', 'dashboard info'),
|
|
566
|
+
degradedAggregates: requireStringArray(record, 'degraded_aggregates', 'dashboard info'),
|
|
183
567
|
};
|
|
184
568
|
}
|
|
185
569
|
function parseAccountResponse(value) {
|
|
186
|
-
|
|
570
|
+
return parseAccountDetail(value, 'account response');
|
|
571
|
+
}
|
|
572
|
+
function parseAccountSnapshot(value) {
|
|
573
|
+
const record = asRecord(value, 'account snapshot');
|
|
574
|
+
const vaultRecord = asRecord(requireField(record, 'vault', 'account snapshot'), 'account snapshot.vault');
|
|
575
|
+
const fungibleRaw = requireArray(vaultRecord, 'fungible', 'account snapshot.vault');
|
|
576
|
+
const nonFungibleRaw = requireArray(vaultRecord, 'non_fungible', 'account snapshot.vault');
|
|
577
|
+
const fungible = fungibleRaw.map((entry, idx) => {
|
|
578
|
+
const ctx = `account snapshot.vault.fungible[${idx}]`;
|
|
579
|
+
const r = asRecord(entry, ctx);
|
|
580
|
+
return {
|
|
581
|
+
faucetId: requireString(r, 'faucet_id', ctx),
|
|
582
|
+
amount: requireString(r, 'amount', ctx),
|
|
583
|
+
};
|
|
584
|
+
});
|
|
585
|
+
const nonFungible = nonFungibleRaw.map((entry, idx) => {
|
|
586
|
+
const ctx = `account snapshot.vault.non_fungible[${idx}]`;
|
|
587
|
+
const r = asRecord(entry, ctx);
|
|
588
|
+
return {
|
|
589
|
+
faucetId: requireString(r, 'faucet_id', ctx),
|
|
590
|
+
vaultKey: requireString(r, 'vault_key', ctx),
|
|
591
|
+
};
|
|
592
|
+
});
|
|
593
|
+
const vault = { fungible, nonFungible };
|
|
187
594
|
return {
|
|
188
|
-
|
|
189
|
-
|
|
595
|
+
commitment: requireString(record, 'commitment', 'account snapshot'),
|
|
596
|
+
updatedAt: requireString(record, 'updated_at', 'account snapshot'),
|
|
597
|
+
hasPendingCandidate: requireBoolean(record, 'has_pending_candidate', 'account snapshot'),
|
|
598
|
+
vault,
|
|
190
599
|
};
|
|
191
600
|
}
|
|
192
601
|
function parseAccountSummary(value, context) {
|
|
193
602
|
const record = asRecord(value, context);
|
|
194
|
-
|
|
603
|
+
const summary = {
|
|
195
604
|
accountId: requireString(record, 'account_id', context),
|
|
196
605
|
authScheme: requireString(record, 'auth_scheme', context),
|
|
197
606
|
authorizedSignerCount: requireInteger(record, 'authorized_signer_count', context),
|
|
@@ -200,7 +609,16 @@ function parseAccountSummary(value, context) {
|
|
|
200
609
|
stateStatus: parseStateStatus(requireString(record, 'state_status', context), `${context}.state_status`),
|
|
201
610
|
createdAt: requireString(record, 'created_at', context),
|
|
202
611
|
updatedAt: requireString(record, 'updated_at', context),
|
|
612
|
+
pausedAt: requireNullableString(record, 'paused_at', context),
|
|
613
|
+
pausedReason: requireNullableString(record, 'paused_reason', context),
|
|
203
614
|
};
|
|
615
|
+
if (record.account_id_bech32 !== undefined && record.account_id_bech32 !== null) {
|
|
616
|
+
if (typeof record.account_id_bech32 !== 'string') {
|
|
617
|
+
throw new GuardianOperatorContractError(context, 'account_id_bech32 must be a string when present');
|
|
618
|
+
}
|
|
619
|
+
summary.accountIdBech32 = record.account_id_bech32;
|
|
620
|
+
}
|
|
621
|
+
return summary;
|
|
204
622
|
}
|
|
205
623
|
function parseAccountDetail(value, context) {
|
|
206
624
|
const summary = parseAccountSummary(value, context);
|
|
@@ -212,6 +630,33 @@ function parseAccountDetail(value, context) {
|
|
|
212
630
|
stateUpdatedAt: requireNullableString(record, 'state_updated_at', context),
|
|
213
631
|
};
|
|
214
632
|
}
|
|
633
|
+
function parseAccountStatus(value, context) {
|
|
634
|
+
if (value === 'active' || value === 'paused') {
|
|
635
|
+
return value;
|
|
636
|
+
}
|
|
637
|
+
throw new GuardianOperatorContractError(context, `expected account status "active" or "paused", got ${JSON.stringify(value)}`);
|
|
638
|
+
}
|
|
639
|
+
function parsePauseResponse(value) {
|
|
640
|
+
const ctx = 'pause response';
|
|
641
|
+
const record = asRecord(value, ctx);
|
|
642
|
+
return {
|
|
643
|
+
accountId: requireString(record, 'account_id', ctx),
|
|
644
|
+
beforeState: parseAccountStatus(requireString(record, 'before_state', ctx), `${ctx}.before_state`),
|
|
645
|
+
afterState: parseAccountStatus(requireString(record, 'after_state', ctx), `${ctx}.after_state`),
|
|
646
|
+
pausedAt: requireString(record, 'paused_at', ctx),
|
|
647
|
+
pausedReason: requireString(record, 'paused_reason', ctx),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function parseUnpauseResponse(value) {
|
|
651
|
+
const ctx = 'unpause response';
|
|
652
|
+
const record = asRecord(value, ctx);
|
|
653
|
+
return {
|
|
654
|
+
accountId: requireString(record, 'account_id', ctx),
|
|
655
|
+
beforeState: parseAccountStatus(requireString(record, 'before_state', ctx), `${ctx}.before_state`),
|
|
656
|
+
afterState: parseAccountStatus(requireString(record, 'after_state', ctx), `${ctx}.after_state`),
|
|
657
|
+
reason: requireNullableString(record, 'reason', ctx),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
215
660
|
function parseErrorResponse(value) {
|
|
216
661
|
const record = asRecord(value, 'error response');
|
|
217
662
|
const success = requireBoolean(record, 'success', 'error response');
|
|
@@ -226,10 +671,74 @@ function parseErrorResponse(value) {
|
|
|
226
671
|
}
|
|
227
672
|
retryAfterSecs = retryAfterValue;
|
|
228
673
|
}
|
|
674
|
+
// Optional stable machine-readable error code added by feature
|
|
675
|
+
// `005-operator-dashboard-metrics` and required for the dashboard
|
|
676
|
+
// error taxonomy (FR-028). Older servers may omit it; tolerate that
|
|
677
|
+
// by leaving the field undefined.
|
|
678
|
+
const codeValue = record.code;
|
|
679
|
+
let code;
|
|
680
|
+
if (codeValue !== undefined) {
|
|
681
|
+
if (typeof codeValue !== 'string') {
|
|
682
|
+
throw new GuardianOperatorContractError('error response', 'code must be a string when present');
|
|
683
|
+
}
|
|
684
|
+
code = mapDashboardErrorCode(codeValue) ?? undefined;
|
|
685
|
+
}
|
|
686
|
+
// Feature 006-operator-authz FR-016: populate `missingPermissions`
|
|
687
|
+
// and `retryable` only on the permission-denial code. Tolerate
|
|
688
|
+
// either ordering or omission on every other code so legacy 5xx /
|
|
689
|
+
// 4xx errors continue to parse byte-for-byte as before.
|
|
690
|
+
let missingPermissions;
|
|
691
|
+
let retryable;
|
|
692
|
+
let pausedAt;
|
|
693
|
+
let pausedReason;
|
|
694
|
+
if (code === 'insufficient_operator_permission') {
|
|
695
|
+
const missingRaw = record.missing_permissions;
|
|
696
|
+
if (missingRaw !== undefined) {
|
|
697
|
+
if (!Array.isArray(missingRaw) ||
|
|
698
|
+
!missingRaw.every((v) => typeof v === 'string')) {
|
|
699
|
+
throw new GuardianOperatorContractError('error response', 'missing_permissions must be an array of strings');
|
|
700
|
+
}
|
|
701
|
+
missingPermissions = missingRaw;
|
|
702
|
+
}
|
|
703
|
+
const retryableRaw = record.retryable;
|
|
704
|
+
if (retryableRaw !== undefined) {
|
|
705
|
+
if (retryableRaw !== false) {
|
|
706
|
+
// FR-016 pins `retryable: false` for permission denials.
|
|
707
|
+
// Surface server contract drift loudly rather than letting
|
|
708
|
+
// retry policy code retry an unretryable failure.
|
|
709
|
+
throw new GuardianOperatorContractError('error response', 'retryable must be false for insufficient_operator_permission');
|
|
710
|
+
}
|
|
711
|
+
retryable = false;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
else if (code === 'account_paused') {
|
|
715
|
+
const pausedAtRaw = record.paused_at;
|
|
716
|
+
if (typeof pausedAtRaw !== 'string') {
|
|
717
|
+
throw new GuardianOperatorContractError('error response', 'paused_at must be a string for account_paused');
|
|
718
|
+
}
|
|
719
|
+
pausedAt = pausedAtRaw;
|
|
720
|
+
const reasonRaw = record.paused_reason;
|
|
721
|
+
if (typeof reasonRaw === 'string' || reasonRaw === null) {
|
|
722
|
+
pausedReason = reasonRaw;
|
|
723
|
+
}
|
|
724
|
+
else if (reasonRaw !== undefined) {
|
|
725
|
+
throw new GuardianOperatorContractError('error response', 'paused_reason must be a string or null for account_paused');
|
|
726
|
+
}
|
|
727
|
+
const retryableRaw = record.retryable;
|
|
728
|
+
if (retryableRaw !== undefined && retryableRaw !== false) {
|
|
729
|
+
throw new GuardianOperatorContractError('error response', 'retryable must be false for account_paused');
|
|
730
|
+
}
|
|
731
|
+
retryable = false;
|
|
732
|
+
}
|
|
229
733
|
return {
|
|
230
734
|
success: false,
|
|
735
|
+
code,
|
|
231
736
|
error: requireString(record, 'error', 'error response'),
|
|
232
737
|
retryAfterSecs,
|
|
738
|
+
missingPermissions,
|
|
739
|
+
retryable,
|
|
740
|
+
pausedAt,
|
|
741
|
+
pausedReason,
|
|
233
742
|
};
|
|
234
743
|
}
|
|
235
744
|
function parseStateStatus(value, context) {
|
|
@@ -303,4 +812,393 @@ function requireStringArray(record, key, context) {
|
|
|
303
812
|
return entry;
|
|
304
813
|
});
|
|
305
814
|
}
|
|
815
|
+
function requireNonNegativeInteger(value, key, context) {
|
|
816
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
|
|
817
|
+
throw new GuardianOperatorContractError(context, `field "${key}" must be a non-negative integer`);
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
function assertStringArray(value, key, context) {
|
|
822
|
+
value.forEach((entry, index) => {
|
|
823
|
+
if (typeof entry !== 'string') {
|
|
824
|
+
throw new GuardianOperatorContractError(context, `field "${key}" entry ${index} must be a string`);
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
return value;
|
|
828
|
+
}
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
// Pagination helpers — feature `005-operator-dashboard-metrics`.
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
function applyPaginationParams(url, options) {
|
|
833
|
+
if (options.limit !== undefined) {
|
|
834
|
+
url.searchParams.set('limit', String(options.limit));
|
|
835
|
+
}
|
|
836
|
+
if (options.cursor !== undefined) {
|
|
837
|
+
url.searchParams.set('cursor', options.cursor);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function parseDeltaPage(value) {
|
|
841
|
+
return parsePagedResult(value, parseDeltaEntry, 'deltas page');
|
|
842
|
+
}
|
|
843
|
+
function parseProposalPage(value) {
|
|
844
|
+
return parsePagedResult(value, parseProposalEntry, 'proposals page');
|
|
845
|
+
}
|
|
846
|
+
function parseGlobalDeltasPage(value) {
|
|
847
|
+
return parsePagedResult(value, (entry, ctx) => parseGlobalDeltaEntry(entry, ctx), 'global deltas page');
|
|
848
|
+
}
|
|
849
|
+
function parseGlobalProposalsPage(value) {
|
|
850
|
+
return parsePagedResult(value, (entry, ctx) => parseGlobalProposalEntry(entry, ctx), 'global proposals page');
|
|
851
|
+
}
|
|
852
|
+
function parseGlobalDeltaEntry(value, context) {
|
|
853
|
+
const base = parseDeltaEntry(value, context);
|
|
854
|
+
if (!base.accountId) {
|
|
855
|
+
throw new GuardianOperatorContractError(context, 'global delta feed entries must include account_id');
|
|
856
|
+
}
|
|
857
|
+
return base;
|
|
858
|
+
}
|
|
859
|
+
function parseGlobalProposalEntry(value, context) {
|
|
860
|
+
const base = parseProposalEntry(value, context);
|
|
861
|
+
if (!base.accountId) {
|
|
862
|
+
throw new GuardianOperatorContractError(context, 'global proposal feed entries must include account_id');
|
|
863
|
+
}
|
|
864
|
+
return base;
|
|
865
|
+
}
|
|
866
|
+
function parsePagedResult(value, parseItem, context) {
|
|
867
|
+
const record = asRecord(value, context);
|
|
868
|
+
const items = requireArray(record, 'items', context).map((entry, index) => parseItem(entry, `${context}.items[${index}]`));
|
|
869
|
+
const nextCursorRaw = requireField(record, 'next_cursor', context);
|
|
870
|
+
let nextCursor;
|
|
871
|
+
if (nextCursorRaw === null) {
|
|
872
|
+
nextCursor = null;
|
|
873
|
+
}
|
|
874
|
+
else if (typeof nextCursorRaw === 'string') {
|
|
875
|
+
nextCursor = nextCursorRaw;
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
throw new GuardianOperatorContractError(context, 'next_cursor must be a string or null');
|
|
879
|
+
}
|
|
880
|
+
return { items, nextCursor };
|
|
881
|
+
}
|
|
882
|
+
function parseDeltaEntry(value, context) {
|
|
883
|
+
const record = asRecord(value, context);
|
|
884
|
+
const entry = {
|
|
885
|
+
nonce: requireInteger(record, 'nonce', context),
|
|
886
|
+
status: parseDeltaStatus(requireString(record, 'status', context), `${context}.status`),
|
|
887
|
+
statusTimestamp: requireString(record, 'status_timestamp', context),
|
|
888
|
+
prevCommitment: requireString(record, 'prev_commitment', context),
|
|
889
|
+
newCommitment: requireNullableString(record, 'new_commitment', context),
|
|
890
|
+
};
|
|
891
|
+
if (record.retry_count !== undefined) {
|
|
892
|
+
const retry = record.retry_count;
|
|
893
|
+
if (typeof retry !== 'number' || !Number.isInteger(retry) || retry < 0) {
|
|
894
|
+
throw new GuardianOperatorContractError(context, 'retry_count must be a non-negative integer when present');
|
|
895
|
+
}
|
|
896
|
+
entry.retryCount = retry;
|
|
897
|
+
}
|
|
898
|
+
if (record.account_id !== undefined) {
|
|
899
|
+
if (typeof record.account_id !== 'string') {
|
|
900
|
+
throw new GuardianOperatorContractError(context, 'account_id must be a string when present');
|
|
901
|
+
}
|
|
902
|
+
entry.accountId = record.account_id;
|
|
903
|
+
}
|
|
904
|
+
if (record.category !== undefined && record.category !== null) {
|
|
905
|
+
entry.category = parseDeltaCategory(requireString(record, 'category', context), `${context}.category`);
|
|
906
|
+
}
|
|
907
|
+
if (record.proposal_type !== undefined) {
|
|
908
|
+
if (typeof record.proposal_type !== 'string') {
|
|
909
|
+
throw new GuardianOperatorContractError(context, 'proposal_type must be a string when present');
|
|
910
|
+
}
|
|
911
|
+
entry.proposalType = record.proposal_type;
|
|
912
|
+
}
|
|
913
|
+
if (record.note_counts !== undefined && record.note_counts !== null) {
|
|
914
|
+
entry.noteCounts = parseDeltaNoteCounts(requireField(record, 'note_counts', context), `${context}.note_counts`);
|
|
915
|
+
}
|
|
916
|
+
if (record.assets !== undefined) {
|
|
917
|
+
entry.assets = parseDeltaAssetSummaryArray(record.assets, `${context}.assets`);
|
|
918
|
+
}
|
|
919
|
+
if (record.counterparty !== undefined && record.counterparty !== null) {
|
|
920
|
+
entry.counterparty = parseDeltaCounterpartySummary(record.counterparty, `${context}.counterparty`);
|
|
921
|
+
}
|
|
922
|
+
return entry;
|
|
923
|
+
}
|
|
924
|
+
function parseProposalEntry(value, context) {
|
|
925
|
+
const record = asRecord(value, context);
|
|
926
|
+
const entry = {
|
|
927
|
+
commitment: requireString(record, 'commitment', context),
|
|
928
|
+
nonce: requireInteger(record, 'nonce', context),
|
|
929
|
+
proposerId: requireString(record, 'proposer_id', context),
|
|
930
|
+
originatingTimestamp: requireString(record, 'originating_timestamp', context),
|
|
931
|
+
signaturesCollected: requireInteger(record, 'signatures_collected', context),
|
|
932
|
+
signaturesRequired: requireInteger(record, 'signatures_required', context),
|
|
933
|
+
prevCommitment: requireString(record, 'prev_commitment', context),
|
|
934
|
+
newCommitment: requireNullableString(record, 'new_commitment', context),
|
|
935
|
+
};
|
|
936
|
+
if (record.account_id !== undefined) {
|
|
937
|
+
if (typeof record.account_id !== 'string') {
|
|
938
|
+
throw new GuardianOperatorContractError(context, 'account_id must be a string when present');
|
|
939
|
+
}
|
|
940
|
+
entry.accountId = record.account_id;
|
|
941
|
+
}
|
|
942
|
+
if (record.proposal_type !== undefined) {
|
|
943
|
+
if (typeof record.proposal_type !== 'string') {
|
|
944
|
+
throw new GuardianOperatorContractError(context, 'proposal_type must be a string when present');
|
|
945
|
+
}
|
|
946
|
+
entry.proposalType = record.proposal_type;
|
|
947
|
+
}
|
|
948
|
+
return entry;
|
|
949
|
+
}
|
|
950
|
+
const DELTA_CATEGORY_VALUES = [
|
|
951
|
+
'asset_transfer',
|
|
952
|
+
'note_consumption',
|
|
953
|
+
'note_creation',
|
|
954
|
+
'account_storage_change',
|
|
955
|
+
'guardian_switch',
|
|
956
|
+
'custom',
|
|
957
|
+
];
|
|
958
|
+
function parseDeltaCategory(value, context) {
|
|
959
|
+
if (DELTA_CATEGORY_VALUES.includes(value)) {
|
|
960
|
+
return value;
|
|
961
|
+
}
|
|
962
|
+
throw new GuardianOperatorContractError(context, `invalid category "${value}" — expected one of ${DELTA_CATEGORY_VALUES.join(', ')}`);
|
|
963
|
+
}
|
|
964
|
+
function parseDeltaAssetSummary(value, context) {
|
|
965
|
+
const record = asRecord(value, context);
|
|
966
|
+
const kind = requireString(record, 'kind', context);
|
|
967
|
+
if (kind !== 'fungible' && kind !== 'non_fungible') {
|
|
968
|
+
throw new GuardianOperatorContractError(context, `asset kind must be "fungible" or "non_fungible", got "${kind}"`);
|
|
969
|
+
}
|
|
970
|
+
const asset = {
|
|
971
|
+
assetId: requireString(record, 'asset_id', context),
|
|
972
|
+
kind: kind,
|
|
973
|
+
};
|
|
974
|
+
if (record.amount !== undefined && record.amount !== null) {
|
|
975
|
+
if (typeof record.amount !== 'string') {
|
|
976
|
+
throw new GuardianOperatorContractError(context, 'asset amount must be a string when present');
|
|
977
|
+
}
|
|
978
|
+
asset.amount = record.amount;
|
|
979
|
+
}
|
|
980
|
+
return asset;
|
|
981
|
+
}
|
|
982
|
+
function parseDeltaAssetSummaryArray(value, context) {
|
|
983
|
+
if (!Array.isArray(value)) {
|
|
984
|
+
throw new GuardianOperatorContractError(context, 'expected an array');
|
|
985
|
+
}
|
|
986
|
+
return value.map((item, index) => parseDeltaAssetSummary(item, `${context}[${index}]`));
|
|
987
|
+
}
|
|
988
|
+
function parseDeltaCounterpartySummary(value, context) {
|
|
989
|
+
const record = asRecord(value, context);
|
|
990
|
+
const direction = requireString(record, 'direction', context);
|
|
991
|
+
if (direction !== 'in' && direction !== 'out') {
|
|
992
|
+
throw new GuardianOperatorContractError(context, `counterparty direction must be "in" or "out", got "${direction}"`);
|
|
993
|
+
}
|
|
994
|
+
return {
|
|
995
|
+
accountId: requireString(record, 'account_id', context),
|
|
996
|
+
direction: direction,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function parseDeltaNoteCounts(value, context) {
|
|
1000
|
+
const record = asRecord(value, context);
|
|
1001
|
+
const input = requireInteger(record, 'input', context);
|
|
1002
|
+
const output = requireInteger(record, 'output', context);
|
|
1003
|
+
if (input < 0 || output < 0) {
|
|
1004
|
+
throw new GuardianOperatorContractError(context, 'note counts must be non-negative integers');
|
|
1005
|
+
}
|
|
1006
|
+
return { input, output };
|
|
1007
|
+
}
|
|
1008
|
+
function parseDeltaProposalMetadata(value, context) {
|
|
1009
|
+
const record = asRecord(value, context);
|
|
1010
|
+
const proposal = {
|
|
1011
|
+
proposalType: requireString(record, 'proposal_type', context),
|
|
1012
|
+
};
|
|
1013
|
+
if (typeof record.description === 'string')
|
|
1014
|
+
proposal.description = record.description;
|
|
1015
|
+
if (typeof record.salt === 'string')
|
|
1016
|
+
proposal.salt = record.salt;
|
|
1017
|
+
if (record.required_signatures !== undefined)
|
|
1018
|
+
proposal.requiredSignatures = requireNonNegativeInteger(record.required_signatures, 'required_signatures', context);
|
|
1019
|
+
if (typeof record.recipient_id === 'string')
|
|
1020
|
+
proposal.recipientId = record.recipient_id;
|
|
1021
|
+
if (typeof record.faucet_id === 'string')
|
|
1022
|
+
proposal.faucetId = record.faucet_id;
|
|
1023
|
+
if (typeof record.amount === 'string')
|
|
1024
|
+
proposal.amount = record.amount;
|
|
1025
|
+
if (record.note_ids !== undefined)
|
|
1026
|
+
proposal.noteIds = assertStringArray(requireArray(record, 'note_ids', context), 'note_ids', context);
|
|
1027
|
+
if (record.consume_notes_metadata_version !== undefined)
|
|
1028
|
+
proposal.consumeNotesMetadataVersion = requireNonNegativeInteger(record.consume_notes_metadata_version, 'consume_notes_metadata_version', context);
|
|
1029
|
+
if (record.consume_notes_notes !== undefined)
|
|
1030
|
+
proposal.consumeNotesNotes = assertStringArray(requireArray(record, 'consume_notes_notes', context), 'consume_notes_notes', context);
|
|
1031
|
+
if (record.target_threshold !== undefined)
|
|
1032
|
+
proposal.targetThreshold = requireNonNegativeInteger(record.target_threshold, 'target_threshold', context);
|
|
1033
|
+
if (record.signer_commitments !== undefined)
|
|
1034
|
+
proposal.signerCommitments = assertStringArray(requireArray(record, 'signer_commitments', context), 'signer_commitments', context);
|
|
1035
|
+
if (typeof record.new_guardian_pubkey === 'string')
|
|
1036
|
+
proposal.newGuardianPubkey = record.new_guardian_pubkey;
|
|
1037
|
+
if (typeof record.new_guardian_endpoint === 'string')
|
|
1038
|
+
proposal.newGuardianEndpoint = record.new_guardian_endpoint;
|
|
1039
|
+
if (typeof record.target_procedure === 'string')
|
|
1040
|
+
proposal.targetProcedure = record.target_procedure;
|
|
1041
|
+
return proposal;
|
|
1042
|
+
}
|
|
1043
|
+
function parseDeltaDetail(value) {
|
|
1044
|
+
const ctx = 'delta detail';
|
|
1045
|
+
const record = asRecord(value, ctx);
|
|
1046
|
+
const detail = {
|
|
1047
|
+
accountId: requireString(record, 'account_id', ctx),
|
|
1048
|
+
nonce: requireInteger(record, 'nonce', ctx),
|
|
1049
|
+
status: parseDeltaStatus(requireString(record, 'status', ctx), `${ctx}.status`),
|
|
1050
|
+
statusTimestamp: requireString(record, 'status_timestamp', ctx),
|
|
1051
|
+
prevCommitment: requireString(record, 'prev_commitment', ctx),
|
|
1052
|
+
newCommitment: requireNullableString(record, 'new_commitment', ctx),
|
|
1053
|
+
inputNotes: parseDecodedNoteArray(requireField(record, 'input_notes', ctx), `${ctx}.input_notes`),
|
|
1054
|
+
outputNotes: parseDecodedNoteArray(requireField(record, 'output_notes', ctx), `${ctx}.output_notes`),
|
|
1055
|
+
vaultChanges: parseVaultChangeArray(requireField(record, 'vault_changes', ctx), `${ctx}.vault_changes`),
|
|
1056
|
+
storageChanges: parseStorageChangeArray(requireField(record, 'storage_changes', ctx), `${ctx}.storage_changes`),
|
|
1057
|
+
};
|
|
1058
|
+
if (record.retry_count !== undefined) {
|
|
1059
|
+
const retry = record.retry_count;
|
|
1060
|
+
if (typeof retry !== 'number' || !Number.isInteger(retry) || retry < 0) {
|
|
1061
|
+
throw new GuardianOperatorContractError(ctx, 'retry_count must be a non-negative integer when present');
|
|
1062
|
+
}
|
|
1063
|
+
detail.retryCount = retry;
|
|
1064
|
+
}
|
|
1065
|
+
if (record.category !== undefined && record.category !== null) {
|
|
1066
|
+
detail.category = parseDeltaCategory(requireString(record, 'category', ctx), `${ctx}.category`);
|
|
1067
|
+
}
|
|
1068
|
+
if (record.proposal !== undefined && record.proposal !== null) {
|
|
1069
|
+
detail.proposal = parseDeltaProposalMetadata(record.proposal, `${ctx}.proposal`);
|
|
1070
|
+
}
|
|
1071
|
+
if (Array.isArray(record.decode_warnings) && record.decode_warnings.length > 0) {
|
|
1072
|
+
detail.decodeWarnings = record.decode_warnings.map((w, i) => parseDecodeWarning(w, `${ctx}.decode_warnings[${i}]`));
|
|
1073
|
+
}
|
|
1074
|
+
if (typeof record.raw_transaction_summary === 'string') {
|
|
1075
|
+
detail.rawTransactionSummary = record.raw_transaction_summary;
|
|
1076
|
+
}
|
|
1077
|
+
return detail;
|
|
1078
|
+
}
|
|
1079
|
+
function parseDecodedNoteArray(value, context) {
|
|
1080
|
+
if (!Array.isArray(value)) {
|
|
1081
|
+
throw new GuardianOperatorContractError(context, 'expected an array of decoded notes');
|
|
1082
|
+
}
|
|
1083
|
+
return value.map((entry, i) => parseDecodedNote(entry, `${context}[${i}]`));
|
|
1084
|
+
}
|
|
1085
|
+
const NOTE_TAG_VALUES = [
|
|
1086
|
+
'p2id',
|
|
1087
|
+
'p2ide',
|
|
1088
|
+
'pswap',
|
|
1089
|
+
'mint',
|
|
1090
|
+
'burn',
|
|
1091
|
+
'custom',
|
|
1092
|
+
];
|
|
1093
|
+
function parseDecodedNote(value, context) {
|
|
1094
|
+
const record = asRecord(value, context);
|
|
1095
|
+
const tag = requireString(record, 'tag', context);
|
|
1096
|
+
if (!NOTE_TAG_VALUES.includes(tag)) {
|
|
1097
|
+
throw new GuardianOperatorContractError(context, `unknown note tag "${tag}"`);
|
|
1098
|
+
}
|
|
1099
|
+
const assets = requireField(record, 'assets', context);
|
|
1100
|
+
if (!Array.isArray(assets)) {
|
|
1101
|
+
throw new GuardianOperatorContractError(context, 'assets must be an array');
|
|
1102
|
+
}
|
|
1103
|
+
const note = {
|
|
1104
|
+
noteId: requireString(record, 'note_id', context),
|
|
1105
|
+
tag: tag,
|
|
1106
|
+
assets: assets.map((a, i) => parseDecodedAsset(a, `${context}.assets[${i}]`)),
|
|
1107
|
+
};
|
|
1108
|
+
if (typeof record.sender === 'string')
|
|
1109
|
+
note.sender = record.sender;
|
|
1110
|
+
if (typeof record.recipient === 'string')
|
|
1111
|
+
note.recipient = record.recipient;
|
|
1112
|
+
return note;
|
|
1113
|
+
}
|
|
1114
|
+
function parseDecodedAsset(value, context) {
|
|
1115
|
+
const record = asRecord(value, context);
|
|
1116
|
+
const kind = requireString(record, 'kind', context);
|
|
1117
|
+
if (kind !== 'fungible' && kind !== 'non_fungible') {
|
|
1118
|
+
throw new GuardianOperatorContractError(context, `asset kind must be "fungible" or "non_fungible", got "${kind}"`);
|
|
1119
|
+
}
|
|
1120
|
+
const asset = {
|
|
1121
|
+
assetId: requireString(record, 'asset_id', context),
|
|
1122
|
+
kind: kind,
|
|
1123
|
+
};
|
|
1124
|
+
if (typeof record.amount === 'string')
|
|
1125
|
+
asset.amount = record.amount;
|
|
1126
|
+
return asset;
|
|
1127
|
+
}
|
|
1128
|
+
function parseVaultChangeArray(value, context) {
|
|
1129
|
+
if (!Array.isArray(value)) {
|
|
1130
|
+
throw new GuardianOperatorContractError(context, 'expected an array of vault changes');
|
|
1131
|
+
}
|
|
1132
|
+
return value.map((entry, i) => parseVaultChange(entry, `${context}[${i}]`));
|
|
1133
|
+
}
|
|
1134
|
+
function parseVaultChange(value, context) {
|
|
1135
|
+
const record = asRecord(value, context);
|
|
1136
|
+
const kind = requireString(record, 'kind', context);
|
|
1137
|
+
if (kind === 'fungible') {
|
|
1138
|
+
return {
|
|
1139
|
+
kind: 'fungible',
|
|
1140
|
+
assetId: requireString(record, 'asset_id', context),
|
|
1141
|
+
change: requireString(record, 'change', context),
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
if (kind === 'non_fungible') {
|
|
1145
|
+
const added = requireField(record, 'added', context);
|
|
1146
|
+
const removed = requireField(record, 'removed', context);
|
|
1147
|
+
if (!Array.isArray(added) || !Array.isArray(removed)) {
|
|
1148
|
+
throw new GuardianOperatorContractError(context, 'non_fungible added/removed must be arrays');
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
kind: 'non_fungible',
|
|
1152
|
+
assetId: requireString(record, 'asset_id', context),
|
|
1153
|
+
added: assertStringArray(added, 'added', context),
|
|
1154
|
+
removed: assertStringArray(removed, 'removed', context),
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
throw new GuardianOperatorContractError(context, `vault change kind must be "fungible" or "non_fungible", got "${kind}"`);
|
|
1158
|
+
}
|
|
1159
|
+
function parseStorageChangeArray(value, context) {
|
|
1160
|
+
if (!Array.isArray(value)) {
|
|
1161
|
+
throw new GuardianOperatorContractError(context, 'expected an array of storage changes');
|
|
1162
|
+
}
|
|
1163
|
+
return value.map((entry, i) => parseStorageChange(entry, `${context}[${i}]`));
|
|
1164
|
+
}
|
|
1165
|
+
function parseStorageChange(value, context) {
|
|
1166
|
+
const record = asRecord(value, context);
|
|
1167
|
+
const change = {
|
|
1168
|
+
slotName: requireString(record, 'slot_name', context),
|
|
1169
|
+
after: requireNullableString(record, 'after', context),
|
|
1170
|
+
};
|
|
1171
|
+
if (record.key !== undefined && record.key !== null) {
|
|
1172
|
+
change.key = requireString(record, 'key', context);
|
|
1173
|
+
}
|
|
1174
|
+
if (record.before !== undefined) {
|
|
1175
|
+
change.before = requireNullableString(record, 'before', context);
|
|
1176
|
+
}
|
|
1177
|
+
return change;
|
|
1178
|
+
}
|
|
1179
|
+
const DECODE_SECTION_VALUES = [
|
|
1180
|
+
'tx_summary',
|
|
1181
|
+
'metadata',
|
|
1182
|
+
'input_notes',
|
|
1183
|
+
'output_notes',
|
|
1184
|
+
'vault',
|
|
1185
|
+
'storage',
|
|
1186
|
+
];
|
|
1187
|
+
function parseDecodeWarning(value, context) {
|
|
1188
|
+
const record = asRecord(value, context);
|
|
1189
|
+
const section = requireString(record, 'section', context);
|
|
1190
|
+
if (!DECODE_SECTION_VALUES.includes(section)) {
|
|
1191
|
+
throw new GuardianOperatorContractError(context, `unknown decode section "${section}"`);
|
|
1192
|
+
}
|
|
1193
|
+
return {
|
|
1194
|
+
section: section,
|
|
1195
|
+
reason: requireString(record, 'reason', context),
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function parseDeltaStatus(value, context) {
|
|
1199
|
+
if (value === 'candidate' || value === 'canonical' || value === 'discarded') {
|
|
1200
|
+
return value;
|
|
1201
|
+
}
|
|
1202
|
+
throw new GuardianOperatorContractError(context, `expected status to be "candidate" / "canonical" / "discarded", got ${JSON.stringify(value)}`);
|
|
1203
|
+
}
|
|
306
1204
|
//# sourceMappingURL=http.js.map
|