@layerzerolabs/common-canton 1.2.8 → 1.2.9

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.
Files changed (37) hide show
  1. package/.turbo/turbo-build.log +639 -639
  2. package/.turbo/turbo-test.log +50 -15
  3. package/dist/3K5JL2TP.js +71 -0
  4. package/dist/3K5JL2TP.js.map +1 -0
  5. package/dist/{SLTAV2JF.js → ANKLDEGA.js} +3 -3
  6. package/dist/{SLTAV2JF.js.map → ANKLDEGA.js.map} +1 -1
  7. package/dist/{NRSV2VHQ.js → AWR6H7TM.js} +3 -3
  8. package/dist/{NRSV2VHQ.js.map → AWR6H7TM.js.map} +1 -1
  9. package/dist/{ESNOKXKB.js → DGVHAULK.js} +4 -4
  10. package/dist/{ESNOKXKB.js.map → DGVHAULK.js.map} +1 -1
  11. package/dist/{4G6A7TFX.js → DSYVONFP.js} +3 -3
  12. package/dist/{4G6A7TFX.js.map → DSYVONFP.js.map} +1 -1
  13. package/dist/{ST5U5WZ4.js → FFZGONTK.js} +4 -4
  14. package/dist/{ST5U5WZ4.js.map → FFZGONTK.js.map} +1 -1
  15. package/dist/{3ZCDWNJG.js → HTVAEYFB.js} +72 -128
  16. package/dist/HTVAEYFB.js.map +1 -0
  17. package/dist/client/amulet.js +2 -2
  18. package/dist/client/canton-client.d.ts +1 -1
  19. package/dist/client/canton-client.d.ts.map +1 -1
  20. package/dist/client/canton-client.js +3 -3
  21. package/dist/client/grpc.js +2 -2
  22. package/dist/client/index.js +4 -4
  23. package/dist/errors.d.ts +21 -0
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +1 -1
  26. package/dist/index.js +10 -10
  27. package/dist/provider.js +5 -5
  28. package/dist/refresh/index.js +5 -5
  29. package/dist/refresh/resolve.js +4 -4
  30. package/dist/sdk/canton-sdk.js +2 -2
  31. package/dist/sdk/index.js +2 -2
  32. package/package.json +10 -10
  33. package/src/client/canton-client.ts +110 -131
  34. package/src/errors.ts +51 -0
  35. package/dist/3ZCDWNJG.js.map +0 -1
  36. package/dist/A7Z5O57E.js +0 -34
  37. package/dist/A7Z5O57E.js.map +0 -1
@@ -2,7 +2,12 @@ import type { ContractId } from '@daml/types';
2
2
  import { once } from 'es-toolkit';
3
3
  import { randomUUID } from 'node:crypto';
4
4
 
5
- import { CantonProtocolError, CantonTransportError } from '../errors';
5
+ import {
6
+ CantonApiError,
7
+ cantonApiErrorFromResponse,
8
+ CantonProtocolError,
9
+ CantonTransportError,
10
+ } from '../errors';
6
11
  import {
7
12
  DefaultApi,
8
13
  type PostV2CommandsAsyncSubmitRequest,
@@ -25,7 +30,7 @@ import {
25
30
  type JsActiveContract,
26
31
  JsActiveContractFromJSON,
27
32
  } from '../open-api/models';
28
- import type { Configuration } from '../open-api/runtime';
33
+ import { type Configuration, ResponseError } from '../open-api/runtime';
29
34
  import type {
30
35
  ExternalSignature,
31
36
  PreparedTransaction,
@@ -121,33 +126,18 @@ export class CantonClient implements ICantonClient {
121
126
  }
122
127
 
123
128
  /**
124
- * Try to extract error body from API response.
125
- * Returns the parsed body or null if extraction fails.
129
+ * Run an API call, normalizing a thrown `ResponseError` into a `CantonApiError`
130
+ * that carries `errorCategory` (for retry classification). Other errors pass through.
126
131
  */
127
- async #extractErrorBody(err: unknown): Promise<Record<string, unknown> | null> {
128
- if (err && typeof err === 'object' && 'response' in err) {
129
- const resp = (err as { response: Response }).response;
130
- if (resp && typeof resp.json === 'function') {
131
- return (await resp.json().catch(() => null)) as Record<string, unknown> | null;
132
+ async #invoke<T>(call: () => Promise<T>): Promise<T> {
133
+ try {
134
+ return await call();
135
+ } catch (err) {
136
+ if (err instanceof ResponseError) {
137
+ throw await cantonApiErrorFromResponse(err.response);
132
138
  }
139
+ throw err;
133
140
  }
134
- return null;
135
- }
136
-
137
- /**
138
- * Throw a CantonProtocolError with extracted error details from API response.
139
- * Handles the common pattern of extracting JSON error details from failed requests.
140
- *
141
- * @param err - The caught error
142
- * @param context - Description of the operation (e.g., "DAR upload", "Party allocation")
143
- * @throws CantonProtocolError with extracted error details, or re-throws original error
144
- */
145
- async #throwApiError(err: unknown, context: string): Promise<never> {
146
- const body = await this.#extractErrorBody(err);
147
- if (body) {
148
- throw new CantonProtocolError(`${context} failed: ${JSON.stringify(body)}`);
149
- }
150
- throw err;
151
141
  }
152
142
 
153
143
  /**
@@ -235,7 +225,7 @@ export class CantonClient implements ICantonClient {
235
225
  * Returns all connected synchronizer IDs.
236
226
  */
237
227
  async connectedSynchronizers(): Promise<string[]> {
238
- const response = await this.#api.getV2StateConnectedSynchronizers();
228
+ const response = await this.#invoke(() => this.#api.getV2StateConnectedSynchronizers());
239
229
  const synchronizers = response.connectedSynchronizers ?? [];
240
230
  return synchronizers.map((s) => s.synchronizerId).filter((id): id is string => !!id);
241
231
  }
@@ -283,19 +273,17 @@ export class CantonClient implements ICantonClient {
283
273
  };
284
274
 
285
275
  try {
286
- return await this.#api.postV2Dars(request);
276
+ return await this.#invoke(() => this.#api.postV2Dars(request));
287
277
  } catch (err: unknown) {
288
278
  // If skipIfVetted, treat KNOWN_PACKAGE_VERSION as a no-op success
289
- if (opts.skipIfVetted) {
290
- const body = await this.#extractErrorBody(err);
291
- if (body) {
292
- if (body.code === 'KNOWN_PACKAGE_VERSION') {
293
- return { skipped: true, reason: 'already_vetted', details: body };
294
- }
295
- throw new CantonProtocolError(`DAR upload failed: ${JSON.stringify(body)}`);
296
- }
279
+ if (
280
+ opts.skipIfVetted &&
281
+ err instanceof CantonApiError &&
282
+ err.code === 'KNOWN_PACKAGE_VERSION'
283
+ ) {
284
+ return { skipped: true, reason: 'already_vetted', details: err.body };
297
285
  }
298
- throw await this.#throwApiError(err, 'DAR upload');
286
+ throw err;
299
287
  }
300
288
  }
301
289
 
@@ -321,11 +309,11 @@ export class CantonClient implements ICantonClient {
321
309
 
322
310
  if (!request.synchronizerId) {
323
311
  return this.#ensureSynchronizerId().then((synchronizerId) =>
324
- this.#api.postV2DarsValidate({ ...request, synchronizerId }),
312
+ this.#invoke(() => this.#api.postV2DarsValidate({ ...request, synchronizerId })),
325
313
  );
326
314
  }
327
315
 
328
- return this.#api.postV2DarsValidate(request);
316
+ return this.#invoke(() => this.#api.postV2DarsValidate(request));
329
317
  }
330
318
 
331
319
  // ─────────────────────────────────────────────────────────────────────────
@@ -342,10 +330,12 @@ export class CantonClient implements ICantonClient {
342
330
  async listParties(
343
331
  opts: { pageSize?: number; pageToken?: string } = {},
344
332
  ): Promise<{ parties: string[]; nextPageToken: string }> {
345
- const response = await this.#api.getV2Parties({
346
- pageSize: opts.pageSize,
347
- pageToken: opts.pageToken,
348
- });
333
+ const response = await this.#invoke(() =>
334
+ this.#api.getV2Parties({
335
+ pageSize: opts.pageSize,
336
+ pageToken: opts.pageToken,
337
+ }),
338
+ );
349
339
 
350
340
  return {
351
341
  parties: response.partyDetails?.map((p) => p.party) ?? [],
@@ -363,9 +353,11 @@ export class CantonClient implements ICantonClient {
363
353
  * ourselves to preserve the actual variant payload.
364
354
  */
365
355
  async listUserRights(): Promise<UserRight[]> {
366
- const apiResponse = await this.#api.getV2UsersUserIdRightsRaw({
367
- userId: this.#userId,
368
- });
356
+ const apiResponse = await this.#invoke(() =>
357
+ this.#api.getV2UsersUserIdRightsRaw({
358
+ userId: this.#userId,
359
+ }),
360
+ );
369
361
  const raw = (await apiResponse.raw.json()) as { rights?: UserRight[] };
370
362
  return raw.rights ?? [];
371
363
  }
@@ -411,19 +403,16 @@ export class CantonClient implements ICantonClient {
411
403
  ): Promise<string> {
412
404
  const synchronizerId = opts.synchronizerId ?? (await this.#ensureSynchronizerId());
413
405
 
414
- let response;
415
- try {
416
- response = await this.#api.postV2Parties({
406
+ const response = await this.#invoke(() =>
407
+ this.#api.postV2Parties({
417
408
  allocatePartyRequest: {
418
409
  partyIdHint,
419
410
  synchronizerId,
420
411
  userId: opts.userId ?? '',
421
412
  identityProviderId: opts.identityProviderId ?? '',
422
413
  },
423
- });
424
- } catch (err: unknown) {
425
- throw await this.#throwApiError(err, 'Party allocation');
426
- }
414
+ }),
415
+ );
427
416
 
428
417
  const party = response.partyDetails?.party;
429
418
  if (!party) {
@@ -460,12 +449,9 @@ export class CantonClient implements ICantonClient {
460
449
  },
461
450
  };
462
451
 
463
- let response;
464
- try {
465
- response = await this.#api.postV2PartiesExternalGenerateTopology(request);
466
- } catch (err: unknown) {
467
- throw await this.#throwApiError(err, 'External party topology generation');
468
- }
452
+ const response = await this.#invoke(() =>
453
+ this.#api.postV2PartiesExternalGenerateTopology(request),
454
+ );
469
455
 
470
456
  if (!response.partyId || !response.multiHash) {
471
457
  throw new CantonProtocolError(
@@ -513,12 +499,7 @@ export class CantonClient implements ICantonClient {
513
499
  },
514
500
  };
515
501
 
516
- let response;
517
- try {
518
- response = await this.#api.postV2PartiesExternalAllocate(request);
519
- } catch (err: unknown) {
520
- throw await this.#throwApiError(err, 'External party allocation');
521
- }
502
+ const response = await this.#invoke(() => this.#api.postV2PartiesExternalAllocate(request));
522
503
 
523
504
  if (!response.partyId) {
524
505
  throw new CantonProtocolError('allocateExternalParty response missing partyId');
@@ -534,18 +515,16 @@ export class CantonClient implements ICantonClient {
534
515
  * POST /v2/users/{userId}/rights
535
516
  */
536
517
  async grantCanActAs(partyId: string): Promise<void> {
537
- try {
538
- await this.#api.postV2UsersUserIdRights({
518
+ await this.#invoke(() =>
519
+ this.#api.postV2UsersUserIdRights({
539
520
  userId: this.#userId,
540
521
  grantUserRightsRequest: {
541
522
  userId: this.#userId,
542
523
  rights: [{ kind: { canActAs: { value: { party: partyId } } } }],
543
524
  identityProviderId: '',
544
525
  },
545
- });
546
- } catch (err: unknown) {
547
- throw await this.#throwApiError(err, `Grant canActAs for party ${partyId}`);
548
- }
526
+ }),
527
+ );
549
528
  }
550
529
 
551
530
  /**
@@ -575,12 +554,9 @@ export class CantonClient implements ICantonClient {
575
554
  },
576
555
  };
577
556
 
578
- let response;
579
- try {
580
- response = await this.#api.postV2InteractiveSubmissionPreferredPackages(request);
581
- } catch (err: unknown) {
582
- throw await this.#throwApiError(err, 'Package lookup');
583
- }
557
+ const response = await this.#invoke(() =>
558
+ this.#api.postV2InteractiveSubmissionPreferredPackages(request),
559
+ );
584
560
 
585
561
  const packageRef = response.packageReferences?.find(
586
562
  (ref) => ref.packageName === packageName,
@@ -612,7 +588,7 @@ export class CantonClient implements ICantonClient {
612
588
  const commandId = opts.commandId ?? randomUUID();
613
589
 
614
590
  // Snapshot ledger end before submission so waitForCommand knows where to start polling
615
- const ledgerEndResponse = await this.#api.getV2StateLedgerEnd();
591
+ const ledgerEndResponse = await this.#invoke(() => this.#api.getV2StateLedgerEnd());
616
592
  const completionOffset = ledgerEndResponse.offset;
617
593
 
618
594
  const request: PostV2CommandsAsyncSubmitRequest = {
@@ -627,11 +603,7 @@ export class CantonClient implements ICantonClient {
627
603
  },
628
604
  };
629
605
 
630
- try {
631
- await this.#api.postV2CommandsAsyncSubmit(request);
632
- } catch (err: unknown) {
633
- throw await this.#throwApiError(err, 'Command submission');
634
- }
606
+ await this.#invoke(() => this.#api.postV2CommandsAsyncSubmit(request));
635
607
 
636
608
  return { commandId, completionOffset };
637
609
  }
@@ -669,7 +641,9 @@ export class CantonClient implements ICantonClient {
669
641
  streamIdleTimeoutMs,
670
642
  };
671
643
 
672
- const rawResponse = await this.#api.postV2CommandsCompletionsRaw(request);
644
+ const rawResponse = await this.#invoke(() =>
645
+ this.#api.postV2CommandsCompletionsRaw(request),
646
+ );
673
647
  const responseText = await rawResponse.raw.text();
674
648
  const completions = JSON.parse(responseText) as unknown[];
675
649
 
@@ -782,7 +756,9 @@ export class CantonClient implements ICantonClient {
782
756
  },
783
757
  };
784
758
 
785
- const response = await this.#api.postV2EventsEventsByContractId(request);
759
+ const response = await this.#invoke(() =>
760
+ this.#api.postV2EventsEventsByContractId(request),
761
+ );
786
762
 
787
763
  // If archived, contract is not active
788
764
  if (response.archived) return undefined;
@@ -849,7 +825,9 @@ export class CantonClient implements ICantonClient {
849
825
  },
850
826
  };
851
827
 
852
- const response = await this.#api.postV2EventsEventsByContractId(request);
828
+ const response = await this.#invoke(() =>
829
+ this.#api.postV2EventsEventsByContractId(request),
830
+ );
853
831
 
854
832
  return {
855
833
  created: response.created?.createdEvent,
@@ -867,12 +845,14 @@ export class CantonClient implements ICantonClient {
867
845
  updateId: string,
868
846
  parties: string[],
869
847
  ): Promise<CantonTransaction> {
870
- const response = await this.#api.postV2UpdatesUpdateByIdRaw({
871
- getUpdateByIdRequest: {
872
- updateId,
873
- updateFormat: this.#buildUpdateFormat(parties),
874
- },
875
- });
848
+ const response = await this.#invoke(() =>
849
+ this.#api.postV2UpdatesUpdateByIdRaw({
850
+ getUpdateByIdRequest: {
851
+ updateId,
852
+ updateFormat: this.#buildUpdateFormat(parties),
853
+ },
854
+ }),
855
+ );
876
856
 
877
857
  const updateResponse: CantonUpdateResponse = await response.raw.json();
878
858
  return this.#parseTransactionFromUpdate(
@@ -888,12 +868,14 @@ export class CantonClient implements ICantonClient {
888
868
  * @returns The full transaction including recordTime
889
869
  */
890
870
  async fetchTransactionByOffset(offset: number, parties: string[]): Promise<CantonTransaction> {
891
- const response = await this.#api.postV2UpdatesUpdateByOffsetRaw({
892
- getUpdateByOffsetRequest: {
893
- offset,
894
- updateFormat: this.#buildUpdateFormat(parties),
895
- },
896
- });
871
+ const response = await this.#invoke(() =>
872
+ this.#api.postV2UpdatesUpdateByOffsetRaw({
873
+ getUpdateByOffsetRequest: {
874
+ offset,
875
+ updateFormat: this.#buildUpdateFormat(parties),
876
+ },
877
+ }),
878
+ );
897
879
 
898
880
  const updateResponse: CantonUpdateResponse = await response.raw.json();
899
881
  return this.#parseTransactionFromUpdate(
@@ -943,12 +925,9 @@ export class CantonClient implements ICantonClient {
943
925
  },
944
926
  };
945
927
 
946
- let response;
947
- try {
948
- response = await this.#api.postV2InteractiveSubmissionPrepare(request);
949
- } catch (error: unknown) {
950
- throw await this.#throwApiError(error, 'Interactive submission prepare');
951
- }
928
+ const response = await this.#invoke(() =>
929
+ this.#api.postV2InteractiveSubmissionPrepare(request),
930
+ );
952
931
  const validated = this.#validatePrepareResponse(response);
953
932
 
954
933
  return {
@@ -969,7 +948,7 @@ export class CantonClient implements ICantonClient {
969
948
  signatures: ExternalSignature[],
970
949
  ): Promise<SubmitResult> {
971
950
  // Snapshot ledger end so the caller can poll for completion from this point
972
- const ledgerEndResponse = await this.#api.getV2StateLedgerEnd();
951
+ const ledgerEndResponse = await this.#invoke(() => this.#api.getV2StateLedgerEnd());
973
952
  const completionOffset = ledgerEndResponse.offset;
974
953
 
975
954
  const request: PostV2InteractiveSubmissionExecuteRequest = {
@@ -1000,11 +979,7 @@ export class CantonClient implements ICantonClient {
1000
979
  },
1001
980
  };
1002
981
 
1003
- try {
1004
- await this.#api.postV2InteractiveSubmissionExecute(request);
1005
- } catch (error: unknown) {
1006
- throw await this.#throwApiError(error, 'Interactive submission execute');
1007
- }
982
+ await this.#invoke(() => this.#api.postV2InteractiveSubmissionExecute(request));
1008
983
 
1009
984
  return { commandId: prepared.commandId, completionOffset };
1010
985
  }
@@ -1017,7 +992,7 @@ export class CantonClient implements ICantonClient {
1017
992
  * Get the current ledger end offset.
1018
993
  */
1019
994
  async getLedgerEndOffset(): Promise<number> {
1020
- const response = await this.#api.getV2StateLedgerEnd();
995
+ const response = await this.#invoke(() => this.#api.getV2StateLedgerEnd());
1021
996
  return response.offset;
1022
997
  }
1023
998
 
@@ -1096,14 +1071,16 @@ export class CantonClient implements ICantonClient {
1096
1071
  parties.map((party) => [party, { cumulative: cumulativeFilter }]),
1097
1072
  );
1098
1073
 
1099
- const rawResponse = await this.#api.postV2StateActiveContractsRaw({
1100
- getActiveContractsRequest: {
1101
- filter: { filtersByParty },
1102
- verbose: true,
1103
- activeAtOffset: offset,
1104
- },
1105
- limit: opts?.limit,
1106
- });
1074
+ const rawResponse = await this.#invoke(() =>
1075
+ this.#api.postV2StateActiveContractsRaw({
1076
+ getActiveContractsRequest: {
1077
+ filter: { filtersByParty },
1078
+ verbose: true,
1079
+ activeAtOffset: offset,
1080
+ },
1081
+ limit: opts?.limit,
1082
+ }),
1083
+ );
1107
1084
 
1108
1085
  // Raw response is an array of streamed entries; extract and deserialize contracts
1109
1086
  type RawActiveContractsResponse = Array<{
@@ -1144,20 +1121,22 @@ export class CantonClient implements ICantonClient {
1144
1121
  },
1145
1122
  verbose: false,
1146
1123
  };
1147
- const rawResponse = await this.#api.postV2UpdatesRaw({
1148
- getUpdatesRequest: {
1149
- beginExclusive,
1150
- endInclusive,
1151
- verbose: false,
1152
- updateFormat: {
1153
- includeTransactions: {
1154
- eventFormat,
1155
- transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS' as const,
1124
+ const rawResponse = await this.#invoke(() =>
1125
+ this.#api.postV2UpdatesRaw({
1126
+ getUpdatesRequest: {
1127
+ beginExclusive,
1128
+ endInclusive,
1129
+ verbose: false,
1130
+ updateFormat: {
1131
+ includeTransactions: {
1132
+ eventFormat,
1133
+ transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS' as const,
1134
+ },
1156
1135
  },
1157
1136
  },
1158
- },
1159
- limit: opts?.limit,
1160
- });
1137
+ limit: opts?.limit,
1138
+ }),
1139
+ );
1161
1140
 
1162
1141
  const updateResponses: CantonUpdateResponse[] = await rawResponse.raw.json();
1163
1142
 
package/src/errors.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * Base error class for Canton SDK errors.
3
5
  */
@@ -31,3 +33,52 @@ export class CantonProtocolError extends CantonError {
31
33
  super(message);
32
34
  }
33
35
  }
36
+
37
+ /** A JSON Ledger API error, normalized from a non-2xx response's `JsCantonError` body. */
38
+ export class CantonApiError extends CantonError {
39
+ constructor(
40
+ message: string,
41
+ /** Canton error category (1–14). See {@link isRetryableCantonError}. */
42
+ public readonly errorCategory: number | undefined,
43
+ /** Canton error code id, e.g. `CONTRACT_NOT_FOUND`. */
44
+ public readonly code: string | undefined,
45
+ public readonly status: number,
46
+ public readonly body: unknown,
47
+ ) {
48
+ super(message);
49
+ }
50
+ }
51
+
52
+ const cantonApiErrorBodySchema = z.object({
53
+ errorCategory: z.number(),
54
+ code: z.string(),
55
+ cause: z.string(),
56
+ });
57
+
58
+ /** Build a {@link CantonApiError} from a non-2xx `Response`, reading a clone of the body. */
59
+ export async function cantonApiErrorFromResponse(response: Response): Promise<CantonApiError> {
60
+ const body = await response.json().catch(() => undefined);
61
+ const fields = cantonApiErrorBodySchema.safeParse(body).data;
62
+ const message = `Canton API error ${response.status}${fields?.code ? ` [${fields.code}]` : ''}${
63
+ fields?.cause ? `: ${fields.cause}` : ''
64
+ }`;
65
+ return new CantonApiError(message, fields?.errorCategory, fields?.code, response.status, body);
66
+ }
67
+
68
+ // Canton's `errorCategory` (1–14) is the retry signal, not the HTTP status.
69
+ // See https://docs.canton.network/appdev/reference/error-codes
70
+ const NON_RETRYABLE_CANTON_ERROR_CATEGORIES: ReadonlySet<number> = new Set([
71
+ 5, 9, 10, 11, 12, 13, 14,
72
+ ]);
73
+
74
+ /**
75
+ * Non-retryable Categories: 5, 9, 10, 11, 12, 13, 14. Everything else is retryable.
76
+ * Errors with no category (transport/connection failures) are treated as retryable.
77
+ */
78
+ export function isRetryableCantonError(error: unknown): boolean {
79
+ return (
80
+ !(error instanceof CantonApiError) ||
81
+ error.errorCategory === undefined ||
82
+ !NON_RETRYABLE_CANTON_ERROR_CATEGORIES.has(error.errorCategory)
83
+ );
84
+ }