@openzeppelin/guardian-operator-client 0.14.6 → 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 +146 -0
- package/dist/http.d.ts +58 -2
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +505 -4
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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 +218 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -14,7 +14,32 @@ const DASHBOARD_ERROR_CODES = new Set([
|
|
|
14
14
|
// Snapshot-specific codes (FR-045).
|
|
15
15
|
'unsupported_for_network',
|
|
16
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',
|
|
17
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
|
+
}
|
|
18
43
|
/**
|
|
19
44
|
* Parse an error response body into a typed `{ code, message }` shape.
|
|
20
45
|
* Clients SHOULD branch on `code` rather than on the HTTP status alone
|
|
@@ -59,14 +84,55 @@ export async function parseErrorBody(response) {
|
|
|
59
84
|
}
|
|
60
85
|
const record = raw;
|
|
61
86
|
const codeRaw = record['code'];
|
|
62
|
-
const code = typeof codeRaw === 'string' ? codeRaw : null;
|
|
87
|
+
const code = mapDashboardErrorCode(typeof codeRaw === 'string' ? codeRaw : null);
|
|
63
88
|
const messageRaw = record['error'];
|
|
64
89
|
const message = typeof messageRaw === 'string' ? messageRaw : null;
|
|
65
90
|
const retryRaw = record['retry_after_secs'];
|
|
66
91
|
const retryAfterSecs = typeof retryRaw === 'number' && Number.isInteger(retryRaw)
|
|
67
92
|
? retryRaw
|
|
68
93
|
: undefined;
|
|
69
|
-
|
|
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
|
+
};
|
|
70
136
|
}
|
|
71
137
|
/**
|
|
72
138
|
* Type guard narrowing an arbitrary string to {@link DashboardErrorCode}.
|
|
@@ -143,8 +209,19 @@ export class GuardianOperatorHttpClient {
|
|
|
143
209
|
async listAccounts(options = {}) {
|
|
144
210
|
const url = new URL('dashboard/accounts', this.baseUrl);
|
|
145
211
|
applyPaginationParams(url, options);
|
|
212
|
+
if (options.paused !== undefined) {
|
|
213
|
+
url.searchParams.set('paused', String(options.paused));
|
|
214
|
+
}
|
|
146
215
|
return this.request(url, { method: 'GET' }, parseAccountListPage);
|
|
147
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
|
+
}
|
|
148
225
|
/**
|
|
149
226
|
* Inventory and lifecycle health snapshot per feature
|
|
150
227
|
* `005-operator-dashboard-metrics` US2 / FR-008..FR-012.
|
|
@@ -152,6 +229,28 @@ export class GuardianOperatorHttpClient {
|
|
|
152
229
|
async getDashboardInfo() {
|
|
153
230
|
return this.request(new URL('dashboard/info', this.baseUrl), { method: 'GET' }, parseDashboardInfo);
|
|
154
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);
|
|
253
|
+
}
|
|
155
254
|
async getAccount(accountId) {
|
|
156
255
|
const encodedAccountId = encodeURIComponent(accountId);
|
|
157
256
|
return this.request(new URL(`dashboard/accounts/${encodedAccountId}`, this.baseUrl), { method: 'GET' }, parseAccountResponse);
|
|
@@ -189,6 +288,23 @@ export class GuardianOperatorHttpClient {
|
|
|
189
288
|
applyPaginationParams(url, options);
|
|
190
289
|
return this.request(url, { method: 'GET' }, parseDeltaPage);
|
|
191
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
|
+
}
|
|
192
308
|
/**
|
|
193
309
|
* List the per-account in-flight multisig proposal queue for
|
|
194
310
|
* `accountId`, paginated newest-first by `(nonce DESC, commitment
|
|
@@ -353,6 +469,30 @@ function parseLogoutResponse(value) {
|
|
|
353
469
|
success: requireSuccess(record, 'logout response'),
|
|
354
470
|
};
|
|
355
471
|
}
|
|
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
|
+
]);
|
|
356
496
|
function parseAccountListPage(value) {
|
|
357
497
|
return parsePagedResult(value, parseAccountSummary, 'accounts page');
|
|
358
498
|
}
|
|
@@ -460,7 +600,7 @@ function parseAccountSnapshot(value) {
|
|
|
460
600
|
}
|
|
461
601
|
function parseAccountSummary(value, context) {
|
|
462
602
|
const record = asRecord(value, context);
|
|
463
|
-
|
|
603
|
+
const summary = {
|
|
464
604
|
accountId: requireString(record, 'account_id', context),
|
|
465
605
|
authScheme: requireString(record, 'auth_scheme', context),
|
|
466
606
|
authorizedSignerCount: requireInteger(record, 'authorized_signer_count', context),
|
|
@@ -469,7 +609,16 @@ function parseAccountSummary(value, context) {
|
|
|
469
609
|
stateStatus: parseStateStatus(requireString(record, 'state_status', context), `${context}.state_status`),
|
|
470
610
|
createdAt: requireString(record, 'created_at', context),
|
|
471
611
|
updatedAt: requireString(record, 'updated_at', context),
|
|
612
|
+
pausedAt: requireNullableString(record, 'paused_at', context),
|
|
613
|
+
pausedReason: requireNullableString(record, 'paused_reason', context),
|
|
472
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;
|
|
473
622
|
}
|
|
474
623
|
function parseAccountDetail(value, context) {
|
|
475
624
|
const summary = parseAccountSummary(value, context);
|
|
@@ -481,6 +630,33 @@ function parseAccountDetail(value, context) {
|
|
|
481
630
|
stateUpdatedAt: requireNullableString(record, 'state_updated_at', context),
|
|
482
631
|
};
|
|
483
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
|
+
}
|
|
484
660
|
function parseErrorResponse(value) {
|
|
485
661
|
const record = asRecord(value, 'error response');
|
|
486
662
|
const success = requireBoolean(record, 'success', 'error response');
|
|
@@ -505,13 +681,64 @@ function parseErrorResponse(value) {
|
|
|
505
681
|
if (typeof codeValue !== 'string') {
|
|
506
682
|
throw new GuardianOperatorContractError('error response', 'code must be a string when present');
|
|
507
683
|
}
|
|
508
|
-
code = codeValue;
|
|
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;
|
|
509
732
|
}
|
|
510
733
|
return {
|
|
511
734
|
success: false,
|
|
512
735
|
code,
|
|
513
736
|
error: requireString(record, 'error', 'error response'),
|
|
514
737
|
retryAfterSecs,
|
|
738
|
+
missingPermissions,
|
|
739
|
+
retryable,
|
|
740
|
+
pausedAt,
|
|
741
|
+
pausedReason,
|
|
515
742
|
};
|
|
516
743
|
}
|
|
517
744
|
function parseStateStatus(value, context) {
|
|
@@ -585,6 +812,20 @@ function requireStringArray(record, key, context) {
|
|
|
585
812
|
return entry;
|
|
586
813
|
});
|
|
587
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
|
+
}
|
|
588
829
|
// ---------------------------------------------------------------------------
|
|
589
830
|
// Pagination helpers — feature `005-operator-dashboard-metrics`.
|
|
590
831
|
// ---------------------------------------------------------------------------
|
|
@@ -660,12 +901,24 @@ function parseDeltaEntry(value, context) {
|
|
|
660
901
|
}
|
|
661
902
|
entry.accountId = record.account_id;
|
|
662
903
|
}
|
|
904
|
+
if (record.category !== undefined && record.category !== null) {
|
|
905
|
+
entry.category = parseDeltaCategory(requireString(record, 'category', context), `${context}.category`);
|
|
906
|
+
}
|
|
663
907
|
if (record.proposal_type !== undefined) {
|
|
664
908
|
if (typeof record.proposal_type !== 'string') {
|
|
665
909
|
throw new GuardianOperatorContractError(context, 'proposal_type must be a string when present');
|
|
666
910
|
}
|
|
667
911
|
entry.proposalType = record.proposal_type;
|
|
668
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
|
+
}
|
|
669
922
|
return entry;
|
|
670
923
|
}
|
|
671
924
|
function parseProposalEntry(value, context) {
|
|
@@ -694,6 +947,254 @@ function parseProposalEntry(value, context) {
|
|
|
694
947
|
}
|
|
695
948
|
return entry;
|
|
696
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
|
+
}
|
|
697
1198
|
function parseDeltaStatus(value, context) {
|
|
698
1199
|
if (value === 'candidate' || value === 'canonical' || value === 'discarded') {
|
|
699
1200
|
return value;
|