@layerzerolabs/common-canton 0.2.105 → 0.2.106

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/src/events.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { buildTransactionWithResultSchema } from '@layerzerolabs/common-chain-model';
2
+ import { NonRetryableError } from '@layerzerolabs/common-error-utils';
2
3
 
3
4
  import type { ICantonClient } from './client';
4
5
  import type { CreatedEvent } from './open-api/models';
@@ -6,6 +7,16 @@ import { cantonTransactionSchema } from './transaction';
6
7
 
7
8
  const cantonTxWithResultSchema = buildTransactionWithResultSchema(cantonTransactionSchema);
8
9
 
10
+ // TODO(canton): confirm these poll values are enough to wait for archival across environments.
11
+ const REQUEST_ARCHIVAL_POLL_INTERVAL_MS = 2000;
12
+ const REQUEST_ARCHIVAL_MAX_ATTEMPTS = 60;
13
+
14
+ // IRequest interface choices that consume the Request contract via `archive reqCid` in their impls.
15
+ const IREQUEST_ACCEPT_CHOICE = 'IRequest_Accept';
16
+ const IREQUEST_REJECT_CHOICE = 'IRequest_Reject';
17
+
18
+ const REQUEST_TEMPLATE_QUALIFIED_NAME = 'Request:Request';
19
+
9
20
  /**
10
21
  * Fetches a transaction by updateId and returns all CreatedEvents.
11
22
  */
@@ -79,3 +90,127 @@ export const cantonDeploymentSucceeded = async (
79
90
  ): Promise<boolean> => {
80
91
  return (await findMatchingCreatedEventFromTx(client, txWithResult)) != null;
81
92
  };
93
+
94
+ /**
95
+ * Success check for register-style deploys (an exercise on Uln302 that creates a
96
+ * Request, which the validator then accepts or rejects in a later transaction).
97
+ *
98
+ * Positively confirms acceptance: locate the Request created by this submission,
99
+ * then delegate to waitForRequestArchival, which waits for the Request to be
100
+ * archived and verifies the IRequest_Accept choice ran (throwing on reject,
101
+ * unexpected archival, or timeout). This is a positive accept check rather than
102
+ * inferring success from the absence of a rejection event.
103
+ */
104
+ export const cantonRegisterSucceeded = async (
105
+ client: ICantonClient,
106
+ txWithResult: unknown,
107
+ ): Promise<boolean> => {
108
+ const parsed = cantonTxWithResultSchema.safeParse(txWithResult);
109
+ if (!parsed.success || parsed.data.result.type !== 'success') return false;
110
+
111
+ const updateId = parsed.data.result.minedTxHash;
112
+ if (!updateId) return false;
113
+
114
+ // Deferred-sender register payloads build with empty actAs (the sender is
115
+ // resolved at sign time), so fall back to the parties this user can read as.
116
+ const { actAs } = parsed.data.populatedTransaction;
117
+ const parties = actAs.length ? actAs : await client.listUserParties();
118
+ if (!parties.length) return false;
119
+
120
+ const createdEvents = await fetchCreatedEvents(client, updateId, parties);
121
+ const requestEvent = createdEvents.find((event) =>
122
+ event.templateId?.includes(REQUEST_TEMPLATE_QUALIFIED_NAME),
123
+ );
124
+ if (!requestEvent) return false;
125
+
126
+ try {
127
+ await waitForRequestArchival(client, requestEvent.contractId, parties);
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ };
133
+
134
+ /**
135
+ * Wait for a Request contract to be archived AND determine accept vs reject
136
+ * by inspecting which IRequest choice ran on it.
137
+ *
138
+ * IRequest_Accept and IRequest_Reject are nonconsuming choices that both end
139
+ * with `archive reqCid` in their implementations. We can't tell accept from
140
+ * reject from the archive alone — must look at the ExercisedEvent in the
141
+ * archive transaction tree.
142
+ *
143
+ * On accept, returns the IRequest_Accept choice argument (the accept call
144
+ * context, including `acceptContext`) so callers can extract the validator's
145
+ * internal address from it.
146
+ *
147
+ * Throws NonRetryableError on rejection, on archival via a non-Accept/Reject
148
+ * path (unexpected), or on timeout.
149
+ */
150
+ export const waitForRequestArchival = async (
151
+ client: ICantonClient,
152
+ requestContractId: string,
153
+ parties: string[],
154
+ ): Promise<unknown> => {
155
+ for (let attempt = 1; attempt <= REQUEST_ARCHIVAL_MAX_ATTEMPTS; attempt++) {
156
+ const { archived } = await client.fetchEventsByContractId(requestContractId, parties);
157
+ if (archived) {
158
+ const transaction = await client.fetchTransactionByOffset(archived.offset, parties);
159
+ // IRequest_Accept and IRequest_Reject are *nonconsuming* interface
160
+ // choices on IRequest. Their template impls (Request.daml's
161
+ // request_AcceptImpl / request_RejectImpl) end with `archive reqCid`,
162
+ // which Canton records as a consuming exercise of the implicit
163
+ // `Archive` choice. So in the transaction tree we see TWO exercises
164
+ // on the Request contract:
165
+ // - a non-consuming `IRequest_Accept` (or `IRequest_Reject`)
166
+ // - a consuming `Archive` (the actual archival)
167
+ // Scan for any IRequest_Accept/Reject exercise on it in the
168
+ // tree to determine the outcome, regardless of which one consumed.
169
+ let interfaceChoice: string | undefined;
170
+ let interfaceArg: unknown;
171
+ let consumingChoice: string | undefined;
172
+ for (const event of transaction.events ?? []) {
173
+ if (!('ExercisedEvent' in event)) continue;
174
+ const ex = event.ExercisedEvent;
175
+ if (ex.contractId !== requestContractId) continue;
176
+ if (ex.choice === IREQUEST_ACCEPT_CHOICE || ex.choice === IREQUEST_REJECT_CHOICE) {
177
+ interfaceChoice = ex.choice;
178
+ interfaceArg = ex.choiceArgument;
179
+ }
180
+ if (ex.consuming) {
181
+ consumingChoice = ex.choice;
182
+ }
183
+ }
184
+ if (interfaceChoice === IREQUEST_ACCEPT_CHOICE) {
185
+ return interfaceArg;
186
+ }
187
+ if (interfaceChoice === IREQUEST_REJECT_CHOICE) {
188
+ const rejectContext =
189
+ typeof interfaceArg === 'object' &&
190
+ interfaceArg !== null &&
191
+ 'rejectContext' in interfaceArg
192
+ ? interfaceArg.rejectContext
193
+ : undefined;
194
+ const reason = rejectContext
195
+ ? JSON.stringify(rejectContext).slice(0, 500)
196
+ : 'unknown';
197
+ throw new NonRetryableError(`Request rejected — rejectContext: ${reason}`);
198
+ }
199
+ // No IRequest_Accept/Reject ran on this Request — it was archived
200
+ // via some other path (default `Archive`, or another consuming
201
+ // choice). That's unexpected for register-style flows — treat as
202
+ // failure so we don't silently call this a success.
203
+ throw new NonRetryableError(
204
+ `Request ${requestContractId} archived via unexpected choice ${consumingChoice ?? '<none>'}`,
205
+ );
206
+ }
207
+ // Skip the sleep on the final attempt — there's no next iteration to
208
+ // wait for, so it would only delay the timeout error pointlessly.
209
+ if (attempt < REQUEST_ARCHIVAL_MAX_ATTEMPTS) {
210
+ await new Promise((resolve) => setTimeout(resolve, REQUEST_ARCHIVAL_POLL_INTERVAL_MS));
211
+ }
212
+ }
213
+ throw new NonRetryableError(
214
+ `Request archival timeout (${REQUEST_ARCHIVAL_MAX_ATTEMPTS} attempts) for ${requestContractId}`,
215
+ );
216
+ };
package/src/provider.ts CHANGED
@@ -24,9 +24,22 @@ export type CantonProvider = {
24
24
  client: ICantonClient;
25
25
  grpcClient: CantonGrpcClient;
26
26
  tokenProvider: TokenProvider;
27
- /** Validator wallet URL for scan-proxy CIP-56 endpoints. Required — amulet fee
28
- * payment needs it on every Canton transaction; enforced in {@link parseCantonUri}. */
27
+ /** Validator wallet URL for scan-proxy CIP-56 endpoints. Required. */
29
28
  walletUrl: string;
29
+ /** LayerZero sequencer base URL (e.g. "http://localhost:8080"); vApp JSON-RPC at `/vapp`. Required. */
30
+ sequencerUrl: string;
31
+ /** Sequencer validator pubkeys plus quorum for verifying signed read responses. */
32
+ sequencerCommittee?: SequencerCommittee;
33
+ };
34
+
35
+ /**
36
+ * Client-side committee config for secp256k1 sequencer read verification.
37
+ * Pubkeys must be hex-encoded with `0x` prefix in the same format returned by
38
+ * `recoverSecp256k1PublicKey`.
39
+ */
40
+ export type SequencerCommittee = {
41
+ publicKeys: `0x${string}`[];
42
+ quorum: number;
30
43
  };
31
44
 
32
45
  // ─────────────────────────────────────────────────────────────────────────────
@@ -40,8 +53,12 @@ export type CantonUriParams = {
40
53
  adminApiHost: string;
41
54
  /** Ledger API user ID (default: "participant_admin") */
42
55
  userId: string;
43
- /** Validator wallet URL for scan-proxy CIP-56 endpoints (e.g. "http://localhost:7201") */
56
+ /** Validator wallet URL for scan-proxy CIP-56 endpoints (e.g. "http://localhost:7201"). Required. */
44
57
  walletUrl: string;
58
+ /** LayerZero sequencer base URL (e.g. "http://localhost:8080"); vApp JSON-RPC at `/vapp`. Required. */
59
+ sequencerUrl: string;
60
+ /** Sequencer committee config parsed from the provider URI. */
61
+ sequencerCommittee?: SequencerCommittee;
45
62
  /** OAuth2 client credentials config — present only for non-local environments */
46
63
  oauth2?: ClientCredentialsTokenProviderConfig;
47
64
  };
@@ -59,32 +76,36 @@ export type CantonUriParams = {
59
76
  export function parseCantonUri(uri: string): CantonUriParams {
60
77
  const url = new URL(uri);
61
78
 
62
- // `admin-api` is a gRPC `host:port` (no scheme), so it carries no TLS signal.
63
- // TODO(CANTON): convey TLS for non-localhost (e.g. `&admin-api-tls=1` or `admin-api=grpcs://host:port`).
64
79
  const adminApiHost = url.searchParams.get('admin-api');
65
80
  if (!adminApiHost) {
66
81
  throw new Error('Canton provider URI missing required "admin-api" query parameter');
67
82
  }
68
83
 
69
84
  const userId = url.searchParams.get('user-id') ?? 'participant_admin';
70
-
71
- // Required: amulet fee payment hits the validator wallet scan-proxy on every
72
- // Canton transaction, so a provider without it can't transact. Fail at parse
73
- // time rather than guarding each downstream use.
74
85
  const walletUrl = url.searchParams.get('wallet-url');
75
86
  if (!walletUrl) {
76
87
  throw new Error('Canton provider URI missing required "wallet-url" query parameter');
77
88
  }
89
+ const sequencerUrl = url.searchParams.get('sequencer-url');
90
+ if (!sequencerUrl) {
91
+ throw new Error('Canton provider URI missing required "sequencer-url" query parameter');
92
+ }
93
+ const sequencerValidatorsParam = url.searchParams.get('sequencer-validators');
94
+ const sequencerQuorumParam = url.searchParams.get('sequencer-quorum');
78
95
  const tokenUrl = url.searchParams.get('token-url') ?? undefined;
79
96
  const clientId = url.searchParams.get('client-id') ?? undefined;
80
97
  const clientSecret = url.searchParams.get('client-secret') ?? undefined;
81
98
  const scope = url.searchParams.get('scope') ?? undefined;
82
99
  const audience = url.searchParams.get('audience') ?? undefined;
83
100
 
101
+ // Remove Canton params to get clean JSON API URL
84
102
  for (const key of [
85
103
  'admin-api',
86
104
  'user-id',
87
105
  'wallet-url',
106
+ 'sequencer-url',
107
+ 'sequencer-validators',
108
+ 'sequencer-quorum',
88
109
  'token-url',
89
110
  'client-id',
90
111
  'client-secret',
@@ -94,6 +115,46 @@ export function parseCantonUri(uri: string): CantonUriParams {
94
115
  url.searchParams.delete(key);
95
116
  }
96
117
 
118
+ let sequencerCommittee: SequencerCommittee | undefined;
119
+ if (sequencerValidatorsParam !== null && sequencerQuorumParam !== null) {
120
+ const publicKeys = sequencerValidatorsParam
121
+ .split(',')
122
+ .map((s) => s.trim().toLowerCase())
123
+ .filter((s) => s.length > 0);
124
+ if (publicKeys.length === 0) {
125
+ throw new Error(
126
+ 'Canton provider URI sequencer-validators must contain at least one pubkey',
127
+ );
128
+ }
129
+ for (const publicKey of publicKeys) {
130
+ if (!publicKey.startsWith('0x')) {
131
+ throw new Error(
132
+ `Canton provider URI sequencer-validators pubkey must start with 0x, got: ${publicKey.slice(0, 10)}...`,
133
+ );
134
+ }
135
+ }
136
+
137
+ const quorum = Number(sequencerQuorumParam);
138
+ if (!Number.isInteger(quorum) || quorum < 1) {
139
+ throw new Error(
140
+ `Canton provider URI sequencer-quorum must be a positive integer, got: ${sequencerQuorumParam}`,
141
+ );
142
+ }
143
+ if (quorum > publicKeys.length) {
144
+ throw new Error(
145
+ `Canton provider URI sequencer-quorum=${quorum} exceeds committee size ${publicKeys.length}`,
146
+ );
147
+ }
148
+ sequencerCommittee = {
149
+ publicKeys: publicKeys as `0x${string}`[],
150
+ quorum,
151
+ };
152
+ } else if (sequencerValidatorsParam !== null || sequencerQuorumParam !== null) {
153
+ throw new Error(
154
+ 'Canton provider URI must set both sequencer-validators and sequencer-quorum together',
155
+ );
156
+ }
157
+
97
158
  const jsonApiUrl = url.toString().replace(/\/$/, '');
98
159
 
99
160
  const oauth2 =
@@ -101,7 +162,15 @@ export function parseCantonUri(uri: string): CantonUriParams {
101
162
  ? { tokenUrl, clientId, clientSecret, scope, audience }
102
163
  : undefined;
103
164
 
104
- return { jsonApiUrl, adminApiHost, userId, walletUrl, oauth2 };
165
+ return {
166
+ jsonApiUrl,
167
+ adminApiHost,
168
+ userId,
169
+ walletUrl,
170
+ sequencerUrl,
171
+ sequencerCommittee,
172
+ oauth2,
173
+ };
105
174
  }
106
175
 
107
176
  // ─────────────────────────────────────────────────────────────────────────────
@@ -148,6 +217,8 @@ export function createCantonProvider({
148
217
  grpcClient,
149
218
  tokenProvider,
150
219
  walletUrl: params.walletUrl,
220
+ sequencerUrl: params.sequencerUrl,
221
+ sequencerCommittee: params.sequencerCommittee,
151
222
  };
152
223
  }
153
224
 
@@ -160,7 +231,7 @@ function createTokenProvider(params: CantonUriParams, isLocal: boolean): TokenPr
160
231
  });
161
232
  }
162
233
 
163
- // TODO(CANTON): add other prod auth methods (direct JWT passthrough, etc.).
234
+ // TODO(canton): add other prod auth methods (direct JWT passthrough, etc.).
164
235
  if (!params.oauth2) {
165
236
  throw new Error(
166
237
  'Canton provider requires OAuth2 credentials (token-url, client-id, client-secret) in the provider URI',
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../src/auth/client-credentials-token-provider', () => ({
4
+ ClientCredentialsTokenProvider: class {},
5
+ }));
6
+ vi.mock('../src/auth/local-token-provider', () => ({
7
+ LocalTokenProvider: class {},
8
+ }));
9
+ vi.mock('../src/client/canton-client', () => ({
10
+ CantonClient: class {},
11
+ }));
12
+ vi.mock('../src/client/grpc', () => ({
13
+ CantonGrpcClient: class {},
14
+ }));
15
+ vi.mock('../src/open-api/runtime', () => ({ Configuration: class {} }));
16
+
17
+ const BASE =
18
+ 'http://localhost:2975?admin-api=localhost:2902&wallet-url=http://localhost:2903&sequencer-url=http://localhost:2904';
19
+
20
+ const loadProvider = async () => import('../src/provider');
21
+
22
+ describe('parseCantonUri / sequencerCommittee', () => {
23
+ it('returns undefined when neither committee param is set', async () => {
24
+ const { parseCantonUri } = await loadProvider();
25
+ const params = parseCantonUri(BASE);
26
+ expect(params.sequencerCommittee).toBeUndefined();
27
+ });
28
+
29
+ it('parses validator pubkeys and quorum', async () => {
30
+ const { parseCantonUri } = await loadProvider();
31
+ const params = parseCantonUri(
32
+ `${BASE}&sequencer-validators=0x04AB,0x04Cd&sequencer-quorum=1`,
33
+ );
34
+ expect(params.sequencerCommittee).toEqual({
35
+ publicKeys: ['0x04ab', '0x04cd'],
36
+ quorum: 1,
37
+ });
38
+ });
39
+
40
+ it('throws when only one committee param is set', async () => {
41
+ const { parseCantonUri } = await loadProvider();
42
+ expect(() => parseCantonUri(`${BASE}&sequencer-validators=0x04ab`)).toThrow(
43
+ /set both sequencer-validators and sequencer-quorum together/,
44
+ );
45
+ expect(() => parseCantonUri(`${BASE}&sequencer-quorum=1`)).toThrow(
46
+ /set both sequencer-validators and sequencer-quorum together/,
47
+ );
48
+ });
49
+
50
+ it('validates quorum range', async () => {
51
+ const { parseCantonUri } = await loadProvider();
52
+ expect(() =>
53
+ parseCantonUri(`${BASE}&sequencer-validators=0x04ab&sequencer-quorum=0`),
54
+ ).toThrow(/positive integer/);
55
+ expect(() =>
56
+ parseCantonUri(`${BASE}&sequencer-validators=0x04ab&sequencer-quorum=2`),
57
+ ).toThrow(/exceeds committee size/);
58
+ });
59
+
60
+ it('strips committee params from jsonApiUrl', async () => {
61
+ const { parseCantonUri } = await loadProvider();
62
+ const params = parseCantonUri(
63
+ `${BASE}&foo=bar&sequencer-validators=0x04ab&sequencer-quorum=1`,
64
+ );
65
+ expect(params.jsonApiUrl).toBe('http://localhost:2975/?foo=bar');
66
+ });
67
+ });
package/dist/623CCK2L.js DELETED
@@ -1,37 +0,0 @@
1
- import { cantonTransactionSchema } from './3YCNHEVD.js';
2
- import { __name } from './VUOMXK5T.js';
3
- import { buildTransactionWithResultSchema } from '@layerzerolabs/common-chain-model';
4
-
5
- var cantonTxWithResultSchema = buildTransactionWithResultSchema(cantonTransactionSchema);
6
- var fetchCreatedEvents = /* @__PURE__ */ __name(async (client, updateId, parties) => {
7
- const cantonTx = await client.fetchTransactionByUpdateId(updateId, parties);
8
- return cantonTx.events?.filter((event) => "CreatedEvent" in event).map((event) => event.CreatedEvent) ?? [];
9
- }, "fetchCreatedEvents");
10
- var findMatchingCreatedEvent = /* @__PURE__ */ __name((createdEvents, expectedTemplateId, expectedSignatories) => {
11
- const expectedSet = new Set(expectedSignatories);
12
- return createdEvents.find((event) => {
13
- if (event.templateId !== expectedTemplateId) return false;
14
- const signatories = event.signatories ?? [];
15
- return signatories.length > 0 && signatories.every((s) => expectedSet.has(s));
16
- });
17
- }, "findMatchingCreatedEvent");
18
- var findMatchingCreatedEventFromTx = /* @__PURE__ */ __name(async (client, tx) => {
19
- if (!tx) return null;
20
- const parsed = cantonTxWithResultSchema.safeParse(tx);
21
- if (!parsed.success) return null;
22
- if (parsed.data.result.type !== "success") return null;
23
- const { commands, actAs } = parsed.data.populatedTransaction;
24
- const updateId = parsed.data.result.minedTxHash;
25
- if (!updateId || !actAs.length) return null;
26
- const createCommand = commands[0];
27
- if (!createCommand || !("createCommand" in createCommand)) return null;
28
- const createdEvents = await fetchCreatedEvents(client, updateId, actAs);
29
- return findMatchingCreatedEvent(createdEvents, createCommand.createCommand.templateId, actAs) ?? null;
30
- }, "findMatchingCreatedEventFromTx");
31
- var cantonDeploymentSucceeded = /* @__PURE__ */ __name(async (client, txWithResult) => {
32
- return await findMatchingCreatedEventFromTx(client, txWithResult) != null;
33
- }, "cantonDeploymentSucceeded");
34
-
35
- export { cantonDeploymentSucceeded, fetchCreatedEvents, findMatchingCreatedEvent, findMatchingCreatedEventFromTx };
36
- //# sourceMappingURL=623CCK2L.js.map
37
- //# sourceMappingURL=623CCK2L.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/events.ts"],"names":["cantonTxWithResultSchema","buildTransactionWithResultSchema","cantonTransactionSchema","fetchCreatedEvents","client","updateId","parties","cantonTx","fetchTransactionByUpdateId","events","filter","event","map","CreatedEvent","findMatchingCreatedEvent","createdEvents","expectedTemplateId","expectedSignatories","expectedSet","Set","find","templateId","signatories","length","every","s","has","findMatchingCreatedEventFromTx","tx","parsed","safeParse","success","data","result","type","commands","actAs","populatedTransaction","minedTxHash","createCommand","cantonDeploymentSucceeded","txWithResult"],"mappings":";;;;AAMA,IAAMA,wBAAAA,GAA2BC,iCAAiCC,uBAAAA,CAAAA;AAK3D,IAAMC,kBAAAA,mBAAqB,MAAA,CAAA,OAC9BC,MAAAA,EACAC,QAAAA,EACAC,OAAAA,KAAAA;AAEA,EAAA,MAAMC,QAAAA,GAAW,MAAMH,MAAAA,CAAOI,0BAAAA,CAA2BH,UAAUC,OAAAA,CAAAA;AACnE,EAAA,OACIC,QAAAA,CAASE,MAAAA,EACHC,MAAAA,CAAO,CAACC,UAAmD,cAAA,IAAkBA,KAAAA,CAAAA,CAC9EC,GAAAA,CAAI,CAACD,KAAAA,KAAUA,KAAAA,CAAME,YAAY,KAAK,EAAA;AAEnD,CAAA,EAXkC,oBAAA;AAiB3B,IAAMC,wBAAAA,mBAA2B,MAAA,CAAA,CACpCC,aAAAA,EACAC,kBAAAA,EACAC,mBAAAA,KAAAA;AAEA,EAAA,MAAMC,WAAAA,GAAc,IAAIC,GAAAA,CAAIF,mBAAAA,CAAAA;AAC5B,EAAA,OAAOF,aAAAA,CAAcK,IAAAA,CAAK,CAACT,KAAAA,KAAAA;AACvB,IAAA,IAAIA,KAAAA,CAAMU,UAAAA,KAAeL,kBAAAA,EAAoB,OAAO,KAAA;AACpD,IAAA,MAAMM,WAAAA,GAAcX,KAAAA,CAAMW,WAAAA,IAAe,EAAA;AACzC,IAAA,OAAOA,WAAAA,CAAYC,MAAAA,GAAS,CAAA,IAAKD,WAAAA,CAAYE,KAAAA,CAAM,CAACC,CAAAA,KAAMP,WAAAA,CAAYQ,GAAAA,CAAID,CAAAA,CAAAA,CAAAA;EAC9E,CAAA,CAAA;AACJ,CAAA,EAXwC,0BAAA;AAkBjC,IAAME,8BAAAA,mBAAiC,MAAA,CAAA,OAC1CvB,MAAAA,EACAwB,EAAAA,KAAAA;AAEA,EAAA,IAAI,CAACA,IAAI,OAAO,IAAA;AAEhB,EAAA,MAAMC,MAAAA,GAAS7B,wBAAAA,CAAyB8B,SAAAA,CAAUF,EAAAA,CAAAA;AAClD,EAAA,IAAI,CAACC,MAAAA,CAAOE,OAAAA,EAAS,OAAO,IAAA;AAC5B,EAAA,IAAIF,MAAAA,CAAOG,IAAAA,CAAKC,MAAAA,CAAOC,IAAAA,KAAS,WAAW,OAAO,IAAA;AAElD,EAAA,MAAM,EAAEC,QAAAA,EAAUC,KAAAA,EAAK,GAAKP,OAAOG,IAAAA,CAAKK,oBAAAA;AACxC,EAAA,MAAMhC,QAAAA,GAAWwB,MAAAA,CAAOG,IAAAA,CAAKC,MAAAA,CAAOK,WAAAA;AAEpC,EAAA,IAAI,CAACjC,QAAAA,IAAY,CAAC+B,KAAAA,CAAMb,QAAQ,OAAO,IAAA;AAEvC,EAAA,MAAMgB,aAAAA,GAAgBJ,SAAS,CAAA,CAAA;AAC/B,EAAA,IAAI,CAACI,aAAAA,IAAiB,EAAE,eAAA,IAAmBA,gBAAgB,OAAO,IAAA;AAElE,EAAA,MAAMxB,aAAAA,GAAgB,MAAMZ,kBAAAA,CAAmBC,MAAAA,EAAQC,UAAU+B,KAAAA,CAAAA;AACjE,EAAA,OACItB,yBAAyBC,aAAAA,EAAewB,aAAAA,CAAcA,aAAAA,CAAclB,UAAAA,EAAYe,KAAAA,CAAAA,IAChF,IAAA;AAER,CAAA,EAvB8C,gCAAA;AA6BvC,IAAMI,yBAAAA,mBAA4B,MAAA,CAAA,OACrCpC,MAAAA,EACAqC,YAAAA,KAAAA;AAEA,EAAA,OAAQ,MAAMd,8BAAAA,CAA+BvB,MAAAA,EAAQqC,YAAAA,CAAAA,IAAkB,IAAA;AAC3E,CAAA,EALyC,2BAAA","file":"623CCK2L.js","sourcesContent":["import { buildTransactionWithResultSchema } from '@layerzerolabs/common-chain-model';\n\nimport type { ICantonClient } from './client';\nimport type { CreatedEvent } from './open-api/models';\nimport { cantonTransactionSchema } from './transaction';\n\nconst cantonTxWithResultSchema = buildTransactionWithResultSchema(cantonTransactionSchema);\n\n/**\n * Fetches a transaction by updateId and returns all CreatedEvents.\n */\nexport const fetchCreatedEvents = async (\n client: ICantonClient,\n updateId: string,\n parties: string[],\n): Promise<CreatedEvent[]> => {\n const cantonTx = await client.fetchTransactionByUpdateId(updateId, parties);\n return (\n cantonTx.events\n ?.filter((event): event is { CreatedEvent: CreatedEvent } => 'CreatedEvent' in event)\n .map((event) => event.CreatedEvent) ?? []\n );\n};\n\n/**\n * Finds a CreatedEvent matching the expected templateId whose signatories\n * are a subset of the provided actAs parties.\n */\nexport const findMatchingCreatedEvent = (\n createdEvents: CreatedEvent[],\n expectedTemplateId: string,\n expectedSignatories: string[],\n): CreatedEvent | undefined => {\n const expectedSet = new Set(expectedSignatories);\n return createdEvents.find((event) => {\n if (event.templateId !== expectedTemplateId) return false;\n const signatories = event.signatories ?? [];\n return signatories.length > 0 && signatories.every((s) => expectedSet.has(s));\n });\n};\n\n/**\n * Parses a Canton transaction-with-result, fetches the created events from the ledger,\n * and returns the first CreatedEvent matching the submitted command's templateId and actAs\n * signatories. Returns null if the transaction is falsy, not successful, or has no match.\n */\nexport const findMatchingCreatedEventFromTx = async (\n client: ICantonClient,\n tx: unknown,\n): Promise<CreatedEvent | null> => {\n if (!tx) return null;\n\n const parsed = cantonTxWithResultSchema.safeParse(tx);\n if (!parsed.success) return null;\n if (parsed.data.result.type !== 'success') return null;\n\n const { commands, actAs } = parsed.data.populatedTransaction;\n const updateId = parsed.data.result.minedTxHash;\n\n if (!updateId || !actAs.length) return null;\n\n const createCommand = commands[0];\n if (!createCommand || !('createCommand' in createCommand)) return null;\n\n const createdEvents = await fetchCreatedEvents(client, updateId, actAs);\n return (\n findMatchingCreatedEvent(createdEvents, createCommand.createCommand.templateId, actAs) ??\n null\n );\n};\n\n/**\n * Checks that a Canton deploy transaction resulted in a CreatedEvent\n * matching the submitted command's templateId and actAs signatories.\n */\nexport const cantonDeploymentSucceeded = async (\n client: ICantonClient,\n txWithResult: unknown,\n): Promise<boolean> => {\n return (await findMatchingCreatedEventFromTx(client, txWithResult)) != null;\n};\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/provider.ts"],"names":["parseCantonUri","uri","url","URL","adminApiHost","searchParams","get","Error","userId","walletUrl","tokenUrl","undefined","clientId","clientSecret","scope","audience","key","delete","jsonApiUrl","toString","replace","oauth2","createCantonProvider","isLocal","params","tokenProvider","createTokenProvider","configuration","Configuration","basePath","accessToken","getToken","client","CantonClient","grpcClient","CantonGrpcClient","host","LocalTokenProvider","actAs","admin","ClientCredentialsTokenProvider"],"mappings":"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","file":"MDLCDBAQ.js","sourcesContent":["import {\n ClientCredentialsTokenProvider,\n type ClientCredentialsTokenProviderConfig,\n} from './auth/client-credentials-token-provider';\nimport { LocalTokenProvider } from './auth/local-token-provider';\nimport type { TokenProvider } from './auth/token-provider';\nimport { CantonClient } from './client/canton-client';\nimport { CantonGrpcClient } from './client/grpc';\nimport type { ICantonClient } from './client/interface';\nimport { Configuration } from './open-api/runtime';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Type\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Composite provider for Canton — bundles both transport layers and shared auth.\n *\n * - {@link client} — JSON Ledger API (commands, queries, updates)\n * - {@link grpcClient} — gRPC Admin API (topology transactions, namespace queries)\n * - {@link tokenProvider} — Shared Bearer-token source used by both transports\n */\nexport type CantonProvider = {\n client: ICantonClient;\n grpcClient: CantonGrpcClient;\n tokenProvider: TokenProvider;\n /** Validator wallet URL for scan-proxy CIP-56 endpoints. Required — amulet fee\n * payment needs it on every Canton transaction; enforced in {@link parseCantonUri}. */\n walletUrl: string;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// URI Parsing\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type CantonUriParams = {\n /** Base URL of the JSON Ledger API (e.g. \"http://localhost:5975\") */\n jsonApiUrl: string;\n /** Host:port of the gRPC Admin API (e.g. \"localhost:5076\") */\n adminApiHost: string;\n /** Ledger API user ID (default: \"participant_admin\") */\n userId: string;\n /** Validator wallet URL for scan-proxy CIP-56 endpoints (e.g. \"http://localhost:7201\") */\n walletUrl: string;\n /** OAuth2 client credentials config — present only for non-local environments */\n oauth2?: ClientCredentialsTokenProviderConfig;\n};\n\n/**\n * Parse a Canton provider URI into its constituent parts.\n *\n * URI format:\n * `<json-api-base>?admin-api=<host:port>&user-id=<id>[&token-url=...&client-id=...&client-secret=...&scope=...&audience=...]`\n *\n * The base URL (everything before the query string) is the JSON Ledger API endpoint.\n * Canton-specific params are extracted and removed; any remaining query params are\n * preserved on the JSON API URL.\n */\nexport function parseCantonUri(uri: string): CantonUriParams {\n const url = new URL(uri);\n\n // `admin-api` is a gRPC `host:port` (no scheme), so it carries no TLS signal.\n // TODO(CANTON): convey TLS for non-localhost (e.g. `&admin-api-tls=1` or `admin-api=grpcs://host:port`).\n const adminApiHost = url.searchParams.get('admin-api');\n if (!adminApiHost) {\n throw new Error('Canton provider URI missing required \"admin-api\" query parameter');\n }\n\n const userId = url.searchParams.get('user-id') ?? 'participant_admin';\n\n // Required: amulet fee payment hits the validator wallet scan-proxy on every\n // Canton transaction, so a provider without it can't transact. Fail at parse\n // time rather than guarding each downstream use.\n const walletUrl = url.searchParams.get('wallet-url');\n if (!walletUrl) {\n throw new Error('Canton provider URI missing required \"wallet-url\" query parameter');\n }\n const tokenUrl = url.searchParams.get('token-url') ?? undefined;\n const clientId = url.searchParams.get('client-id') ?? undefined;\n const clientSecret = url.searchParams.get('client-secret') ?? undefined;\n const scope = url.searchParams.get('scope') ?? undefined;\n const audience = url.searchParams.get('audience') ?? undefined;\n\n for (const key of [\n 'admin-api',\n 'user-id',\n 'wallet-url',\n 'token-url',\n 'client-id',\n 'client-secret',\n 'scope',\n 'audience',\n ]) {\n url.searchParams.delete(key);\n }\n\n const jsonApiUrl = url.toString().replace(/\\/$/, '');\n\n const oauth2 =\n tokenUrl && clientId && clientSecret\n ? { tokenUrl, clientId, clientSecret, scope, audience }\n : undefined;\n\n return { jsonApiUrl, adminApiHost, userId, walletUrl, oauth2 };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Provider Factory\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type CreateCantonProviderOptions = {\n /** Raw provider URI (from provider config) */\n uri: string;\n /** When true, uses LocalTokenProvider (self-signed JWTs) instead of OAuth2 */\n isLocal: boolean;\n};\n\n/**\n * Create a {@link CantonProvider} from a provider URI.\n *\n * - **isLocal=true**: Uses {@link LocalTokenProvider} with admin-level access (self-signed JWTs).\n * - **isLocal=false**: Uses {@link ClientCredentialsTokenProvider} with OAuth2 client\n * credentials extracted from the URI query parameters.\n */\nexport function createCantonProvider({\n uri,\n isLocal,\n}: CreateCantonProviderOptions): CantonProvider {\n const params = parseCantonUri(uri);\n\n const tokenProvider = createTokenProvider(params, isLocal);\n\n // ── JSON Ledger API Client ──────────────────────────────────────────\n const configuration = new Configuration({\n basePath: params.jsonApiUrl,\n accessToken: () => tokenProvider.getToken(),\n });\n const client = new CantonClient(configuration, params.userId);\n\n // ── gRPC Admin API Client ───────────────────────────────────────────\n const grpcClient = new CantonGrpcClient({\n host: params.adminApiHost,\n tokenProvider,\n });\n\n return {\n client,\n grpcClient,\n tokenProvider,\n walletUrl: params.walletUrl,\n };\n}\n\nfunction createTokenProvider(params: CantonUriParams, isLocal: boolean): TokenProvider {\n if (isLocal) {\n return new LocalTokenProvider({\n userId: params.userId,\n actAs: [],\n admin: true,\n });\n }\n\n // TODO(CANTON): add other prod auth methods (direct JWT passthrough, etc.).\n if (!params.oauth2) {\n throw new Error(\n 'Canton provider requires OAuth2 credentials (token-url, client-id, client-secret) in the provider URI',\n );\n }\n\n return new ClientCredentialsTokenProvider(params.oauth2);\n}\n"]}