From bc21a0a8ad6ab85ee65fa5c1f8f69c71567af054 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:01:14 +0100 Subject: [PATCH 01/17] add basic canton support with json api client --- ccip-sdk/package.json | 3 +- ccip-sdk/src/all-chains.ts | 2 + ccip-sdk/src/canton/client/client.ts | 274 + .../canton/client/generated/ledger-api.d.ts | 7636 +++++++++++++++++ ccip-sdk/src/canton/client/index.ts | 25 + ccip-sdk/src/canton/index.ts | 467 + ccip-sdk/src/chain.ts | 1 + ccip-sdk/src/errors/codes.ts | 3 + ccip-sdk/src/selectors.ts | 35 + ccip-sdk/src/types.ts | 3 +- package-lock.json | 99 +- 11 files changed, 8533 insertions(+), 15 deletions(-) create mode 100644 ccip-sdk/src/canton/client/client.ts create mode 100644 ccip-sdk/src/canton/client/generated/ledger-api.d.ts create mode 100644 ccip-sdk/src/canton/client/index.ts create mode 100644 ccip-sdk/src/canton/index.ts diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 7842cf0d..8425fcd4 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -77,7 +77,6 @@ }, "dependencies": { "@aptos-labs/ts-sdk": "^5.2.1", - "buffer": "^6.0.3", "@coral-xyz/anchor": "^0.29.0", "@mysten/bcs": "^1.9.2", "@mysten/sui": "^1.45.2", @@ -90,10 +89,12 @@ "bn.js": "^5.2.3", "borsh": "^2.0.0", "bs58": "^6.0.0", + "buffer": "^6.0.3", "ethers": "6.16.0", "got": "^11.8.6", "micro-memoize": "^5.1.1", "type-fest": "^5.5.0", + "openapi-fetch": "^0.17.0", "yaml": "2.8.2" }, "overrides": { diff --git a/ccip-sdk/src/all-chains.ts b/ccip-sdk/src/all-chains.ts index c9658428..4e97aec9 100644 --- a/ccip-sdk/src/all-chains.ts +++ b/ccip-sdk/src/all-chains.ts @@ -5,6 +5,7 @@ */ import { AptosChain } from './aptos/index.ts' +import { CantonChain } from './canton/index.ts' import { EVMChain } from './evm/index.ts' import { SolanaChain } from './solana/index.ts' import { SuiChain } from './sui/index.ts' @@ -21,6 +22,7 @@ export const allSupportedChains = { [ChainFamily.Aptos]: AptosChain, [ChainFamily.Sui]: SuiChain, [ChainFamily.TON]: TONChain, + [ChainFamily.Canton]: CantonChain, } export { supportedChains } from './supported-chains.ts' diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts new file mode 100644 index 00000000..b67bab59 --- /dev/null +++ b/ccip-sdk/src/canton/client/client.ts @@ -0,0 +1,274 @@ +import createClient from 'openapi-fetch' + +import type { components, paths } from './generated/ledger-api.ts' +import { CCIPError } from '../../errors/CCIPError.ts' +import { CCIPErrorCode } from '../../errors/codes.ts' + +// Re-export useful types from the generated schema +export type JsCommands = components['schemas']['JsCommands'] +export type Command = components['schemas']['Command'] +export type SubmitAndWaitResponse = components['schemas']['SubmitAndWaitResponse'] +export type JsSubmitAndWaitForTransactionResponse = + components['schemas']['JsSubmitAndWaitForTransactionResponse'] +export type GetActiveContractsRequest = components['schemas']['GetActiveContractsRequest'] +export type JsGetActiveContractsResponse = components['schemas']['JsGetActiveContractsResponse'] +export type JsActiveContract = components['schemas']['JsActiveContract'] +export type CreatedEvent = components['schemas']['CreatedEvent'] +export type JsCantonError = components['schemas']['JsCantonError'] +export type TransactionFilter = components['schemas']['TransactionFilter'] +export type EventFormat = components['schemas']['EventFormat'] +export type TemplateFilter = components['schemas']['TemplateFilter'] +export type WildcardFilter = components['schemas']['WildcardFilter'] +export type ConnectedSynchronizer = components['schemas']['ConnectedSynchronizer'] +export type GetConnectedSynchronizersResponse = + components['schemas']['GetConnectedSynchronizersResponse'] + +/** + * Configuration for the Canton Ledger API client + */ +export interface CantonClientConfig { + /** Base URL of the Canton JSON Ledger API (e.g., http://localhost:7575) */ + baseUrl: string + /** Optional bearer token for authentication */ + token?: string + /** Request timeout in milliseconds */ + timeout?: number +} + +/** + * Create a typed Canton Ledger API client + */ +export function createCantonClient(config: CantonClientConfig) { + const client = createClient({ + baseUrl: config.baseUrl, + headers: config.token ? { Authorization: `Bearer ${config.token}` } : undefined, + }) + + return { + /** + * Raw openapi-fetch client for advanced usage + */ + raw: client, + + /** + * Submit a command and wait for completion + * @returns The update ID and completion offset + */ + async submitAndWait(commands: JsCommands): Promise { + const { data, error } = await client.POST('/v2/commands/submit-and-wait', { + body: commands, + }) + + if (error || !data) { + throw new CantonApiError('submitAndWait failed', error) + } + + return data + }, + + /** + * Submit a command and wait for the full transaction response + * @returns The transaction with all created/archived events + */ + async submitAndWaitForTransaction( + commands: JsCommands, + eventFormat?: EventFormat, + ): Promise { + const { data, error } = await client.POST('/v2/commands/submit-and-wait-for-transaction', { + body: { + commands, + eventFormat, + }, + }) + + if (error || !data) { + throw new CantonApiError('submitAndWaitForTransaction failed', error) + } + + return data + }, + + /** + * Query active contracts on the ledger + * @returns Array of active contracts matching the filter + */ + async getActiveContracts( + request: GetActiveContractsRequest, + options?: { limit?: number }, + ): Promise { + const { data, error } = await client.POST('/v2/state/active-contracts', { + body: request, + params: { + query: options?.limit ? { limit: options.limit } : undefined, + }, + }) + + if (error || !data) { + throw new CantonApiError('getActiveContracts failed', error) + } + + return data + }, + + /** + * Get the current ledger end offset + */ + async getLedgerEnd(): Promise<{ offset: number }> { + const { data, error } = await client.GET('/v2/state/ledger-end') + + if (error || !data) { + throw new CantonApiError('getLedgerEnd failed', error) + } + + return { offset: data.offset ?? 0 } + }, + + /** + * List known parties on the participant + */ + async listParties(options?: { filterParty?: string }) { + const { data, error } = await client.GET('/v2/parties', { + params: { + query: options?.filterParty ? { 'filter-party': options.filterParty } : undefined, + }, + }) + + if (error || !data) { + throw new CantonApiError('listParties failed', error) + } + + return data.partyDetails + }, + + /** + * Get the participant ID + */ + async getParticipantId(): Promise { + const { data, error } = await client.GET('/v2/parties/participant-id') + + if (error || !data) { + throw new CantonApiError('getParticipantId failed', error) + } + + return data.participantId ?? '' + }, + + /** + * Get the list of synchronizers the participant is currently connected to + */ + async getConnectedSynchronizers(): Promise { + const { data, error } = await client.GET('/v2/state/connected-synchronizers') + + if (error || !data) { + throw new CantonApiError('getConnectedSynchronizers failed', error) + } + + return data.connectedSynchronizers ?? [] + }, + + /** + * Check if the ledger API is alive + */ + async isAlive(): Promise { + try { + const { error } = await client.GET('/livez') + return !error + } catch { + return false + } + }, + + /** + * Check if the ledger API is ready + */ + async isReady(): Promise { + try { + const { data, error } = await client.GET('/readyz') + return !error && data !== undefined + } catch { + return false + } + }, + + /** + * Get update by ID + * @param updateId - The update ID returned from submit-and-wait + * @param party - The party ID to filter events for + * @returns The full update with all events + */ + async getUpdateById(updateId: string, party: string): Promise { + const { data, error } = await client.POST('/v2/updates/update-by-id', { + body: { + updateId, + updateFormat: { + includeTransactions: { + eventFormat: { + filtersByParty: { + [party]: { + cumulative: [ + { + identifierFilter: { + WildcardFilter: { + value: { + includeCreatedEventBlob: false, + }, + }, + }, + }, + ], + }, + }, + verbose: true, + }, + transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS', + }, + }, + }, + }) + + if (error || !data) { + throw new CantonApiError('getUpdateById failed', error) + } + + return data + }, + } +} + +/** + * Custom error class for Canton API errors + */ +export class CantonApiError extends CCIPError { + override readonly name = 'CantonApiError' + + constructor(message: string, error: unknown, statusCode?: number) { + const context: Record = {} + let fullMessage = message + + if (statusCode !== undefined) { + context['statusCode'] = statusCode + } + + if (typeof error === 'object' && error !== null && 'code' in error) { + const cantonError = error as JsCantonError + context['cantonCode'] = cantonError.code + context['cantonCause'] = cantonError.cause + fullMessage = `${message}: [${cantonError.code}] ${cantonError.cause}` + } else if (typeof error === 'string') { + fullMessage = `${message}: ${error}` + context['errorDetail'] = error + } else if (error != null) { + context['error'] = error + } + + super(CCIPErrorCode.CANTON_API_ERROR, fullMessage, { + cause: error instanceof Error ? error : undefined, + context, + }) + } +} + +/** + * Type alias for the Canton client instance + */ +export type CantonClient = ReturnType diff --git a/ccip-sdk/src/canton/client/generated/ledger-api.d.ts b/ccip-sdk/src/canton/client/generated/ledger-api.d.ts new file mode 100644 index 00000000..b1e6d785 --- /dev/null +++ b/ccip-sdk/src/canton/client/generated/ledger-api.d.ts @@ -0,0 +1,7636 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + * + * ref: https://github.com/digital-asset/canton/blob/main/community/ledger/ledger-json-api/src/test/resources/json-api-docs/openapi.yaml + */ + +export interface paths { + '/v2/commands/submit-and-wait': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Submits a single composite command and waits for its result. + * Propagates the gRPC error of failed submissions including Daml interpretation errors. + */ + post: operations['postV2CommandsSubmit-and-wait'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/submit-and-wait-for-transaction': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Submits a single composite command, waits for its result, and returns the transaction. + * Propagates the gRPC error of failed submissions including Daml interpretation errors. + */ + post: operations['postV2CommandsSubmit-and-wait-for-transaction'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/submit-and-wait-for-reassignment': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Submits a single composite reassignment command, waits for its result, and returns the reassignment. + * Propagates the gRPC error of failed submission. + */ + post: operations['postV2CommandsSubmit-and-wait-for-reassignment'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/submit-and-wait-for-transaction-tree': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Submit a batch of commands and wait for the transaction trees response. Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use submit-and-wait-for-transaction instead. + */ + post: operations['postV2CommandsSubmit-and-wait-for-transaction-tree'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/async/submit': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Submit a single composite command. */ + post: operations['postV2CommandsAsyncSubmit'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/async/submit-reassignment': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Submit a single reassignment. */ + post: operations['postV2CommandsAsyncSubmit-reassignment'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/commands/completions': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Query completions list (blocking call) + * + * Subscribe to command completion events. + * Notice: This endpoint should be used for small results set. + * When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + * there will be an error (`413 Content Too Large`) returned. + * Increasing this limit may lead to performance issues and high memory consumption. + * Consider using websockets (asyncapi) for better efficiency with larger results. + */ + post: operations['postV2CommandsCompletions'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/events/events-by-contract-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Get the create and the consuming exercise event for the contract with the provided ID. + * No events will be returned for contracts that have been pruned because they + * have already been archived before the latest pruning offset. + * If the contract cannot be found for the request, or all the contract-events are filtered, a CONTRACT_EVENTS_NOT_FOUND error will be raised. + */ + post: operations['postV2EventsEvents-by-contract-id'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/version': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Read the Ledger API version */ + get: operations['getV2Version'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/dars/validate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Validates the DAR and checks the upgrade compatibility of the DAR's packages + * with the set of the already vetted packages on the target vetting synchronizer. + * See ValidateDarFileRequest for details regarding the target vetting synchronizer. + * + * The operation has no effect on the state of the participant or the Canton ledger: + * the DAR payload and its packages are not persisted neither are the packages vetted. + */ + post: operations['postV2DarsValidate'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/dars': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Upload a DAR to the participant node */ + post: operations['postV2Dars'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/packages': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Returns the identifiers of all supported packages. */ + get: operations['getV2Packages'] + put?: never + /** + * @description Behaves the same as /dars. This endpoint will be deprecated and removed in a future release. + * Upload a DAR file to the participant. + * + * If vetting is enabled in the request, the DAR is checked for upgrade compatibility + * with the set of the already vetted packages on the target vetting synchronizer + * See UploadDarFileRequest for details regarding vetting and the target vetting synchronizer. + */ + post: operations['postV2Packages'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/packages/{package-id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Returns the contents of a single package. */ + get: operations['getV2PackagesPackage-id'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/packages/{package-id}/status': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Returns the status of a single package. */ + get: operations['getV2PackagesPackage-idStatus'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/package-vetting': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description Lists which participant node vetted what packages on which synchronizer. + * Can be called by any authenticated user. + */ + get: operations['getV2Package-vetting'] + put?: never + /** @description Update the vetted packages of this participant */ + post: operations['postV2Package-vetting'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/parties': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description List the parties known by the participant. + * The list returned contains parties whose ledger access is facilitated by + * the participant and the ones maintained elsewhere. + */ + get: operations['getV2Parties'] + put?: never + /** + * @description Allocates a new party on a ledger and adds it to the set managed by the participant. + * Caller specifies a party identifier suggestion, the actual identifier + * allocated might be different and is implementation specific. + * Caller can specify party metadata that is stored locally on the participant. + * This call may: + * + * - Succeed, in which case the actual allocated identifier is visible in + * the response. + * - Respond with a gRPC error + * + * daml-on-kv-ledger: suggestion's uniqueness is checked by the validators in + * the consensus layer and call rejected if the identifier is already present. + * canton: completely different globally unique identifier is allocated. + * Behind the scenes calls to an internal protocol are made. As that protocol + * is richer than the surface protocol, the arguments take implicit values + * The party identifier suggestion must be a valid party name. Party names are required to be non-empty US-ASCII strings built from letters, digits, space, + * colon, minus and underscore limited to 255 chars + */ + post: operations['postV2Parties'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/parties/external/allocate': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Alpha 3.3: Endpoint to allocate a new external party on a synchronizer + * + * Expected to be stable in 3.5 + * + * The external party must be hosted (at least) on this node with either confirmation or observation permissions + * It can optionally be hosted on other nodes (then called a multi-hosted party). + * If hosted on additional nodes, explicit authorization of the hosting relationship must be performed on those nodes + * before the party can be used. + * Decentralized namespaces are supported but must be provided fully authorized by their owners. + * The individual owner namespace transactions can be submitted in the same call (fully authorized as well). + * In the simple case of a non-multi hosted, non-decentralized party, the RPC will return once the party is + * effectively allocated and ready to use, similarly to the AllocateParty behavior. + * For more complex scenarios applications may need to query the party status explicitly (only through the admin API as of now). + */ + post: operations['postV2PartiesExternalAllocate'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/parties/participant-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description Return the identifier of the participant. + * All horizontally scaled replicas should return the same id. + * daml-on-kv-ledger: returns an identifier supplied on command line at launch time + * canton: returns globally unique identifier of the participant + */ + get: operations['getV2PartiesParticipant-id'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/parties/{party}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description Get the party details of the given parties. Only known parties will be + * returned in the list. + */ + get: operations['getV2PartiesParty'] + put?: never + post?: never + delete?: never + options?: never + head?: never + /** + * @description Update selected modifiable participant-local attributes of a party details resource. + * Can update the participant's local information for local parties. + */ + patch: operations['patchV2PartiesParty'] + trace?: never + } + '/v2/parties/external/generate-topology': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Alpha 3.3: Convenience endpoint to generate topology transactions for external signing + * + * Expected to be stable in 3.5 + * + * You may use this endpoint to generate the common external topology transactions + * which can be signed externally and uploaded as part of the allocate party process + * + * Note that this request will create a normal namespace using the same key for the + * identity as for signing. More elaborate schemes such as multi-signature + * or decentralized parties require you to construct the topology transactions yourself. + */ + post: operations['postV2PartiesExternalGenerate-topology'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/state/active-contracts': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Query active contracts list (blocking call). + * Querying active contracts is an expensive operation and if possible should not be repeated often. + * Consider querying active contracts initially (for a given offset) + * and then repeatedly call one of `/v2/updates/...`endpoints to get subsequent modifications. + * You can also use websockets to get updates with better performance. + * + * Returns a stream of the snapshot of the active contracts and incomplete (un)assignments at a ledger offset. + * Once the stream of GetActiveContractsResponses completes, + * the client SHOULD begin streaming updates from the update service, + * starting at the GetActiveContractsRequest.active_at_offset specified in this request. + * Clients SHOULD NOT assume that the set of active contracts they receive reflects the state at the ledger end. + * + * Notice: This endpoint should be used for small results set. + * When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + * there will be an error (`413 Content Too Large`) returned. + * Increasing this limit may lead to performance issues and high memory consumption. + * Consider using websockets (asyncapi) for better efficiency with larger results. + */ + post: operations['postV2StateActive-contracts'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/state/connected-synchronizers': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the list of connected synchronizers at the time of the query. */ + get: operations['getV2StateConnected-synchronizers'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/state/ledger-end': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description Get the current ledger end. + * Subscriptions started with the returned offset will serve events after this RPC was called. + */ + get: operations['getV2StateLedger-end'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/state/latest-pruned-offsets': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the latest successfully pruned ledger offsets */ + get: operations['getV2StateLatest-pruned-offsets'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Read the ledger's filtered update stream for the specified contents and filters. + * It returns the event types in accordance with the stream contents selected. Also the selection criteria + * for individual events depends on the transaction shape chosen. + * + * - ACS delta: a requesting party must be a stakeholder of an event for it to be included. + * - ledger effects: a requesting party must be a witness of an event for it to be included. + * Notice: This endpoint should be used for small results set. + * When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + * there will be an error (`413 Content Too Large`) returned. + * Increasing this limit may lead to performance issues and high memory consumption. + * Consider using websockets (asyncapi) for better efficiency with larger results. + */ + post: operations['postV2Updates'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/flats': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Query flat transactions update list (blocking call). Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates instead. + * Notice: This endpoint should be used for small results set. + * When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + * there will be an error (`413 Content Too Large`) returned. + * Increasing this limit may lead to performance issues and high memory consumption. + * Consider using websockets (asyncapi) for better efficiency with larger results. + */ + post: operations['postV2UpdatesFlats'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/trees': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Query update transactions tree list (blocking call). Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates instead. + * Notice: This endpoint should be used for small results set. + * When number of results exceeded node configuration limit (`http-list-max-elements-limit`) + * there will be an error (`413 Content Too Large`) returned. + * Increasing this limit may lead to performance issues and high memory consumption. + * Consider using websockets (asyncapi) for better efficiency with larger results. + */ + post: operations['postV2UpdatesTrees'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/transaction-tree-by-offset/{offset}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @deprecated + * @description Get transaction tree by offset. Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates/update-by-offset instead. + */ + get: operations['getV2UpdatesTransaction-tree-by-offsetOffset'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/transaction-by-offset': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Get transaction by offset. Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates/update-by-offset instead. + */ + post: operations['postV2UpdatesTransaction-by-offset'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/update-by-offset': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Lookup an update by its offset. + * If there is no update with this offset, or all the events are filtered, an UPDATE_NOT_FOUND error will be raised. + */ + post: operations['postV2UpdatesUpdate-by-offset'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/transaction-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @deprecated + * @description Get transaction by id. Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates/update-by-id instead. + */ + post: operations['postV2UpdatesTransaction-by-id'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/update-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Lookup an update by its ID. + * If there is no update with this ID, or all the events are filtered, an UPDATE_NOT_FOUND error will be raised. + */ + post: operations['postV2UpdatesUpdate-by-id'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/updates/transaction-tree-by-id/{update-id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @deprecated + * @description Get transaction tree by id. Provided for backwards compatibility, it will be removed in the Canton version 3.5.0, use v2/updates/update-by-id instead. + */ + get: operations['getV2UpdatesTransaction-tree-by-idUpdate-id'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/users': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all existing users. */ + get: operations['getV2Users'] + put?: never + /** @description Create a new user. */ + post: operations['postV2Users'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/users/{user-id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the user data of a specific user or the authenticated user. */ + get: operations['getV2UsersUser-id'] + put?: never + post?: never + /** @description Delete an existing user and all its rights. */ + delete: operations['deleteV2UsersUser-id'] + options?: never + head?: never + /** @description Update selected modifiable attribute of a user resource described by the ``User`` message. */ + patch: operations['patchV2UsersUser-id'] + trace?: never + } + '/v2/authenticated-user': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the user data of the current authenticated user. */ + get: operations['getV2Authenticated-user'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/users/{user-id}/rights': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List the set of all rights granted to a user. */ + get: operations['getV2UsersUser-idRights'] + put?: never + /** + * @description Grant rights to a user. + * Granting rights does not affect the resource version of the corresponding user. + */ + post: operations['postV2UsersUser-idRights'] + delete?: never + options?: never + head?: never + /** + * @description Revoke rights from a user. + * Revoking rights does not affect the resource version of the corresponding user. + */ + patch: operations['patchV2UsersUser-idRights'] + trace?: never + } + '/v2/users/{user-id}/identity-provider-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + post?: never + delete?: never + options?: never + head?: never + /** @description Update the assignment of a user from one IDP to another. */ + patch: operations['patchV2UsersUser-idIdentity-provider-id'] + trace?: never + } + '/v2/idps': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description List all existing identity provider configurations. */ + get: operations['getV2Idps'] + put?: never + /** + * @description Create a new identity provider configuration. + * The request will fail if the maximum allowed number of separate configurations is reached. + */ + post: operations['postV2Idps'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/idps/{idp-id}': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Get the identity provider configuration data by id. */ + get: operations['getV2IdpsIdp-id'] + put?: never + post?: never + /** @description Delete an existing identity provider configuration. */ + delete: operations['deleteV2IdpsIdp-id'] + options?: never + head?: never + /** + * @description Update selected modifiable attribute of an identity provider config resource described + * by the ``IdentityProviderConfig`` message. + */ + patch: operations['patchV2IdpsIdp-id'] + trace?: never + } + '/v2/interactive-submission/prepare': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** @description Requires `readAs` scope for the submitting party when LAPI User authorization is enabled */ + post: operations['postV2Interactive-submissionPrepare'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/interactive-submission/execute': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Execute a prepared submission _asynchronously_ on the ledger. + * Requires a signature of the transaction from the submitting external party. + */ + post: operations['postV2Interactive-submissionExecute'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/interactive-submission/executeAndWait': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Similar to ExecuteSubmission but _synchronously_ wait for the completion of the transaction + * IMPORTANT: Relying on the response from this endpoint requires trusting the Participant Node to be honest. + * A malicious node could make a successfully committed request appeared failed and vice versa + */ + post: operations['postV2Interactive-submissionExecuteandwait'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/interactive-submission/executeAndWaitForTransaction': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Similar to ExecuteSubmissionAndWait but additionally returns the transaction + * IMPORTANT: Relying on the response from this endpoint requires trusting the Participant Node to be honest. + * A malicious node could make a successfully committed request appear as failed and vice versa + */ + post: operations['postV2Interactive-submissionExecuteandwaitfortransaction'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/interactive-submission/preferred-package-version': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** + * @description A preferred package is the highest-versioned package for a provided package-name + * that is vetted by all the participants hosting the provided parties. + * + * Ledger API clients should use this endpoint for constructing command submissions + * that are compatible with the provided preferred package, by making informed decisions on: + * - which are the compatible packages that can be used to create contracts + * - which contract or exercise choice argument version can be used in the command + * - which choices can be executed on a template or interface of a contract + * + * Can be accessed by any Ledger API client with a valid token when Ledger API authorization is enabled. + * + * Provided for backwards compatibility, it will be removed in the Canton version 3.4.0 + */ + get: operations['getV2Interactive-submissionPreferred-package-version'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/interactive-submission/preferred-packages': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Compute the preferred packages for the vetting requirements in the request. + * A preferred package is the highest-versioned package for a provided package-name + * that is vetted by all the participants hosting the provided parties. + * + * Ledger API clients should use this endpoint for constructing command submissions + * that are compatible with the provided preferred packages, by making informed decisions on: + * - which are the compatible packages that can be used to create contracts + * - which contract or exercise choice argument version can be used in the command + * - which choices can be executed on a template or interface of a contract + * + * If the package preferences could not be computed due to no selection satisfying the requirements, + * a `FAILED_PRECONDITION` error will be returned. + * + * Can be accessed by any Ledger API client with a valid token when Ledger API authorization is enabled. + * + * Experimental API: this endpoint is not guaranteed to provide backwards compatibility in future releases + */ + post: operations['postV2Interactive-submissionPreferred-packages'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/livez': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Checks if the service is alive */ + get: operations['getLivez'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/readyz': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + /** @description Checks if the service is ready to serve requests */ + get: operations['getReadyz'] + put?: never + post?: never + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } + '/v2/contracts/contract-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + get?: never + put?: never + /** + * @description Looking up contract data by contract ID. + * This endpoint is experimental / alpha, therefore no backwards compatibility is guaranteed. + * This endpoint must not be used to look up contracts which entered the participant via party replication + * or repair service. + */ + post: operations['postV2ContractsContract-by-id'] + delete?: never + options?: never + head?: never + patch?: never + trace?: never + } +} +export type webhooks = Record +export interface components { + schemas: { + /** + * AllocateExternalPartyRequest + * @description Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(identity_provider_id)`` + */ + AllocateExternalPartyRequest: { + /** + * @description TODO(#27670) support synchronizer aliases + * Synchronizer ID on which to onboard the party + * Required + */ + synchronizer: string + /** + * @description TopologyTransactions to onboard the external party + * Can contain: + * - A namespace for the party. + * This can be either a single NamespaceDelegation, + * or DecentralizedNamespaceDefinition along with its authorized namespace owners in the form of NamespaceDelegations. + * May be provided, if so it must be fully authorized by the signatures in this request combined with the existing topology state. + * - A PartyToKeyMapping to register the party's signing keys. + * May be provided, if so it must be fully authorized by the signatures in this request combined with the existing topology state. + * - A PartyToParticipant to register the hosting relationship of the party. + * Must be provided. + * Required + */ + onboardingTransactions: components['schemas']['SignedTransaction'][] + /** + * @description Optional signatures of the combined hash of all onboarding_transactions + * This may be used instead of providing signatures on each individual transaction + */ + multiHashSignatures?: components['schemas']['Signature'][] + /** + * @description The id of the ``Identity Provider`` + * If not set, assume the party is managed by the default identity provider. + * Optional + */ + identityProviderId?: string + } + /** AllocateExternalPartyResponse */ + AllocateExternalPartyResponse: { + partyId?: string + } + /** + * AllocatePartyRequest + * @description Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(identity_provider_id)`` + */ + AllocatePartyRequest: { + /** + * @description A hint to the participant which party ID to allocate. It can be + * ignored. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Optional + */ + partyIdHint?: string + /** + * @description Formerly "display_name" + * Participant-local metadata to be stored in the ``PartyDetails`` of this newly allocated party. + * Optional + */ + localMetadata?: components['schemas']['ObjectMeta'] + /** + * @description The id of the ``Identity Provider`` + * Optional, if not set, assume the party is managed by the default identity provider or party is not hosted by the participant. + */ + identityProviderId?: string + /** + * @description The synchronizer, on which the party should be allocated. + * For backwards compatibility, this field may be omitted, if the participant is connected to only one synchronizer. + * Otherwise a synchronizer must be specified. + * Optional + */ + synchronizerId?: string + /** + * @description The user who will get the act_as rights to the newly allocated party. + * If set to an empty string (the default), no user will get rights to the party. + * Optional + */ + userId?: string + } + /** AllocatePartyResponse */ + AllocatePartyResponse: { + partyDetails?: components['schemas']['PartyDetails'] + } + /** + * ArchivedEvent + * @description Records that a contract has been archived, and choices may no longer be exercised on it. + */ + ArchivedEvent: { + /** + * Format: int64 + * @description The offset of origin. + * Offsets are managed by the participant nodes. + * Transactions can thus NOT be assumed to have the same offsets on different participant nodes. + * Required, it is a valid absolute offset (positive integer) + */ + offset: number + /** + * Format: int32 + * @description The position of this event in the originating transaction or reassignment. + * Node IDs are not necessarily equal across participants, + * as these may see different projections/parts of transactions. + * Required, must be valid node ID (non-negative integer) + */ + nodeId: number + /** + * @description The ID of the archived contract. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description Identifies the template that defines the choice that archived the contract. + * This template's package-id may differ from the target contract's package-id + * if the target contract has been upgraded or downgraded. + * + * The identifier uses the package-id reference format. + * + * Required + */ + templateId: string + /** + * @description The parties that are notified of this event. For an ``ArchivedEvent``, + * these are the intersection of the stakeholders of the contract in + * question and the parties specified in the ``TransactionFilter``. The + * stakeholders are the union of the signatories and the observers of + * the contract. + * Each one of its elements must be a valid PartyIdString (as described + * in ``value.proto``). + * Required + */ + witnessParties: string[] + /** + * @description The package name of the contract. + * Required + */ + packageName: string + /** + * @description The interfaces implemented by the target template that have been + * matched from the interface filter query. + * Populated only in case interface filters with include_interface_view set. + * + * If defined, the identifier uses the package-id reference format. + * + * Optional + */ + implementedInterfaces?: string[] + } + /** + * AssignCommand + * @description Assign a contract + */ + AssignCommand: { + value?: components['schemas']['AssignCommand1'] + } + /** + * AssignCommand + * @description Assign a contract + */ + AssignCommand1: { + /** + * @description The ID from the unassigned event to be completed by this assignment. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + reassignmentId: string + /** + * @description The ID of the source synchronizer + * Must be a valid synchronizer id + * Required + */ + source: string + /** + * @description The ID of the target synchronizer + * Must be a valid synchronizer id + * Required + */ + target: string + } + /** CanActAs */ + CanActAs: { + value: components['schemas']['CanActAs1'] + } + /** CanActAs */ + CanActAs1: { + party: string + } + /** CanExecuteAs */ + CanExecuteAs: { + value: components['schemas']['CanExecuteAs1'] + } + /** CanExecuteAs */ + CanExecuteAs1: { + party: string + } + /** CanExecuteAsAnyParty */ + CanExecuteAsAnyParty: { + value: components['schemas']['CanExecuteAsAnyParty1'] + } + /** CanExecuteAsAnyParty */ + CanExecuteAsAnyParty1: Record + /** CanReadAs */ + CanReadAs: { + value: components['schemas']['CanReadAs1'] + } + /** CanReadAs */ + CanReadAs1: { + party: string + } + /** CanReadAsAnyParty */ + CanReadAsAnyParty: { + value: components['schemas']['CanReadAsAnyParty1'] + } + /** CanReadAsAnyParty */ + CanReadAsAnyParty1: Record + /** + * Command + * @description A command can either create a new contract or exercise a choice on an existing contract. + */ + Command: + | { + CreateAndExerciseCommand: components['schemas']['CreateAndExerciseCommand'] + } + | { + CreateCommand: components['schemas']['CreateCommand'] + } + | { + ExerciseByKeyCommand: components['schemas']['ExerciseByKeyCommand'] + } + | { + ExerciseCommand: components['schemas']['ExerciseCommand'] + } + /** + * Command + * @description A command can either create a new contract or exercise a choice on an existing contract. + */ + Command1: + | { + AssignCommand: components['schemas']['AssignCommand'] + } + | { + Empty: components['schemas']['Empty2'] + } + | { + UnassignCommand: components['schemas']['UnassignCommand'] + } + /** + * Completion + * @description A completion represents the status of a submitted command on the ledger: it can be successful or failed. + */ + Completion: { + value?: components['schemas']['Completion1'] + } + /** + * Completion + * @description A completion represents the status of a submitted command on the ledger: it can be successful or failed. + */ + Completion1: { + /** + * @description The ID of the succeeded or failed command. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + commandId: string + /** + * @description Identifies the exact type of the error. + * It uses the same format of conveying error details as it is used for the RPC responses of the APIs. + * Optional + */ + status?: components['schemas']['JsStatus'] + /** + * @description The update_id of the transaction or reassignment that resulted from the command with command_id. + * Only set for successfully executed commands. + * Must be a valid LedgerString (as described in ``value.proto``). + */ + updateId?: string + /** + * @description The user-id that was used for the submission, as described in ``commands.proto``. + * Must be a valid UserIdString (as described in ``value.proto``). + * Optional for historic completions where this data is not available. + */ + userId?: string + /** + * @description The set of parties on whose behalf the commands were executed. + * Contains the ``act_as`` parties from ``commands.proto`` + * filtered to the requesting parties in CompletionStreamRequest. + * The order of the parties need not be the same as in the submission. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Optional for historic completions where this data is not available. + */ + actAs?: string[] + /** + * @description The submission ID this completion refers to, as described in ``commands.proto``. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + submissionId?: string + deduplicationPeriod?: components['schemas']['DeduplicationPeriod1'] + /** + * @description Optional; ledger API trace context + * + * The trace context transported in this message corresponds to the trace context supplied + * by the client application in a HTTP2 header of the original command submission. + * We typically use a header to transfer this type of information. Here we use message + * body, because it is used in gRPC streams which do not support per message headers. + * This field will be populated with the trace context contained in the original submission. + * If that was not provided, a unique ledger-api-server generated trace context will be used + * instead. + */ + traceContext?: components['schemas']['TraceContext'] + /** + * Format: int64 + * @description May be used in a subsequent CompletionStreamRequest to resume the consumption of this stream at a later time. + * Required, must be a valid absolute offset (positive integer). + */ + offset: number + /** + * @description The synchronizer along with its record time. + * The synchronizer id provided, in case of + * + * - successful/failed transactions: identifies the synchronizer of the transaction + * - for successful/failed unassign commands: identifies the source synchronizer + * - for successful/failed assign commands: identifies the target synchronizer + * + * Required + */ + synchronizerTime: components['schemas']['SynchronizerTime'] + } + /** CompletionResponse */ + CompletionResponse: + | { + Completion: components['schemas']['Completion'] + } + | { + Empty: components['schemas']['Empty4'] + } + | { + OffsetCheckpoint: components['schemas']['OffsetCheckpoint'] + } + /** CompletionStreamRequest */ + CompletionStreamRequest: { + /** + * @description Only completions of commands submitted with the same user_id will be visible in the stream. + * Must be a valid UserIdString (as described in ``value.proto``). + * Required unless authentication is used with a user token. + * In that case, the token's user-id will be used for the request's user_id. + */ + userId?: string + /** + * @description Non-empty list of parties whose data should be included. + * The stream shows only completions of commands for which at least one of the ``act_as`` parties is in the given set of parties. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Required + */ + parties: string[] + /** + * Format: int64 + * @description This optional field indicates the minimum offset for completions. This can be used to resume an earlier completion stream. + * If not set the ledger uses the ledger begin offset instead. + * If specified, it must be a valid absolute offset (positive integer) or zero (ledger begin offset). + * If the ledger has been pruned, this parameter must be specified and greater than the pruning offset. + * Optional + */ + beginExclusive?: number + } + /** CompletionStreamResponse */ + CompletionStreamResponse: { + completionResponse?: components['schemas']['CompletionResponse'] + } + /** ConnectedSynchronizer */ + ConnectedSynchronizer: { + synchronizerAlias: string + synchronizerId: string + /** @enum {string} */ + permission: + | 'PARTICIPANT_PERMISSION_UNSPECIFIED' + | 'PARTICIPANT_PERMISSION_SUBMISSION' + | 'PARTICIPANT_PERMISSION_CONFIRMATION' + | 'PARTICIPANT_PERMISSION_OBSERVATION' + } + /** + * CostEstimation + * @description Estimation of the cost of submitting the prepared transaction + * The estimation is done against the synchronizer chosen during preparation of the transaction + * (or the one explicitly requested). + * The cost of re-assigning contracts to another synchronizer when necessary is not included in the estimation. + */ + CostEstimation: { + /** @description Timestamp at which the estimation was made */ + estimationTimestamp?: string + /** + * Format: int64 + * @description Estimated traffic cost of the confirmation request associated with the transaction + */ + confirmationRequestTrafficCostEstimation?: number + /** + * Format: int64 + * @description Estimated traffic cost of the confirmation response associated with the transaction + * This field can also be used as an indication of the cost that other potential confirming nodes + * of the party will incur to approve or reject the transaction + */ + confirmationResponseTrafficCostEstimation?: number + /** + * Format: int64 + * @description Sum of the fields above + */ + totalTrafficCostEstimation?: number + } + /** + * CostEstimationHints + * @description Hints to improve cost estimation precision of a prepared transaction + */ + CostEstimationHints: { + /** + * @description Disable cost estimation + * Default (not set) is false + */ + disabled?: boolean + /** + * @description Details on the keys that will be used to sign the transaction (how many and of which type). + * Signature size impacts the cost of the transaction. + * If empty, the signature sizes will be approximated with threshold-many signatures (where threshold is defined + * in the PartyToKeyMapping of the external party), using keys in the order they are registered. + * Optional (empty list is equivalent to not providing this field) + */ + expectedSignatures?: ( + | 'SIGNING_ALGORITHM_SPEC_UNSPECIFIED' + | 'SIGNING_ALGORITHM_SPEC_ED25519' + | 'SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_256' + | 'SIGNING_ALGORITHM_SPEC_EC_DSA_SHA_384' + )[] + } + /** + * CreateAndExerciseCommand + * @description Create a contract and exercise a choice on it in the same transaction. + */ + CreateAndExerciseCommand: { + /** + * @description The template of the contract the client wants to create. + * Both package-name and package-id reference identifier formats for the template-id are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + templateId: string + /** + * @description The arguments required for creating a contract from this template. + * Required + */ + createArguments: unknown + /** + * @description The name of the choice the client wants to exercise. + * Must be a valid NameString (as described in ``value.proto``). + * Required + */ + choice: string + /** + * @description The argument for this choice. + * Required + */ + choiceArgument: unknown + } + /** + * CreateCommand + * @description Create a new contract instance based on a template. + */ + CreateCommand: { + /** + * @description The template of contract the client wants to create. + * Both package-name and package-id reference identifier formats for the template-id are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + templateId: string + /** + * @description The arguments required for creating a contract from this template. + * Required + */ + createArguments: unknown + } + /** CreateIdentityProviderConfigRequest */ + CreateIdentityProviderConfigRequest: { + /** @description Required */ + identityProviderConfig: components['schemas']['IdentityProviderConfig'] + } + /** CreateIdentityProviderConfigResponse */ + CreateIdentityProviderConfigResponse: { + identityProviderConfig?: components['schemas']['IdentityProviderConfig'] + } + /** + * CreateUserRequest + * @description RPC requests and responses + * /////////////////////////// + * Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(user.identity_provider_id)`` + */ + CreateUserRequest: { + /** + * @description The user to create. + * Required + */ + user: components['schemas']['User'] + /** + * @description The rights to be assigned to the user upon creation, + * which SHOULD include appropriate rights for the ``user.primary_party``. + * Optional + */ + rights?: components['schemas']['Right'][] + } + /** CreateUserResponse */ + CreateUserResponse: { + /** @description Created user. */ + user?: components['schemas']['User'] + } + /** + * CreatedEvent + * @description Records that a contract has been created, and choices may now be exercised on it. + */ + CreatedEvent: { + /** + * Format: int64 + * @description The offset of origin, which has contextual meaning, please see description at messages that include a CreatedEvent. + * Offsets are managed by the participant nodes. + * Transactions can thus NOT be assumed to have the same offsets on different participant nodes. + * Required, it is a valid absolute offset (positive integer) + */ + offset: number + /** + * Format: int32 + * @description The position of this event in the originating transaction or reassignment. + * The origin has contextual meaning, please see description at messages that include a CreatedEvent. + * Node IDs are not necessarily equal across participants, + * as these may see different projections/parts of transactions. + * Required, must be valid node ID (non-negative integer) + */ + nodeId: number + /** + * @description The ID of the created contract. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description The template of the created contract. + * The identifier uses the package-id reference format. + * + * Required + */ + templateId: string + /** + * @description The key of the created contract. + * This will be set if and only if ``template_id`` defines a contract key. + * Optional + */ + contractKey?: unknown + /** + * @description The arguments that have been used to create the contract. + * + * Required + */ + createArgument: unknown + /** + * @description Opaque representation of contract create event payload intended for forwarding + * to an API server as a contract disclosed as part of a command + * submission. + * Optional + */ + createdEventBlob?: string + /** + * @description Interface views specified in the transaction filter. + * Includes an ``InterfaceView`` for each interface for which there is a ``InterfaceFilter`` with + * + * - its party in the ``witness_parties`` of this event, + * - and which is implemented by the template of this event, + * - and which has ``include_interface_view`` set. + * + * Optional + */ + interfaceViews?: components['schemas']['JsInterfaceView'][] + /** + * @description The parties that are notified of this event. When a ``CreatedEvent`` + * is returned as part of a transaction tree or ledger-effects transaction, this will include all + * the parties specified in the ``TransactionFilter`` that are witnesses of the event + * (the stakeholders of the contract and all informees of all the ancestors + * of this create action that this participant knows about). + * If served as part of a ACS delta transaction those will + * be limited to all parties specified in the ``TransactionFilter`` that + * are stakeholders of the contract (i.e. either signatories or observers). + * If the ``CreatedEvent`` is returned as part of an AssignedEvent, + * ActiveContract or IncompleteUnassigned (so the event is related to + * an assignment or unassignment): this will include all parties of the + * ``TransactionFilter`` that are stakeholders of the contract. + * + * The behavior of reading create events visible to parties not hosted + * on the participant node serving the Ledger API is undefined. Concretely, + * there is neither a guarantee that the participant node will serve all their + * create events on the ACS stream, nor is there a guarantee that matching archive + * events are delivered for such create events. + * + * For most clients this is not a problem, as they only read events for parties + * that are hosted on the participant node. If you need to read events + * for parties that may not be hosted at all times on the participant node, + * subscribe to the ``TopologyEvent``s for that party by setting a corresponding + * ``UpdateFormat``. Using these events, query the ACS as-of an offset where the + * party is hosted on the participant node, and ignore create events at offsets + * where the party is not hosted on the participant node. + * Required + */ + witnessParties: string[] + /** + * @description The signatories for this contract as specified by the template. + * Required + */ + signatories: string[] + /** + * @description The observers for this contract as specified explicitly by the template or implicitly as choice controllers. + * This field never contains parties that are signatories. + * Required + */ + observers: string[] + /** + * @description Ledger effective time of the transaction that created the contract. + * Required + */ + createdAt: string + /** + * @description The package name of the created contract. + * Required + */ + packageName: string + /** + * @description A package-id present in the participant package store that typechecks the contract's argument. + * This may differ from the package-id of the template used to create the contract. + * For contracts created before Canton 3.4, this field matches the contract's creation package-id. + * + * NOTE: Experimental, server internal concept, not for client consumption. Subject to change without notice. + * + * Required + */ + representativePackageId: string + /** + * @description Whether this event would be part of respective ACS_DELTA shaped stream, + * and should therefore considered when tracking contract activeness on the client-side. + * Required + */ + acsDelta: boolean + } + /** CreatedTreeEvent */ + CreatedTreeEvent: { + value: components['schemas']['CreatedEvent'] + } + /** + * CumulativeFilter + * @description A filter that matches all contracts that are either an instance of one of + * the ``template_filters`` or that match one of the ``interface_filters``. + */ + CumulativeFilter: { + identifierFilter?: components['schemas']['IdentifierFilter'] + } + /** DeduplicationDuration */ + DeduplicationDuration: { + value: components['schemas']['Duration'] + } + /** DeduplicationDuration */ + DeduplicationDuration1: { + value: components['schemas']['Duration'] + } + /** DeduplicationDuration */ + DeduplicationDuration2: { + value: components['schemas']['Duration'] + } + /** DeduplicationOffset */ + DeduplicationOffset: { + /** Format: int64 */ + value: number + } + /** DeduplicationOffset */ + DeduplicationOffset1: { + /** Format: int64 */ + value: number + } + /** DeduplicationOffset */ + DeduplicationOffset2: { + /** Format: int64 */ + value: number + } + /** + * DeduplicationPeriod + * @description Specifies the deduplication period for the change ID. + * If omitted, the participant will assume the configured maximum deduplication time. + */ + DeduplicationPeriod: + | { + DeduplicationDuration: components['schemas']['DeduplicationDuration'] + } + | { + DeduplicationOffset: components['schemas']['DeduplicationOffset'] + } + | { + Empty: components['schemas']['Empty'] + } + /** + * DeduplicationPeriod + * @description The actual deduplication window used for the submission, which is derived from + * ``Commands.deduplication_period``. The ledger may convert the deduplication period into other + * descriptions and extend the period in implementation-specified ways. + * + * Used to audit the deduplication guarantee described in ``commands.proto``. + * + * Optional; the deduplication guarantee applies even if the completion omits this field. + */ + DeduplicationPeriod1: + | { + DeduplicationDuration: components['schemas']['DeduplicationDuration1'] + } + | { + DeduplicationOffset: components['schemas']['DeduplicationOffset1'] + } + | { + Empty: components['schemas']['Empty3'] + } + /** DeduplicationPeriod */ + DeduplicationPeriod2: + | { + DeduplicationDuration: components['schemas']['DeduplicationDuration2'] + } + | { + DeduplicationOffset: components['schemas']['DeduplicationOffset2'] + } + | { + Empty: components['schemas']['Empty10'] + } + /** + * DeleteIdentityProviderConfigResponse + * @description Does not (yet) contain any data. + */ + DeleteIdentityProviderConfigResponse: Record + /** + * DisclosedContract + * @description An additional contract that is used to resolve + * contract & contract key lookups. + */ + DisclosedContract: { + /** + * @description The template id of the contract. + * The identifier uses the package-id reference format. + * + * If provided, used to validate the template id of the contract serialized in the created_event_blob. + * Optional + */ + templateId?: string + /** + * @description The contract id + * + * If provided, used to validate the contract id of the contract serialized in the created_event_blob. + * Optional + */ + contractId?: string + /** + * @description Opaque byte string containing the complete payload required by the Daml engine + * to reconstruct a contract not known to the receiving participant. + * Required + */ + createdEventBlob: string + /** + * @description The ID of the synchronizer where the contract is currently assigned + * Optional + */ + synchronizerId?: string + } + /** Duration */ + Duration: { + /** Format: int64 */ + seconds: number + /** Format: int32 */ + nanos: number + /** @description This field is automatically added as part of protobuf to json mapping */ + unknownFields?: components['schemas']['UnknownFieldSet'] + } + /** Empty */ + Empty: Record + /** Empty */ + Empty1: Record + /** Empty */ + Empty10: Record + /** Empty */ + Empty2: Record + /** Empty */ + Empty3: Record + /** Empty */ + Empty4: Record + /** Empty */ + Empty5: Record + /** Empty */ + Empty6: Record + /** Empty */ + Empty7: Record + /** Empty */ + Empty8: Record + /** Empty */ + Empty9: Record + /** + * Event + * @description Events in transactions can have two primary shapes: + * + * - ACS delta: events can be CreatedEvent or ArchivedEvent + * - ledger effects: events can be CreatedEvent or ExercisedEvent + * + * In the update service the events are restricted to the events + * visible for the parties specified in the transaction filter. Each + * event message type below contains a ``witness_parties`` field which + * indicates the subset of the requested parties that can see the event + * in question. + */ + Event: + | { + ArchivedEvent: components['schemas']['ArchivedEvent'] + } + | { + CreatedEvent: components['schemas']['CreatedEvent'] + } + | { + ExercisedEvent: components['schemas']['ExercisedEvent'] + } + /** + * EventFormat + * @description A format for events which defines both which events should be included + * and what data should be computed and included for them. + * + * Note that some of the filtering behavior depends on the `TransactionShape`, + * which is expected to be specified alongside usages of `EventFormat`. + */ + EventFormat: { + /** + * @description Each key must be a valid PartyIdString (as described in ``value.proto``). + * The interpretation of the filter depends on the transaction-shape being filtered: + * + * 1. For **ledger-effects** create and exercise events are returned, for which the witnesses include at least one of + * the listed parties and match the per-party filter. + * 2. For **transaction and active-contract-set streams** create and archive events are returned for all contracts whose + * stakeholders include at least one of the listed parties and match the per-party filter. + * + * Optional + */ + filtersByParty?: components['schemas']['Map_Filters'] + /** + * @description Wildcard filters that apply to all the parties existing on the participant. The interpretation of the filters is the same + * with the per-party filter as described above. + * Optional + */ + filtersForAnyParty?: components['schemas']['Filters'] + /** + * @description If enabled, values served over the API will contain more information than strictly necessary to interpret the data. + * In particular, setting the verbose flag to true triggers the ledger to include labels for record fields. + * Optional + */ + verbose?: boolean + } + /** ExecuteSubmissionAndWaitResponse */ + ExecuteSubmissionAndWaitResponse: { + /** + * @description The id of the transaction that resulted from the submitted command. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * Format: int64 + * @description The details of the offset field are described in ``community/ledger-api/README.md``. + * Required + */ + completionOffset: number + } + /** ExecuteSubmissionResponse */ + ExecuteSubmissionResponse: Record + /** + * ExerciseByKeyCommand + * @description Exercise a choice on an existing contract specified by its key. + */ + ExerciseByKeyCommand: { + /** + * @description The template of contract the client wants to exercise. + * Both package-name and package-id reference identifier formats for the template-id are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + templateId: string + /** + * @description The key of the contract the client wants to exercise upon. + * Required + */ + contractKey: unknown + /** + * @description The name of the choice the client wants to exercise. + * Must be a valid NameString (as described in ``value.proto``) + * Required + */ + choice: string + /** + * @description The argument for this choice. + * Required + */ + choiceArgument: unknown + } + /** + * ExerciseCommand + * @description Exercise a choice on an existing contract. + */ + ExerciseCommand: { + /** + * @description The template or interface of the contract the client wants to exercise. + * Both package-name and package-id reference identifier formats for the template-id are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * To exercise a choice on an interface, specify the interface identifier in the template_id field. + * + * Required + */ + templateId: string + /** + * @description The ID of the contract the client wants to exercise upon. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description The name of the choice the client wants to exercise. + * Must be a valid NameString (as described in ``value.proto``) + * Required + */ + choice: string + /** + * @description The argument for this choice. + * Required + */ + choiceArgument: unknown + } + /** + * ExercisedEvent + * @description Records that a choice has been exercised on a target contract. + */ + ExercisedEvent: { + /** + * Format: int64 + * @description The offset of origin. + * Offsets are managed by the participant nodes. + * Transactions can thus NOT be assumed to have the same offsets on different participant nodes. + * Required, it is a valid absolute offset (positive integer) + */ + offset: number + /** + * Format: int32 + * @description The position of this event in the originating transaction or reassignment. + * Node IDs are not necessarily equal across participants, + * as these may see different projections/parts of transactions. + * Required, must be valid node ID (non-negative integer) + */ + nodeId: number + /** + * @description The ID of the target contract. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description Identifies the template that defines the executed choice. + * This template's package-id may differ from the target contract's package-id + * if the target contract has been upgraded or downgraded. + * + * The identifier uses the package-id reference format. + * + * Required + */ + templateId: string + /** + * @description The interface where the choice is defined, if inherited. + * If defined, the identifier uses the package-id reference format. + * + * Optional + */ + interfaceId?: string + /** + * @description The choice that was exercised on the target contract. + * Must be a valid NameString (as described in ``value.proto``). + * Required + */ + choice: string + /** + * @description The argument of the exercised choice. + * Required + */ + choiceArgument: unknown + /** + * @description The parties that exercised the choice. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Required + */ + actingParties: string[] + /** + * @description If true, the target contract may no longer be exercised. + * Required + */ + consuming: boolean + /** + * @description The parties that are notified of this event. The witnesses of an exercise + * node will depend on whether the exercise was consuming or not. + * If consuming, the witnesses are the union of the stakeholders, + * the actors and all informees of all the ancestors of this event this + * participant knows about. + * If not consuming, the witnesses are the union of the signatories, + * the actors and all informees of all the ancestors of this event this + * participant knows about. + * In both cases the witnesses are limited to the querying parties, or not + * limited in case anyParty filters are used. + * Note that the actors might not necessarily be observers + * and thus stakeholders. This is the case when the controllers of a + * choice are specified using "flexible controllers", using the + * ``choice ... controller`` syntax, and said controllers are not + * explicitly marked as observers. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Required + */ + witnessParties: string[] + /** + * Format: int32 + * @description Specifies the upper boundary of the node ids of the events in the same transaction that appeared as a result of + * this ``ExercisedEvent``. This allows unambiguous identification of all the members of the subtree rooted at this + * node. A full subtree can be constructed when all descendant nodes are present in the stream. If nodes are heavily + * filtered, it is only possible to determine if a node is in a consequent subtree or not. + * Required + */ + lastDescendantNodeId: number + /** + * @description The result of exercising the choice. + * Required + */ + exerciseResult: unknown + /** + * @description The package name of the contract. + * Required + */ + packageName: string + /** + * @description If the event is consuming, the interfaces implemented by the target template that have been + * matched from the interface filter query. + * Populated only in case interface filters with include_interface_view set. + * + * The identifier uses the package-id reference format. + * + * Optional + */ + implementedInterfaces?: string[] + /** + * @description Whether this event would be part of respective ACS_DELTA shaped stream, + * and should therefore considered when tracking contract activeness on the client-side. + * Required + */ + acsDelta: boolean + } + /** ExercisedTreeEvent */ + ExercisedTreeEvent: { + value: components['schemas']['ExercisedEvent'] + } + /** + * ExperimentalCommandInspectionService + * @description Whether the Ledger API supports command inspection service + */ + ExperimentalCommandInspectionService: { + supported?: boolean + } + /** + * ExperimentalFeatures + * @description See the feature message definitions for descriptions. + */ + ExperimentalFeatures: { + staticTime?: components['schemas']['ExperimentalStaticTime'] + commandInspectionService?: components['schemas']['ExperimentalCommandInspectionService'] + } + /** + * ExperimentalStaticTime + * @description Ledger is in the static time mode and exposes a time service. + */ + ExperimentalStaticTime: { + supported?: boolean + } + /** FeaturesDescriptor */ + FeaturesDescriptor: { + /** + * @description Features under development or features that are used + * for ledger implementation testing purposes only. + * + * Daml applications SHOULD not depend on these in production. + */ + experimental?: components['schemas']['ExperimentalFeatures'] + /** + * @description If set, then the Ledger API server supports user management. + * It is recommended that clients query this field to gracefully adjust their behavior for + * ledgers that do not support user management. + */ + userManagement?: components['schemas']['UserManagementFeature'] + /** + * @description If set, then the Ledger API server supports party management configurability. + * It is recommended that clients query this field to gracefully adjust their behavior to + * maximum party page size. + */ + partyManagement?: components['schemas']['PartyManagementFeature'] + /** @description It contains the timeouts related to the periodic offset checkpoint emission */ + offsetCheckpoint?: components['schemas']['OffsetCheckpointFeature'] + /** + * @description If set, then the Ledger API server supports package listing + * configurability. It is recommended that clients query this field to + * gracefully adjust their behavior to maximum package listing page size. + */ + packageFeature?: components['schemas']['PackageFeature'] + } + /** Field */ + Field: { + varint?: number[] + fixed64?: number[] + fixed32?: number[] + lengthDelimited?: string[] + } + /** FieldMask */ + FieldMask: { + paths?: string[] + unknownFields: components['schemas']['UnknownFieldSet'] + } + /** + * Filters + * @description The union of a set of template filters, interface filters, or a wildcard. + */ + Filters: { + /** + * @description Every filter in the cumulative list expands the scope of the resulting stream. Each interface, + * template or wildcard filter means additional events that will match the query. + * The impact of include_interface_view and include_created_event_blob fields in the filters will + * also be accumulated. + * A template or an interface SHOULD NOT appear twice in the accumulative field. + * A wildcard filter SHOULD NOT be defined more than once in the accumulative field. + * Optional, if no ``CumulativeFilter`` defined, the default of a single ``WildcardFilter`` with + * include_created_event_blob unset is used. + */ + cumulative?: components['schemas']['CumulativeFilter'][] + } + /** GenerateExternalPartyTopologyRequest */ + GenerateExternalPartyTopologyRequest: { + /** + * @description TODO(#27670) support synchronizer aliases + * Required: synchronizer-id for which we are building this request. + */ + synchronizer: string + /** @description Required: the actual party id will be constructed from this hint and a fingerprint of the public key */ + partyHint: string + /** @description Required: public key */ + publicKey: components['schemas']['SigningPublicKey'] + /** @description Optional: if true, then the local participant will only be observing, not confirming. Default false. */ + localParticipantObservationOnly?: boolean + /** @description Optional: other participant ids which should be confirming for this party */ + otherConfirmingParticipantUids?: string[] + /** + * Format: int32 + * @description Optional: Confirmation threshold >= 1 for the party. Defaults to all available confirmers (or if set to 0). + */ + confirmationThreshold?: number + /** @description Optional: other observing participant ids for this party */ + observingParticipantUids?: string[] + } + /** + * GenerateExternalPartyTopologyResponse + * @description Response message with topology transactions and the multi-hash to be signed. + */ + GenerateExternalPartyTopologyResponse: { + /** @description the generated party id */ + partyId?: string + /** @description the fingerprint of the supplied public key */ + publicKeyFingerprint?: string + /** + * @description The serialized topology transactions which need to be signed and submitted as part of the allocate party process + * Note that the serialization includes the versioning information. Therefore, the transaction here is serialized + * as an `UntypedVersionedMessage` which in turn contains the serialized `TopologyTransaction` in the version + * supported by the synchronizer. + */ + topologyTransactions?: string[] + /** @description the multi-hash which may be signed instead of each individual transaction */ + multiHash?: string + } + /** + * GetActiveContractsRequest + * @description If the given offset is different than the ledger end, and there are (un)assignments in-flight at the given offset, + * the snapshot may fail with "FAILED_PRECONDITION/PARTICIPANT_PRUNED_DATA_ACCESSED". + * Note that it is ok to request acs snapshots for party migration with offsets other than ledger end, because party + * migration is not concerned with incomplete (un)assignments. + */ + GetActiveContractsRequest: { + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Templates to include in the served snapshot, per party. + * Optional, if specified event_format must be unset, if not specified event_format must be set. + */ + filter?: components['schemas']['TransactionFilter'] + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * If enabled, values served over the API will contain more information than strictly necessary to interpret the data. + * In particular, setting the verbose flag to true triggers the ledger to include labels for record fields. + * Optional, if specified event_format must be unset. + */ + verbose?: boolean + /** + * Format: int64 + * @description The offset at which the snapshot of the active contracts will be computed. + * Must be no greater than the current ledger end offset. + * Must be greater than or equal to the last pruning offset. + * Required, must be a valid absolute offset (positive integer) or ledger begin offset (zero). + * If zero, the empty set will be returned. + */ + activeAtOffset: number + /** + * @description Format of the contract_entries in the result. In case of CreatedEvent the presentation will be of + * TRANSACTION_SHAPE_ACS_DELTA. + * Optional for backwards compatibility, defaults to an EventFormat where: + * + * - filters_by_party is the filter.filters_by_party from this request + * - filters_for_any_party is the filter.filters_for_any_party from this request + * - verbose is the verbose field from this request + */ + eventFormat?: components['schemas']['EventFormat'] + } + /** GetConnectedSynchronizersResponse */ + GetConnectedSynchronizersResponse: { + connectedSynchronizers?: components['schemas']['ConnectedSynchronizer'][] + } + /** GetContractRequest */ + GetContractRequest: { + /** + * @description The ID of the contract. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description The list of querying parties + * The stakeholders of the referenced contract must have an intersection with any of these parties + * to return the result. + * Optional, if no querying_parties specified, all possible contracts could be returned. + */ + queryingParties?: string[] + } + /** GetContractResponse */ + GetContractResponse: { + /** + * @description The representative_package_id will be always set to the contract package ID, therefore this endpoint should + * not be used to lookup contract which entered the participant via party replication or repair service. + * The witnesses field will contain only the querying_parties which are also stakeholders of the contract as well. + * The following fields of the created event cannot be populated, so those should not be used / parsed: + * + * - offset + * - node_id + * - created_event_blob + * - interface_views + * - acs_delta + * + * Required + */ + createdEvent: components['schemas']['CreatedEvent'] + } + /** GetEventsByContractIdRequest */ + GetEventsByContractIdRequest: { + /** + * @description The contract id being queried. + * Required + */ + contractId: string + /** + * @description Format of the events in the result, the presentation will be of TRANSACTION_SHAPE_ACS_DELTA. + * Required + */ + eventFormat: components['schemas']['EventFormat'] + } + /** GetIdentityProviderConfigResponse */ + GetIdentityProviderConfigResponse: { + identityProviderConfig?: components['schemas']['IdentityProviderConfig'] + } + /** GetLatestPrunedOffsetsResponse */ + GetLatestPrunedOffsetsResponse: { + /** + * Format: int64 + * @description It will always be a non-negative integer. + * If positive, the absolute offset up to which the ledger has been pruned, + * disregarding the state of all divulged contracts pruning. + * If zero, the ledger has not been pruned yet. + */ + participantPrunedUpToInclusive?: number + /** + * Format: int64 + * @description It will always be a non-negative integer. + * If positive, the absolute offset up to which all divulged events have been pruned on the ledger. + * It can be at or before the ``participant_pruned_up_to_inclusive`` offset. + * For more details about all divulged events pruning, + * see ``PruneRequest.prune_all_divulged_contracts`` in ``participant_pruning_service.proto``. + * If zero, the divulged events have not been pruned yet. + */ + allDivulgedContractsPrunedUpToInclusive?: number + } + /** GetLedgerApiVersionResponse */ + GetLedgerApiVersionResponse: { + /** @description The version of the ledger API. */ + version?: string + /** + * @description The features supported by this Ledger API endpoint. + * + * Daml applications CAN use the feature descriptor on top of + * version constraints on the Ledger API version to determine + * whether a given Ledger API endpoint supports the features + * required to run the application. + * + * See the feature descriptions themselves for the relation between + * Ledger API versions and feature presence. + */ + features?: components['schemas']['FeaturesDescriptor'] + } + /** GetLedgerEndResponse */ + GetLedgerEndResponse: { + /** + * Format: int64 + * @description It will always be a non-negative integer. + * If zero, the participant view of the ledger is empty. + * If positive, the absolute offset of the ledger as viewed by the participant. + */ + offset?: number + } + /** GetPackageStatusResponse */ + GetPackageStatusResponse: { + /** + * @description The status of the package. + * @enum {string} + */ + packageStatus?: 'PACKAGE_STATUS_UNSPECIFIED' | 'PACKAGE_STATUS_REGISTERED' + } + /** GetParticipantIdResponse */ + GetParticipantIdResponse: { + /** + * @description Identifier of the participant, which SHOULD be globally unique. + * Must be a valid LedgerString (as describe in ``value.proto``). + */ + participantId?: string + } + /** GetPartiesResponse */ + GetPartiesResponse: { + /** + * @description The details of the requested Daml parties by the participant, if known. + * The party details may not be in the same order as requested. + * Required + */ + partyDetails: components['schemas']['PartyDetails'][] + } + /** GetPreferredPackageVersionResponse */ + GetPreferredPackageVersionResponse: { + /** + * @description Not populated when no preferred package is found + * Optional + */ + packagePreference?: components['schemas']['PackagePreference'] + } + /** GetPreferredPackagesRequest */ + GetPreferredPackagesRequest: { + /** + * @description The package-name vetting requirements for which the preferred packages should be resolved. + * + * Generally it is enough to provide the requirements for the intended command's root package-names. + * Additional package-name requirements can be provided when additional Daml transaction informees need to use + * package dependencies of the command's root packages. + * + * Required + */ + packageVettingRequirements: components['schemas']['PackageVettingRequirement'][] + /** + * @description The synchronizer whose vetting state should be used for resolving this query. + * If not specified, the vetting states of all synchronizers to which the participant is connected are used. + * Optional + */ + synchronizerId?: string + /** + * @description The timestamp at which the package vetting validity should be computed + * on the latest topology snapshot as seen by the participant. + * If not provided, the participant's current clock time is used. + * Optional + */ + vettingValidAt?: string + } + /** GetPreferredPackagesResponse */ + GetPreferredPackagesResponse: { + /** + * @description The package references of the preferred packages. + * Must contain one package reference for each requested package-name. + * + * If you build command submissions whose content depends on the returned + * preferred packages, then we recommend submitting the preferred package-ids + * in the ``package_id_selection_preference`` of the command submission to + * avoid race conditions with concurrent changes of the on-ledger package vetting state. + * + * Required + */ + packageReferences: components['schemas']['PackageReference'][] + /** + * @description The synchronizer for which the package preferences are computed. + * If the synchronizer_id was specified in the request, then it matches the request synchronizer_id. + * Required + */ + synchronizerId: string + } + /** + * GetTransactionByIdRequest + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + GetTransactionByIdRequest: { + /** + * @description The ID of a particular transaction. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * The parties whose events the client expects to see. + * Events that are not visible for the parties in this collection will not be present in the response. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Optional for backwards compatibility for GetTransactionById request: if defined transaction_format must be + * unset (falling back to defaults). + */ + requestingParties?: string[] + /** + * @description Optional for GetTransactionById request for backwards compatibility: defaults to a transaction_format, where: + * + * - event_format.filters_by_party will have template-wildcard filters for all the requesting_parties + * - event_format.filters_for_any_party is unset + * - event_format.verbose = true + * - transaction_shape = TRANSACTION_SHAPE_ACS_DELTA + */ + transactionFormat?: components['schemas']['TransactionFormat'] + } + /** + * GetTransactionByOffsetRequest + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + GetTransactionByOffsetRequest: { + /** + * Format: int64 + * @description The offset of the transaction being looked up. + * Must be a valid absolute offset (positive integer). + * Required + */ + offset: number + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * The parties whose events the client expects to see. + * Events that are not visible for the parties in this collection will not be present in the response. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Optional for backwards compatibility for GetTransactionByOffset request: if defined transaction_format must be + * unset (falling back to defaults). + */ + requestingParties?: string[] + /** + * @description Optional for GetTransactionByOffset request for backwards compatibility: defaults to a TransactionFormat, where: + * + * - event_format.filters_by_party will have template-wildcard filters for all the requesting_parties + * - event_format.filters_for_any_party is unset + * - event_format.verbose = true + * - transaction_shape = TRANSACTION_SHAPE_ACS_DELTA + */ + transactionFormat?: components['schemas']['TransactionFormat'] + } + /** GetUpdateByIdRequest */ + GetUpdateByIdRequest: { + /** + * @description The ID of a particular update. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * @description The format for the update. + * Required + */ + updateFormat: components['schemas']['UpdateFormat'] + } + /** GetUpdateByOffsetRequest */ + GetUpdateByOffsetRequest: { + /** + * Format: int64 + * @description The offset of the update being looked up. + * Must be a valid absolute offset (positive integer). + * Required + */ + offset: number + /** + * @description The format for the update. + * Required + */ + updateFormat: components['schemas']['UpdateFormat'] + } + /** GetUpdatesRequest */ + GetUpdatesRequest: { + /** + * Format: int64 + * @description Beginning of the requested ledger section (non-negative integer). + * The response will only contain transactions whose offset is strictly greater than this. + * If zero, the stream will start from the beginning of the ledger. + * If positive, the streaming will start after this absolute offset. + * If the ledger has been pruned, this parameter must be specified and be greater than the pruning offset. + */ + beginExclusive?: number + /** + * Format: int64 + * @description End of the requested ledger section. + * The response will only contain transactions whose offset is less than or equal to this. + * Optional, if empty, the stream will not terminate. + * If specified, the stream will terminate after this absolute offset (positive integer) is reached. + */ + endInclusive?: number + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Requesting parties with template filters. + * Template filters must be empty for GetUpdateTrees requests. + * Optional for backwards compatibility, if defined update_format must be unset + */ + filter?: components['schemas']['TransactionFilter'] + /** + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * If enabled, values served over the API will contain more information than strictly necessary to interpret the data. + * In particular, setting the verbose flag to true triggers the ledger to include labels, record and variant type ids + * for record fields. + * Optional for backwards compatibility, if defined update_format must be unset + */ + verbose?: boolean + /** + * @description Must be unset for GetUpdateTrees request. + * Optional for backwards compatibility for GetUpdates request: defaults to an UpdateFormat where: + * + * - include_transactions.event_format.filters_by_party = the filter.filters_by_party on this request + * - include_transactions.event_format.filters_for_any_party = the filter.filters_for_any_party on this request + * - include_transactions.event_format.verbose = the same flag specified on this request + * - include_transactions.transaction_shape = TRANSACTION_SHAPE_ACS_DELTA + * - include_reassignments.filter = the same filter specified on this request + * - include_reassignments.verbose = the same flag specified on this request + * - include_topology_events.include_participant_authorization_events.parties = all the parties specified in filter + */ + updateFormat?: components['schemas']['UpdateFormat'] + } + /** GetUserResponse */ + GetUserResponse: { + /** @description Retrieved user. */ + user?: components['schemas']['User'] + } + /** + * GrantUserRightsRequest + * @description Add the rights to the set of rights granted to the user. + * + * Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(identity_provider_id)`` + */ + GrantUserRightsRequest: { + /** + * @description The user to whom to grant rights. + * Required + */ + userId: string + /** + * @description The rights to grant. + * Optional + */ + rights?: components['schemas']['Right'][] + /** + * @description The id of the ``Identity Provider`` + * Optional, if not set, assume the user is managed by the default identity provider. + */ + identityProviderId?: string + } + /** GrantUserRightsResponse */ + GrantUserRightsResponse: { + /** @description The rights that were newly granted by the request. */ + newlyGrantedRights?: components['schemas']['Right'][] + } + /** IdentifierFilter */ + IdentifierFilter: + | { + Empty: components['schemas']['Empty1'] + } + | { + InterfaceFilter: components['schemas']['InterfaceFilter'] + } + | { + TemplateFilter: components['schemas']['TemplateFilter'] + } + | { + WildcardFilter: components['schemas']['WildcardFilter'] + } + /** IdentityProviderAdmin */ + IdentityProviderAdmin: { + value: components['schemas']['IdentityProviderAdmin1'] + } + /** IdentityProviderAdmin */ + IdentityProviderAdmin1: Record + /** IdentityProviderConfig */ + IdentityProviderConfig: { + /** + * @description The identity provider identifier + * Must be a valid LedgerString (as describe in ``value.proto``). + * Required + */ + identityProviderId: string + /** + * @description When set, the callers using JWT tokens issued by this identity provider are denied all access + * to the Ledger API. + * Optional, + * Modifiable + */ + isDeactivated?: boolean + /** + * @description Specifies the issuer of the JWT token. + * The issuer value is a case sensitive URL using the https scheme that contains scheme, host, + * and optionally, port number and path components and no query or fragment components. + * Required + * Modifiable + */ + issuer: string + /** + * @description The JWKS (JSON Web Key Set) URL. + * The Ledger API uses JWKs (JSON Web Keys) from the provided URL to verify that the JWT has been + * signed with the loaded JWK. Only RS256 (RSA Signature with SHA-256) signing algorithm is supported. + * Required + * Modifiable + */ + jwksUrl: string + /** + * @description Specifies the audience of the JWT token. + * When set, the callers using JWT tokens issued by this identity provider are allowed to get an access + * only if the "aud" claim includes the string specified here + * Optional, + * Modifiable + */ + audience?: string + } + /** + * InterfaceFilter + * @description This filter matches contracts that implement a specific interface. + */ + InterfaceFilter: { + value?: components['schemas']['InterfaceFilter1'] + } + /** + * InterfaceFilter + * @description This filter matches contracts that implement a specific interface. + */ + InterfaceFilter1: { + /** + * @description The interface that a matching contract must implement. + * The ``interface_id`` needs to be valid: corresponding interface should be defined in + * one of the available packages at the time of the query. + * Both package-name and package-id reference formats for the identifier are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + interfaceId: string + /** + * @description Whether to include the interface view on the contract in the returned ``CreatedEvent``. + * Use this to access contract data in a uniform manner in your API client. + * Optional + */ + includeInterfaceView?: boolean + /** + * @description Whether to include a ``created_event_blob`` in the returned ``CreatedEvent``. + * Use this to access the contract create event payload in your API client + * for submitting it as a disclosed contract with future commands. + * Optional + */ + includeCreatedEventBlob?: boolean + } + /** JsActiveContract */ + JsActiveContract: { + /** + * @description Required + * The event as it appeared in the context of its last update (i.e. daml transaction or + * reassignment). In particular, the last offset, node_id pair is preserved. + * The last update is the most recent update created or assigned this contract on synchronizer_id synchronizer. + * The offset of the CreatedEvent might point to an already pruned update, therefore it cannot necessarily be used + * for lookups. + */ + createdEvent: components['schemas']['CreatedEvent'] + /** + * @description A valid synchronizer id + * Required + */ + synchronizerId: string + /** + * Format: int64 + * @description Each corresponding assigned and unassigned event has the same reassignment_counter. This strictly increases + * with each unassign command for the same contract. Creation of the contract corresponds to reassignment_counter + * equals zero. + * This field will be the reassignment_counter of the latest observable activation event on this synchronizer, which is + * before the active_at_offset. + * Required + */ + reassignmentCounter: number + } + /** JsArchived */ + JsArchived: { + /** @description Required */ + archivedEvent: components['schemas']['ArchivedEvent'] + /** + * @description Required + * The synchronizer which sequenced the archival of the contract + */ + synchronizerId: string + } + /** + * JsAssignedEvent + * @description Records that a contract has been assigned, and it can be used on the target synchronizer. + */ + JsAssignedEvent: { + /** + * @description The ID of the source synchronizer. + * Must be a valid synchronizer id. + * Required + */ + source: string + /** + * @description The ID of the target synchronizer. + * Must be a valid synchronizer id. + * Required + */ + target: string + /** + * @description The ID from the unassigned event. + * For correlation capabilities. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + reassignmentId: string + /** + * @description Party on whose behalf the assign command was executed. + * Empty if the assignment happened offline via the repair service. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Optional + */ + submitter?: string + /** + * Format: int64 + * @description Each corresponding assigned and unassigned event has the same reassignment_counter. This strictly increases + * with each unassign command for the same contract. Creation of the contract corresponds to reassignment_counter + * equals zero. + * Required + */ + reassignmentCounter: number + /** + * @description Required + * The offset of this event refers to the offset of the assignment, + * while the node_id is the index of within the batch. + */ + createdEvent: components['schemas']['CreatedEvent'] + } + /** JsAssignmentEvent */ + JsAssignmentEvent: { + source: string + target: string + reassignmentId: string + submitter: string + /** Format: int64 */ + reassignmentCounter: number + createdEvent: components['schemas']['CreatedEvent'] + } + /** JsCantonError */ + JsCantonError: { + code: string + cause: string + correlationId?: string + traceId?: string + context: components['schemas']['Map_String'] + resources?: components['schemas']['Tuple2_String_String'][] + /** Format: int32 */ + errorCategory: number + /** Format: int32 */ + grpcCodeValue?: number + retryInfo?: string + definiteAnswer?: boolean + } + /** + * JsCommands + * @description A composite command that groups multiple commands together. + */ + JsCommands: { + /** + * @description Individual elements of this atomic command. Must be non-empty. + * Required + */ + commands: components['schemas']['Command'][] + /** + * @description Uniquely identifies the command. + * The triple (user_id, act_as, command_id) constitutes the change ID for the intended ledger change, + * where act_as is interpreted as a set of party names. + * The change ID can be used for matching the intended ledger changes with all their completions. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + commandId: string + /** + * @description Set of parties on whose behalf the command should be executed. + * If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request + * to act on behalf of each of the given parties. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Required, must be non-empty. + */ + actAs: string[] + /** + * @description Uniquely identifies the participant user that issued the command. + * Must be a valid UserIdString (as described in ``value.proto``). + * Required unless authentication is used with a user token. + * In that case, the token's user-id will be used for the request's user_id. + */ + userId?: string + /** + * @description Set of parties on whose behalf (in addition to all parties listed in ``act_as``) contracts can be retrieved. + * This affects Daml operations such as ``fetch``, ``fetchByKey``, ``lookupByKey``, ``exercise``, and ``exerciseByKey``. + * Note: A participant node of a Daml network can host multiple parties. Each contract present on the participant + * node is only visible to a subset of these parties. A command can only use contracts that are visible to at least + * one of the parties in ``act_as`` or ``read_as``. This visibility check is independent from the Daml authorization + * rules for fetch operations. + * If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request + * to read contract data on behalf of each of the given parties. + * Optional + */ + readAs?: string[] + /** + * @description Identifier of the on-ledger workflow that this command is a part of. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + deduplicationPeriod?: components['schemas']['DeduplicationPeriod'] + /** + * @description Lower bound for the ledger time assigned to the resulting transaction. + * Note: The ledger time of a transaction is assigned as part of command interpretation. + * Use this property if you expect that command interpretation will take a considerate amount of time, such that by + * the time the resulting transaction is sequenced, its assigned ledger time is not valid anymore. + * Must not be set at the same time as min_ledger_time_rel. + * Optional + */ + minLedgerTimeAbs?: string + /** + * @description Same as min_ledger_time_abs, but specified as a duration, starting from the time the command is received by the server. + * Must not be set at the same time as min_ledger_time_abs. + * Optional + */ + minLedgerTimeRel?: components['schemas']['Duration'] + /** + * @description A unique identifier to distinguish completions for different submissions with the same change ID. + * Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission + * with the same change ID. + * Must be a valid LedgerString (as described in ``value.proto``). + * + * If omitted, the participant or the committer may set a value of their choice. + * Optional + */ + submissionId?: string + /** + * @description Additional contracts used to resolve contract & contract key lookups. + * Optional + */ + disclosedContracts?: components['schemas']['DisclosedContract'][] + /** + * @description Must be a valid synchronizer id + * Optional + */ + synchronizerId?: string + /** + * @description The package-id selection preference of the client for resolving + * package names and interface instances in command submission and interpretation + */ + packageIdSelectionPreference?: string[] + /** + * @description Fetches the contract keys into the caches to speed up the command processing. + * Should only contain contract keys that are expected to be resolved during interpretation of the commands. + * Keys of disclosed contracts do not need prefetching. + * + * Optional + */ + prefetchContractKeys?: components['schemas']['PrefetchContractKey'][] + } + /** + * JsContractEntry + * @description For a contract there could be multiple contract_entry-s in the entire snapshot. These together define + * the state of one contract in the snapshot. + * A contract_entry is included in the result, if and only if there is at least one stakeholder party of the contract + * that is hosted on the synchronizer at the time of the event and the party satisfies the + * ``TransactionFilter`` in the query. + */ + JsContractEntry: + | { + JsActiveContract: components['schemas']['JsActiveContract'] + } + | { + JsEmpty: components['schemas']['JsEmpty'] + } + | { + JsIncompleteAssigned: components['schemas']['JsIncompleteAssigned'] + } + | { + JsIncompleteUnassigned: components['schemas']['JsIncompleteUnassigned'] + } + /** JsCreated */ + JsCreated: { + /** + * @description Required + * The event as it appeared in the context of its original update (i.e. daml transaction or + * reassignment) on this participant node. You can use its offset and node_id to find the + * corresponding update and the node within it. + */ + createdEvent: components['schemas']['CreatedEvent'] + /** + * @description The synchronizer which sequenced the creation of the contract + * Required + */ + synchronizerId: string + } + /** JsEmpty */ + JsEmpty: Record + /** JsExecuteSubmissionAndWaitForTransactionRequest */ + JsExecuteSubmissionAndWaitForTransactionRequest: { + /** + * @description the prepared transaction + * Typically this is the value of the `prepared_transaction` field in `PrepareSubmissionResponse` + * obtained from calling `prepareSubmission`. + * Required + */ + preparedTransaction: string + /** + * @description The party(ies) signatures that authorize the prepared submission to be executed by this node. + * Each party can provide one or more signatures.. + * and one or more parties can sign. + * Note that currently, only single party submissions are supported. + * Required + */ + partySignatures: components['schemas']['PartySignatures'] + deduplicationPeriod?: components['schemas']['DeduplicationPeriod2'] + /** + * @description A unique identifier to distinguish completions for different submissions with the same change ID. + * Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission + * with the same change ID. + * Must be a valid LedgerString (as described in ``value.proto``). + * + * Required + */ + submissionId: string + /** + * @description See [PrepareSubmissionRequest.user_id] + * Optional + */ + userId?: string + /** + * @description The hashing scheme version used when building the hash + * Required + * @enum {string} + */ + hashingSchemeVersion: + | 'HASHING_SCHEME_VERSION_UNSPECIFIED' + | 'HASHING_SCHEME_VERSION_V2' + | 'HASHING_SCHEME_VERSION_V3' + /** + * @description If set will influence the chosen ledger effective time but will not result in a submission delay so any override + * should be scheduled to executed within the window allowed by synchronizer. + * Optional + */ + minLedgerTime?: components['schemas']['MinLedgerTime'] + /** + * @description If no ``transaction_format`` is provided, a default will be used where ``transaction_shape`` is set to + * TRANSACTION_SHAPE_ACS_DELTA, ``event_format`` is defined with ``filters_by_party`` containing wildcard-template + * filter for all original ``act_as`` and ``read_as`` parties and the ``verbose`` flag is set. + * When the ``transaction_shape`` TRANSACTION_SHAPE_ACS_DELTA shape is used (explicitly or is defaulted to as explained above), + * events will only be returned if the submitting party is hosted on this node. + * Optional + */ + transactionFormat?: components['schemas']['TransactionFormat'] + } + /** JsExecuteSubmissionAndWaitForTransactionResponse */ + JsExecuteSubmissionAndWaitForTransactionResponse: { + /** + * @description The transaction that resulted from the submitted command. + * The transaction might contain no events (request conditions result in filtering out all of them). + * Required + */ + transaction: components['schemas']['JsTransaction'] + } + /** JsExecuteSubmissionAndWaitRequest */ + JsExecuteSubmissionAndWaitRequest: { + /** + * @description the prepared transaction + * Typically this is the value of the `prepared_transaction` field in `PrepareSubmissionResponse` + * obtained from calling `prepareSubmission`. + * Required + */ + preparedTransaction: string + /** + * @description The party(ies) signatures that authorize the prepared submission to be executed by this node. + * Each party can provide one or more signatures.. + * and one or more parties can sign. + * Note that currently, only single party submissions are supported. + * Required + */ + partySignatures: components['schemas']['PartySignatures'] + deduplicationPeriod?: components['schemas']['DeduplicationPeriod2'] + /** + * @description A unique identifier to distinguish completions for different submissions with the same change ID. + * Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission + * with the same change ID. + * Must be a valid LedgerString (as described in ``value.proto``). + * + * Required + */ + submissionId: string + /** + * @description See [PrepareSubmissionRequest.user_id] + * Optional + */ + userId?: string + /** + * @description The hashing scheme version used when building the hash + * Required + * @enum {string} + */ + hashingSchemeVersion: + | 'HASHING_SCHEME_VERSION_UNSPECIFIED' + | 'HASHING_SCHEME_VERSION_V2' + | 'HASHING_SCHEME_VERSION_V3' + /** + * @description If set will influence the chosen ledger effective time but will not result in a submission delay so any override + * should be scheduled to executed within the window allowed by synchronizer. + * Optional + */ + minLedgerTime?: components['schemas']['MinLedgerTime'] + } + /** JsExecuteSubmissionRequest */ + JsExecuteSubmissionRequest: { + /** + * @description the prepared transaction + * Typically this is the value of the `prepared_transaction` field in `PrepareSubmissionResponse` + * obtained from calling `prepareSubmission`. + * Required + */ + preparedTransaction: string + /** + * @description The party(ies) signatures that authorize the prepared submission to be executed by this node. + * Each party can provide one or more signatures.. + * and one or more parties can sign. + * Note that currently, only single party submissions are supported. + * Required + */ + partySignatures: components['schemas']['PartySignatures'] + deduplicationPeriod?: components['schemas']['DeduplicationPeriod2'] + /** + * @description A unique identifier to distinguish completions for different submissions with the same change ID. + * Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission + * with the same change ID. + * Must be a valid LedgerString (as described in ``value.proto``). + * + * Required + */ + submissionId: string + /** + * @description See [PrepareSubmissionRequest.user_id] + * Optional + */ + userId?: string + /** + * @description The hashing scheme version used when building the hash + * Required + * @enum {string} + */ + hashingSchemeVersion: + | 'HASHING_SCHEME_VERSION_UNSPECIFIED' + | 'HASHING_SCHEME_VERSION_V2' + | 'HASHING_SCHEME_VERSION_V3' + /** + * @description If set will influence the chosen ledger effective time but will not result in a submission delay so any override + * should be scheduled to executed within the window allowed by synchronizer. + * Optional + */ + minLedgerTime?: components['schemas']['MinLedgerTime'] + } + /** JsGetActiveContractsResponse */ + JsGetActiveContractsResponse: { + /** + * @description The workflow ID used in command submission which corresponds to the contract_entry. Only set if + * the ``workflow_id`` for the command was set. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + contractEntry?: components['schemas']['JsContractEntry'] + } + /** JsGetEventsByContractIdResponse */ + JsGetEventsByContractIdResponse: { + /** + * @description The create event for the contract with the ``contract_id`` given in the request + * provided it exists and has not yet been pruned. + * Optional + */ + created?: components['schemas']['JsCreated'] + /** + * @description The archive event for the contract with the ``contract_id`` given in the request + * provided such an archive event exists and it has not yet been pruned. + * Optional + */ + archived?: components['schemas']['JsArchived'] + } + /** + * JsGetTransactionResponse + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + JsGetTransactionResponse: { + /** @description Required */ + transaction: components['schemas']['JsTransaction'] + } + /** + * JsGetTransactionTreeResponse + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + JsGetTransactionTreeResponse: { + /** @description Required */ + transaction: components['schemas']['JsTransactionTree'] + } + /** JsGetUpdateResponse */ + JsGetUpdateResponse: { + update?: components['schemas']['Update'] + } + /** + * JsGetUpdateTreesResponse + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + JsGetUpdateTreesResponse: { + update?: components['schemas']['Update1'] + } + /** JsGetUpdatesResponse */ + JsGetUpdatesResponse: { + update?: components['schemas']['Update'] + } + /** JsIncompleteAssigned */ + JsIncompleteAssigned: { + /** @description Required */ + assignedEvent: components['schemas']['JsAssignedEvent'] + } + /** JsIncompleteUnassigned */ + JsIncompleteUnassigned: { + /** + * @description Required + * The event as it appeared in the context of its last activation update (i.e. daml transaction or + * reassignment). In particular, the last activation offset, node_id pair is preserved. + * The last activation update is the most recent update created or assigned this contract on synchronizer_id synchronizer before + * the unassigned_event. + * The offset of the CreatedEvent might point to an already pruned update, therefore it cannot necessarily be used + * for lookups. + */ + createdEvent: components['schemas']['CreatedEvent'] + /** @description Required */ + unassignedEvent: components['schemas']['UnassignedEvent'] + } + /** + * JsInterfaceView + * @description View of a create event matched by an interface filter. + */ + JsInterfaceView: { + /** + * @description The interface implemented by the matched event. + * The identifier uses the package-id reference format. + * + * Required + */ + interfaceId: string + /** + * @description Whether the view was successfully computed, and if not, + * the reason for the error. The error is reported using the same rules + * for error codes and messages as the errors returned for API requests. + * Required + */ + viewStatus: components['schemas']['JsStatus'] + /** + * @description The value of the interface's view method on this event. + * Set if it was requested in the ``InterfaceFilter`` and it could be + * successfully computed. + * Optional + */ + viewValue?: unknown + /** + * @description The package defining the interface implementation used to compute the view. + * Can be different from the package that was used to create the contract itself, + * as the contract arguments can be upgraded or downgraded using smart-contract upgrading + * as part of computing the interface view. + * Populated if the view computation is successful, otherwise empty. + * Optional + */ + implementationPackageId?: string + } + /** JsPrepareSubmissionRequest */ + JsPrepareSubmissionRequest: { + /** + * @description Uniquely identifies the participant user that prepares the transaction. + * Must be a valid UserIdString (as described in ``value.proto``). + * Required unless authentication is used with a user token. + * In that case, the token's user-id will be used for the request's user_id. + * Optional + */ + userId?: string + /** + * @description Uniquely identifies the command. + * The triple (user_id, act_as, command_id) constitutes the change ID for the intended ledger change, + * where act_as is interpreted as a set of party names. + * The change ID can be used for matching the intended ledger changes with all their completions. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + commandId: string + /** + * @description Individual elements of this atomic command. Must be non-empty. + * Limitation: Only single command transaction are currently supported by the API. + * The field is marked as repeated in preparation for future support of multiple commands. + * Required + */ + commands: components['schemas']['Command'][] + /** @description Optional */ + minLedgerTime?: components['schemas']['MinLedgerTime'] + /** + * @description Set of parties on whose behalf the command should be executed, if submitted. + * If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request + * to **read** (not act) on behalf of each of the given parties. This is because this RPC merely prepares a transaction + * and does not execute it. Therefore read authorization is sufficient even for actAs parties. + * Note: This may change, and more specific authorization scope may be introduced in the future. + * Each element must be a valid PartyIdString (as described in ``value.proto``). + * Required, must be non-empty. + */ + actAs: string[] + /** + * @description Set of parties on whose behalf (in addition to all parties listed in ``act_as``) contracts can be retrieved. + * This affects Daml operations such as ``fetch``, ``fetchByKey``, ``lookupByKey``, ``exercise``, and ``exerciseByKey``. + * Note: A command can only use contracts that are visible to at least + * one of the parties in ``act_as`` or ``read_as``. This visibility check is independent from the Daml authorization + * rules for fetch operations. + * If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request + * to read contract data on behalf of each of the given parties. + * Optional + */ + readAs?: string[] + /** + * @description Additional contracts used to resolve contract & contract key lookups. + * Optional + */ + disclosedContracts?: components['schemas']['DisclosedContract'][] + /** + * @description Must be a valid synchronizer id + * If not set, a suitable synchronizer that this node is connected to will be chosen + * Optional + */ + synchronizerId?: string + /** + * @description The package-id selection preference of the client for resolving + * package names and interface instances in command submission and interpretation + * Optional + */ + packageIdSelectionPreference?: string[] + /** + * @description When true, the response will contain additional details on how the transaction was encoded and hashed + * This can be useful for troubleshooting of hash mismatches. Should only be used for debugging. + * Optional, default to false + */ + verboseHashing?: boolean + /** + * @description Fetches the contract keys into the caches to speed up the command processing. + * Should only contain contract keys that are expected to be resolved during interpretation of the commands. + * Keys of disclosed contracts do not need prefetching. + * + * Optional + */ + prefetchContractKeys?: components['schemas']['PrefetchContractKey'][] + /** + * @description Maximum timestamp at which the transaction can be recorded onto the ledger via the synchronizer specified in the `PrepareSubmissionResponse`. + * If submitted after it will be rejected even if otherwise valid, in which case it needs to be prepared and signed again + * with a new valid max_record_time. + * Use this to limit the time-to-life of a prepared transaction, + * which is useful to know when it can definitely not be accepted + * anymore and resorting to preparing another transaction for the same + * intent is safe again. + * Optional + */ + maxRecordTime?: string + /** + * @description Hints to improve the accuracy of traffic cost estimation. + * The estimation logic assumes that this node will be used for the execution of the transaction + * If another node is used instead, the estimation may be less precise. + * Request amplification is not accounted for in the estimation: each amplified request will + * result in the cost of the confirmation request to be charged additionally. + * + * Optional - Traffic cost estimation is enabled by default if this field is not set + * To turn off cost estimation, set the CostEstimationHints#disabled field to true + */ + estimateTrafficCost?: components['schemas']['CostEstimationHints'] + /** + * @description Optional - defaults to HASHING_SCHEME_VERSION_V2 + * The hashing scheme version to be used when building the hash + * @enum {string} + */ + hashingSchemeVersion?: + | 'HASHING_SCHEME_VERSION_UNSPECIFIED' + | 'HASHING_SCHEME_VERSION_V2' + | 'HASHING_SCHEME_VERSION_V3' + } + /** + * JsPrepareSubmissionResponse + * @description [docs-entry-end: HashingSchemeVersion] + */ + JsPrepareSubmissionResponse: { + /** + * @description The interpreted transaction, it represents the ledger changes necessary to execute the commands specified in the request. + * Clients MUST display the content of the transaction to the user for them to validate before signing the hash if the preparing participant is not trusted. + */ + preparedTransaction?: string + /** + * @description Hash of the transaction, this is what needs to be signed by the party to authorize the transaction. + * Only provided for convenience, clients MUST recompute the hash from the raw transaction if the preparing participant is not trusted. + * May be removed in future versions + */ + preparedTransactionHash?: string + /** + * @description The hashing scheme version used when building the hash + * @enum {string} + */ + hashingSchemeVersion?: + | 'HASHING_SCHEME_VERSION_UNSPECIFIED' + | 'HASHING_SCHEME_VERSION_V2' + | 'HASHING_SCHEME_VERSION_V3' + /** + * @description Optional additional details on how the transaction was encoded and hashed. Only set if verbose_hashing = true in the request + * Note that there are no guarantees on the stability of the format or content of this field. + * Its content should NOT be parsed and should only be used for troubleshooting purposes. + */ + hashingDetails?: string + /** + * @description Traffic cost estimation of the prepared transaction + * Optional + */ + costEstimation?: components['schemas']['CostEstimation'] + } + /** + * JsReassignment + * @description Complete view of an on-ledger reassignment. + */ + JsReassignment: { + /** + * @description Assigned by the server. Useful for correlating logs. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * @description The ID of the command which resulted in this reassignment. Missing for everyone except the submitting party on the submitting participant. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + commandId?: string + /** + * @description The workflow ID used in reassignment command submission. Only set if the ``workflow_id`` for the command was set. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + /** + * Format: int64 + * @description The participant's offset. The details of this field are described in ``community/ledger-api/README.md``. + * Required, must be a valid absolute offset (positive integer). + */ + offset: number + /** + * @description The collection of reassignment events. + * Required. + */ + events: components['schemas']['JsReassignmentEvent'][] + /** + * @description Optional; ledger API trace context + * + * The trace context transported in this message corresponds to the trace context supplied + * by the client application in a HTTP2 header of the original command submission. + * We typically use a header to transfer this type of information. Here we use message + * body, because it is used in gRPC streams which do not support per message headers. + * This field will be populated with the trace context contained in the original submission. + * If that was not provided, a unique ledger-api-server generated trace context will be used + * instead. + */ + traceContext?: components['schemas']['TraceContext'] + /** + * @description The time at which the reassignment was recorded. The record time refers to the source/target + * synchronizer for an unassign/assign event respectively. + * Required + */ + recordTime: string + /** + * @description A valid synchronizer id. + * Identifies the synchronizer that synchronized this Reassignment. + * Required + */ + synchronizerId: string + } + /** JsReassignmentEvent */ + JsReassignmentEvent: + | { + JsAssignmentEvent: components['schemas']['JsAssignmentEvent'] + } + | { + JsUnassignedEvent: components['schemas']['JsUnassignedEvent'] + } + /** JsStatus */ + JsStatus: { + /** Format: int32 */ + code: number + message: string + details?: components['schemas']['ProtoAny'][] + } + /** JsSubmitAndWaitForReassignmentResponse */ + JsSubmitAndWaitForReassignmentResponse: { + /** + * @description The reassignment that resulted from the submitted reassignment command. + * The reassignment might contain no events (request conditions result in filtering out all of them). + * Required + */ + reassignment: components['schemas']['JsReassignment'] + } + /** + * JsSubmitAndWaitForTransactionRequest + * @description These commands are executed as a single atomic transaction. + */ + JsSubmitAndWaitForTransactionRequest: { + /** + * @description The commands to be submitted. + * Required + */ + commands: components['schemas']['JsCommands'] + /** + * @description If no ``transaction_format`` is provided, a default will be used where ``transaction_shape`` is set to + * TRANSACTION_SHAPE_ACS_DELTA, ``event_format`` is defined with ``filters_by_party`` containing wildcard-template + * filter for all original ``act_as`` and ``read_as`` parties and the ``verbose`` flag is set. + * Optional + */ + transactionFormat?: components['schemas']['TransactionFormat'] + } + /** JsSubmitAndWaitForTransactionResponse */ + JsSubmitAndWaitForTransactionResponse: { + /** + * @description The transaction that resulted from the submitted command. + * The transaction might contain no events (request conditions result in filtering out all of them). + * Required + */ + transaction: components['schemas']['JsTransaction'] + } + /** + * JsSubmitAndWaitForTransactionTreeResponse + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + */ + JsSubmitAndWaitForTransactionTreeResponse: { + transactionTree?: components['schemas']['JsTransactionTree'] + } + /** JsTopologyTransaction */ + JsTopologyTransaction: { + /** + * @description Assigned by the server. Useful for correlating logs. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * Format: int64 + * @description The absolute offset. The details of this field are described in ``community/ledger-api/README.md``. + * Required, it is a valid absolute offset (positive integer). + */ + offset: number + /** + * @description A valid synchronizer id. + * Identifies the synchronizer that synchronized the topology transaction. + * Required + */ + synchronizerId: string + /** + * @description The time at which the changes in the topology transaction become effective. There is a small delay between a + * topology transaction being sequenced and the changes it contains becoming effective. Topology transactions appear + * in order relative to a synchronizer based on their effective time rather than their sequencing time. + * Required + */ + recordTime: string + /** + * @description A non-empty list of topology events. + * Required + */ + events: components['schemas']['TopologyEvent'][] + /** + * @description Optional; ledger API trace context + * + * The trace context transported in this message corresponds to the trace context supplied + * by the client application in a HTTP2 header of the original command submission. + * We typically use a header to transfer this type of information. Here we use message + * body, because it is used in gRPC streams which do not support per message headers. + * This field will be populated with the trace context contained in the original submission. + * If that was not provided, a unique ledger-api-server generated trace context will be used + * instead. + */ + traceContext?: components['schemas']['TraceContext'] + } + /** + * JsTransaction + * @description Filtered view of an on-ledger transaction's create and archive events. + */ + JsTransaction: { + /** + * @description Assigned by the server. Useful for correlating logs. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * @description The ID of the command which resulted in this transaction. Missing for everyone except the submitting party. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + commandId?: string + /** + * @description The workflow ID used in command submission. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + /** + * @description Ledger effective time. + * Required + */ + effectiveAt: string + /** + * @description The collection of events. + * Contains: + * + * - ``CreatedEvent`` or ``ArchivedEvent`` in case of ACS_DELTA transaction shape + * - ``CreatedEvent`` or ``ExercisedEvent`` in case of LEDGER_EFFECTS transaction shape + * + * Required + */ + events: components['schemas']['Event'][] + /** + * Format: int64 + * @description The absolute offset. The details of this field are described in ``community/ledger-api/README.md``. + * Required, it is a valid absolute offset (positive integer). + */ + offset: number + /** + * @description A valid synchronizer id. + * Identifies the synchronizer that synchronized the transaction. + * Required + */ + synchronizerId: string + /** + * @description Optional; ledger API trace context + * + * The trace context transported in this message corresponds to the trace context supplied + * by the client application in a HTTP2 header of the original command submission. + * We typically use a header to transfer this type of information. Here we use message + * body, because it is used in gRPC streams which do not support per message headers. + * This field will be populated with the trace context contained in the original submission. + * If that was not provided, a unique ledger-api-server generated trace context will be used + * instead. + */ + traceContext?: components['schemas']['TraceContext'] + /** + * @description The time at which the transaction was recorded. The record time refers to the synchronizer + * which synchronized the transaction. + * Required + */ + recordTime: string + /** + * @description For transaction externally signed, contains the external transaction hash + * signed by the external party. Can be used to correlate an external submission with a committed transaction. + * Optional + */ + externalTransactionHash?: string + } + /** + * JsTransactionTree + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Complete view of an on-ledger transaction. + */ + JsTransactionTree: { + /** + * @description Assigned by the server. Useful for correlating logs. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * @description The ID of the command which resulted in this transaction. Missing for everyone except the submitting party. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + commandId?: string + /** + * @description The workflow ID used in command submission. Only set if the ``workflow_id`` for the command was set. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + /** + * @description Ledger effective time. + * Required + */ + effectiveAt: string + /** + * Format: int64 + * @description The absolute offset. The details of this field are described in ``community/ledger-api/README.md``. + * Required, it is a valid absolute offset (positive integer). + */ + offset: number + /** + * @description Changes to the ledger that were caused by this transaction. Nodes of the transaction tree. + * Each key must be a valid node ID (non-negative integer). + * Required + */ + eventsById: components['schemas']['Map_Int_TreeEvent'] + /** + * @description A valid synchronizer id. + * Identifies the synchronizer that synchronized the transaction. + * Required + */ + synchronizerId: string + /** + * @description Optional; ledger API trace context + * + * The trace context transported in this message corresponds to the trace context supplied + * by the client application in a HTTP2 header of the original command submission. + * We typically use a header to transfer this type of information. Here we use message + * body, because it is used in gRPC streams which do not support per message headers. + * This field will be populated with the trace context contained in the original submission. + * If that was not provided, a unique ledger-api-server generated trace context will be used + * instead. + */ + traceContext?: components['schemas']['TraceContext'] + /** + * @description The time at which the transaction was recorded. The record time refers to the synchronizer + * which synchronized the transaction. + * Required + */ + recordTime: string + } + /** + * JsUnassignedEvent + * @description Records that a contract has been unassigned, and it becomes unusable on the source synchronizer + */ + JsUnassignedEvent: { + value?: components['schemas']['UnassignedEvent'] + } + /** + * Kind + * @description Required + */ + Kind: + | { + CanActAs: components['schemas']['CanActAs'] + } + | { + CanExecuteAs: components['schemas']['CanExecuteAs'] + } + | { + CanExecuteAsAnyParty: components['schemas']['CanExecuteAsAnyParty'] + } + | { + CanReadAs: components['schemas']['CanReadAs'] + } + | { + CanReadAsAnyParty: components['schemas']['CanReadAsAnyParty'] + } + | { + Empty: components['schemas']['Empty8'] + } + | { + IdentityProviderAdmin: components['schemas']['IdentityProviderAdmin'] + } + | { + ParticipantAdmin: components['schemas']['ParticipantAdmin'] + } + /** ListIdentityProviderConfigsResponse */ + ListIdentityProviderConfigsResponse: { + identityProviderConfigs?: components['schemas']['IdentityProviderConfig'][] + } + /** ListKnownPartiesResponse */ + ListKnownPartiesResponse: { + /** + * @description The details of all Daml parties known by the participant. + * Required + */ + partyDetails: components['schemas']['PartyDetails'][] + /** + * @description Pagination token to retrieve the next page. + * Empty, if there are no further results. + */ + nextPageToken?: string + } + /** ListPackagesResponse */ + ListPackagesResponse: { + /** + * @description The IDs of all Daml-LF packages supported by the server. + * Each element must be a valid PackageIdString (as described in ``value.proto``). + * Required + */ + packageIds: string[] + } + /** ListUserRightsResponse */ + ListUserRightsResponse: { + /** @description All rights of the user. */ + rights?: components['schemas']['Right'][] + } + /** ListUsersResponse */ + ListUsersResponse: { + /** @description A subset of users of the participant node that fit into this page. */ + users?: components['schemas']['User'][] + /** + * @description Pagination token to retrieve the next page. + * Empty, if there are no further results. + */ + nextPageToken?: string + } + /** ListVettedPackagesRequest */ + ListVettedPackagesRequest: { + /** + * @description The package metadata filter the returned vetted packages set must satisfy. + * Optional + */ + packageMetadataFilter?: components['schemas']['PackageMetadataFilter'] + /** + * @description The topology filter the returned vetted packages set must satisfy. + * Optional + */ + topologyStateFilter?: components['schemas']['TopologyStateFilter'] + /** + * @description Pagination token to determine the specific page to fetch. Using the token + * guarantees that ``VettedPackages`` on a subsequent page are all greater + * (``VettedPackages`` are sorted by synchronizer ID then participant ID) than + * the last ``VettedPackages`` on a previous page. + * + * The server does not store intermediate results between calls chained by a + * series of page tokens. As a consequence, if new vetted packages are being + * added and a page is requested twice using the same token, more packages can + * be returned on the second call. + * + * Leave unspecified (i.e. as empty string) to fetch the first page. + * + * Optional + */ + pageToken?: string + /** + * Format: int32 + * @description Maximum number of ``VettedPackages`` results to return in a single page. + * + * If the page_size is unspecified (i.e. left as 0), the server will decide + * the number of results to be returned. + * + * If the page_size exceeds the maximum supported by the server, an + * error will be returned. + * + * To obtain the server's maximum consult the PackageService descriptor + * available in the VersionService. + * + * Optional + */ + pageSize?: number + } + /** ListVettedPackagesResponse */ + ListVettedPackagesResponse: { + /** + * @description All ``VettedPackages`` that contain at least one ``VettedPackage`` matching + * both a ``PackageMetadataFilter`` and a ``TopologyStateFilter``. + * Sorted by synchronizer_id then participant_id. + */ + vettedPackages?: components['schemas']['VettedPackages'][] + /** + * @description Pagination token to retrieve the next page. + * Empty string if there are no further results. + */ + nextPageToken?: string + } + /** Map_Filters */ + Map_Filters: { + [key: string]: components['schemas']['Filters'] + } + /** Map_Int_Field */ + Map_Int_Field: { + [key: string]: components['schemas']['Field'] + } + /** Map_Int_TreeEvent */ + Map_Int_TreeEvent: { + [key: string]: components['schemas']['TreeEvent'] + } + /** Map_String */ + Map_String: { + [key: string]: string + } + /** MinLedgerTime */ + MinLedgerTime: { + time?: components['schemas']['Time'] + } + /** MinLedgerTimeAbs */ + MinLedgerTimeAbs: { + value: string + } + /** MinLedgerTimeRel */ + MinLedgerTimeRel: { + value: components['schemas']['Duration'] + } + /** NoPrior */ + NoPrior: Record + /** + * ObjectMeta + * @description Represents metadata corresponding to a participant resource (e.g. a participant user or participant local information about a party). + * + * Based on ``ObjectMeta`` meta used in Kubernetes API. + * See https://github.com/kubernetes/apimachinery/blob/master/pkg/apis/meta/v1/generated.proto#L640 + */ + ObjectMeta: { + /** + * @description An opaque, non-empty value, populated by a participant server which represents the internal version of the resource + * this ``ObjectMeta`` message is attached to. The participant server will change it to a unique value each time the corresponding resource is updated. + * You must not rely on the format of resource version. The participant server might change it without notice. + * You can obtain the newest resource version value by issuing a read request. + * You may use it for concurrent change detection by passing it back unmodified in an update request. + * The participant server will then compare the passed value with the value maintained by the system to determine + * if any other updates took place since you had read the resource version. + * Upon a successful update you are guaranteed that no other update took place during your read-modify-write sequence. + * However, if another update took place during your read-modify-write sequence then your update will fail with an appropriate error. + * Concurrent change control is optional. It will be applied only if you include a resource version in an update request. + * When creating a new instance of a resource you must leave the resource version empty. + * Its value will be populated by the participant server upon successful resource creation. + * Optional + */ + resourceVersion?: string + /** + * @description A set of modifiable key-value pairs that can be used to represent arbitrary, client-specific metadata. + * Constraints: + * + * 1. The total size over all keys and values cannot exceed 256kb in UTF-8 encoding. + * 2. Keys are composed of an optional prefix segment and a required name segment such that: + * + * - key prefix, when present, must be a valid DNS subdomain with at most 253 characters, followed by a '/' (forward slash) character, + * - name segment must have at most 63 characters that are either alphanumeric ([a-z0-9A-Z]), or a '.' (dot), '-' (dash) or '_' (underscore); + * and it must start and end with an alphanumeric character. + * + * 3. Values can be any non-empty strings. + * + * Keys with empty prefix are reserved for end-users. + * Properties set by external tools or internally by the participant server must use non-empty key prefixes. + * Duplicate keys are disallowed by the semantics of the protobuf3 maps. + * See: https://developers.google.com/protocol-buffers/docs/proto3#maps + * Annotations may be a part of a modifiable resource. + * Use the resource's update RPC to update its annotations. + * In order to add a new annotation or update an existing one using an update RPC, provide the desired annotation in the update request. + * In order to remove an annotation using an update RPC, provide the target annotation's key but set its value to the empty string in the update request. + * Optional + * Modifiable + */ + annotations?: components['schemas']['Map_String'] + } + /** + * OffsetCheckpoint + * @description OffsetCheckpoints may be used to: + * + * - detect time out of commands. + * - provide an offset which can be used to restart consumption. + */ + OffsetCheckpoint: { + value?: components['schemas']['OffsetCheckpoint1'] + } + /** + * OffsetCheckpoint + * @description OffsetCheckpoints may be used to: + * + * - detect time out of commands. + * - provide an offset which can be used to restart consumption. + */ + OffsetCheckpoint1: { + /** + * Format: int64 + * @description The participant's offset, the details of the offset field are described in ``community/ledger-api/README.md``. + * Required, must be a valid absolute offset (positive integer). + */ + offset: number + synchronizerTimes?: components['schemas']['SynchronizerTime'][] + } + /** + * OffsetCheckpoint + * @description OffsetCheckpoints may be used to: + * + * - detect time out of commands. + * - provide an offset which can be used to restart consumption. + */ + OffsetCheckpoint2: { + value?: components['schemas']['OffsetCheckpoint1'] + } + /** + * OffsetCheckpoint + * @description OffsetCheckpoints may be used to: + * + * - detect time out of commands. + * - provide an offset which can be used to restart consumption. + */ + OffsetCheckpoint3: { + value?: components['schemas']['OffsetCheckpoint1'] + } + /** OffsetCheckpointFeature */ + OffsetCheckpointFeature: { + /** @description The maximum delay to emmit a new OffsetCheckpoint if it exists */ + maxOffsetCheckpointEmissionDelay?: components['schemas']['Duration'] + } + /** Operation */ + Operation: + | { + Empty: components['schemas']['Empty5'] + } + | { + Unvet: components['schemas']['Unvet'] + } + | { + Vet: components['schemas']['Vet'] + } + /** PackageFeature */ + PackageFeature: { + /** + * Format: int32 + * @description The maximum number of vetted packages the server can return in a single + * response (page) when listing them. + */ + maxVettedPackagesPageSize?: number + } + /** + * PackageMetadataFilter + * @description Filter the VettedPackages by package metadata. + * + * A PackageMetadataFilter without package_ids and without package_name_prefixes + * matches any vetted package. + * + * Non-empty fields specify candidate values of which at least one must match. + * If both fields are set, then a candidate is returned if it matches one of the fields. + */ + PackageMetadataFilter: { + /** + * @description If this list is non-empty, any vetted package with a package ID in this + * list will match the filter. + */ + packageIds?: string[] + /** + * @description If this list is non-empty, any vetted package with a name matching at least + * one prefix in this list will match the filter. + */ + packageNamePrefixes?: string[] + } + /** PackagePreference */ + PackagePreference: { + /** + * @description The package reference of the preferred package. + * Required + */ + packageReference: components['schemas']['PackageReference'] + /** + * @description The synchronizer for which the preferred package was computed. + * If the synchronizer_id was specified in the request, then it matches the request synchronizer_id. + * Required + */ + synchronizerId: string + } + /** PackageReference */ + PackageReference: { + /** @description Required */ + packageId: string + /** @description Required */ + packageName: string + /** @description Required */ + packageVersion: string + } + /** + * PackageVettingRequirement + * @description Defines a package-name for which the commonly vetted package with the highest version must be found. + */ + PackageVettingRequirement: { + /** + * @description The parties whose participants' vetting state should be considered when resolving the preferred package. + * Required + */ + parties: string[] + /** + * @description The package-name for which the preferred package should be resolved. + * Required + */ + packageName: string + } + /** ParticipantAdmin */ + ParticipantAdmin: { + value: components['schemas']['ParticipantAdmin1'] + } + /** ParticipantAdmin */ + ParticipantAdmin1: Record + /** ParticipantAuthorizationAdded */ + ParticipantAuthorizationAdded: { + value?: components['schemas']['ParticipantAuthorizationAdded1'] + } + /** ParticipantAuthorizationAdded */ + ParticipantAuthorizationAdded1: { + /** @description Required */ + partyId: string + /** @description Required */ + participantId: string + /** + * @description Required + * @enum {string} + */ + participantPermission: + | 'PARTICIPANT_PERMISSION_UNSPECIFIED' + | 'PARTICIPANT_PERMISSION_SUBMISSION' + | 'PARTICIPANT_PERMISSION_CONFIRMATION' + | 'PARTICIPANT_PERMISSION_OBSERVATION' + } + /** ParticipantAuthorizationChanged */ + ParticipantAuthorizationChanged: { + value?: components['schemas']['ParticipantAuthorizationChanged1'] + } + /** ParticipantAuthorizationChanged */ + ParticipantAuthorizationChanged1: { + /** @description Required */ + partyId: string + /** @description Required */ + participantId: string + /** + * @description Required + * @enum {string} + */ + participantPermission: + | 'PARTICIPANT_PERMISSION_UNSPECIFIED' + | 'PARTICIPANT_PERMISSION_SUBMISSION' + | 'PARTICIPANT_PERMISSION_CONFIRMATION' + | 'PARTICIPANT_PERMISSION_OBSERVATION' + } + /** ParticipantAuthorizationRevoked */ + ParticipantAuthorizationRevoked: { + value?: components['schemas']['ParticipantAuthorizationRevoked1'] + } + /** ParticipantAuthorizationRevoked */ + ParticipantAuthorizationRevoked1: { + /** @description Required */ + partyId: string + /** @description Required */ + participantId: string + } + /** + * ParticipantAuthorizationTopologyFormat + * @description A format specifying which participant authorization topology transactions to include and how to render them. + */ + ParticipantAuthorizationTopologyFormat: { + /** + * @description List of parties for which the topology transactions should be sent. + * Empty means: for all parties. + */ + parties?: string[] + } + /** PartyDetails */ + PartyDetails: { + /** + * @description The stable unique identifier of a Daml party. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Required + */ + party: string + /** + * @description true if party is hosted by the participant and the party shares the same identity provider as the user issuing the request. + * Optional + */ + isLocal?: boolean + /** + * @description Participant-local metadata of this party. + * Optional, + * Modifiable + */ + localMetadata?: components['schemas']['ObjectMeta'] + /** + * @description The id of the ``Identity Provider`` + * Optional, if not set, there could be 3 options: + * + * 1. the party is managed by the default identity provider. + * 2. party is not hosted by the participant. + * 3. party is hosted by the participant, but is outside of the user's identity provider. + */ + identityProviderId?: string + } + /** PartyManagementFeature */ + PartyManagementFeature: { + /** + * Format: int32 + * @description The maximum number of parties the server can return in a single response (page). + */ + maxPartiesPageSize?: number + } + /** + * PartySignatures + * @description Additional signatures provided by the submitting parties + */ + PartySignatures: { + /** + * @description Additional signatures provided by all individual parties + * Required + */ + signatures: components['schemas']['SinglePartySignatures'][] + } + /** + * PrefetchContractKey + * @description Preload contracts + */ + PrefetchContractKey: { + /** + * @description The template of contract the client wants to prefetch. + * Both package-name and package-id reference identifier formats for the template-id are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + templateId: string + /** + * @description The key of the contract the client wants to prefetch. + * Required + */ + contractKey: unknown + } + /** Prior */ + Prior: { + /** Format: int32 */ + value: number + } + /** + * PriorTopologySerial + * @description The serial of last ``VettedPackages`` topology transaction on a given + * participant and synchronizer. + */ + PriorTopologySerial: { + serial?: components['schemas']['Serial'] + } + /** ProtoAny */ + ProtoAny: { + typeUrl: string + value: string + unknownFields: components['schemas']['UnknownFieldSet'] + valueDecoded?: string + } + /** + * Reassignment + * @description Complete view of an on-ledger reassignment. + */ + Reassignment: { + value?: components['schemas']['JsReassignment'] + } + /** + * Reassignment + * @description Complete view of an on-ledger reassignment. + */ + Reassignment1: { + value?: components['schemas']['JsReassignment'] + } + /** ReassignmentCommand */ + ReassignmentCommand: { + command?: components['schemas']['Command1'] + } + /** ReassignmentCommands */ + ReassignmentCommands: { + /** + * @description Identifier of the on-ledger workflow that this command is a part of. + * Must be a valid LedgerString (as described in ``value.proto``). + * Optional + */ + workflowId?: string + /** + * @description Uniquely identifies the participant user that issued the command. + * Must be a valid UserIdString (as described in ``value.proto``). + * Required unless authentication is used with a user token. + * In that case, the token's user-id will be used for the request's user_id. + */ + userId?: string + /** + * @description Uniquely identifies the command. + * The triple (user_id, submitter, command_id) constitutes the change ID for the intended ledger change. + * The change ID can be used for matching the intended ledger changes with all their completions. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + commandId: string + /** + * @description Party on whose behalf the command should be executed. + * If ledger API authorization is enabled, then the authorization metadata must authorize the sender of the request + * to act on behalf of the given party. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Required + */ + submitter: string + /** + * @description A unique identifier to distinguish completions for different submissions with the same change ID. + * Typically a random UUID. Applications are expected to use a different UUID for each retry of a submission + * with the same change ID. + * Must be a valid LedgerString (as described in ``value.proto``). + * + * If omitted, the participant or the committer may set a value of their choice. + * Optional + */ + submissionId?: string + /** @description Individual elements of this reassignment. Must be non-empty. */ + commands?: components['schemas']['ReassignmentCommand'][] + } + /** + * RevokeUserRightsRequest + * @description Remove the rights from the set of rights granted to the user. + * + * Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(identity_provider_id)`` + */ + RevokeUserRightsRequest: { + /** + * @description The user from whom to revoke rights. + * Required + */ + userId: string + /** + * @description The rights to revoke. + * Optional + */ + rights?: components['schemas']['Right'][] + /** + * @description The id of the ``Identity Provider`` + * Optional, if not set, assume the user is managed by the default identity provider. + */ + identityProviderId?: string + } + /** RevokeUserRightsResponse */ + RevokeUserRightsResponse: { + /** @description The rights that were actually revoked by the request. */ + newlyRevokedRights?: components['schemas']['Right'][] + } + /** + * Right + * @description A right granted to a user. + */ + Right: { + kind?: components['schemas']['Kind'] + } + /** Serial */ + Serial: + | { + Empty: components['schemas']['Empty6'] + } + | { + NoPrior: components['schemas']['NoPrior'] + } + | { + Prior: components['schemas']['Prior'] + } + /** Signature */ + Signature: { + format?: string + signature?: string + /** @description The fingerprint/id of the keypair used to create this signature and needed to verify. */ + signedBy?: string + /** @description The signing algorithm specification used to produce this signature */ + signingAlgorithmSpec?: string + } + /** SignedTransaction */ + SignedTransaction: { + transaction: string + signatures?: components['schemas']['Signature'][] + } + /** SigningPublicKey */ + SigningPublicKey: { + /** + * @description The serialization format of the public key + * @example CRYPTO_KEY_FORMAT_DER_X509_SUBJECT_PUBLIC_KEY_INFO + */ + format?: string + /** @description Serialized public key in the format specified above */ + keyData?: string + /** + * @description The key specification + * @example SIGNING_KEY_SPEC_EC_CURVE25519 + */ + keySpec?: string + } + /** + * SinglePartySignatures + * @description Signatures provided by a single party + */ + SinglePartySignatures: { + /** + * @description Submitting party + * Required + */ + party: string + /** + * @description Signatures + * Required + */ + signatures: components['schemas']['Signature'][] + } + /** + * SubmitAndWaitForReassignmentRequest + * @description This reassignment is executed as a single atomic update. + */ + SubmitAndWaitForReassignmentRequest: { + /** + * @description The reassignment commands to be submitted. + * Required + */ + reassignmentCommands: components['schemas']['ReassignmentCommands'] + /** + * @description Optional + * If no event_format provided, the result will contain no events. + * The events in the result, will take shape TRANSACTION_SHAPE_ACS_DELTA. + */ + eventFormat?: components['schemas']['EventFormat'] + } + /** SubmitAndWaitResponse */ + SubmitAndWaitResponse: { + /** + * @description The id of the transaction that resulted from the submitted command. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + updateId: string + /** + * Format: int64 + * @description The details of the offset field are described in ``community/ledger-api/README.md``. + * Required + */ + completionOffset: number + } + /** SubmitReassignmentRequest */ + SubmitReassignmentRequest: { + /** + * @description The reassignment command to be submitted. + * Required + */ + reassignmentCommands: components['schemas']['ReassignmentCommands'] + } + /** SubmitReassignmentResponse */ + SubmitReassignmentResponse: Record + /** SubmitResponse */ + SubmitResponse: Record + /** SynchronizerTime */ + SynchronizerTime: { + /** + * @description The id of the synchronizer. + * Required + */ + synchronizerId: string + /** + * @description All commands with a maximum record time below this value MUST be considered lost if their completion has not arrived before this checkpoint. + * Required + */ + recordTime: string + } + /** + * TemplateFilter + * @description This filter matches contracts of a specific template. + */ + TemplateFilter: { + value?: components['schemas']['TemplateFilter1'] + } + /** + * TemplateFilter + * @description This filter matches contracts of a specific template. + */ + TemplateFilter1: { + /** + * @description A template for which the payload should be included in the response. + * The ``template_id`` needs to be valid: corresponding template should be defined in + * one of the available packages at the time of the query. + * Both package-name and package-id reference formats for the identifier are supported. + * Note: The package-id reference identifier format is deprecated. We plan to end support for this format in version 3.4. + * + * Required + */ + templateId: string + /** + * @description Whether to include a ``created_event_blob`` in the returned ``CreatedEvent``. + * Use this to access the contract event payload in your API client + * for submitting it as a disclosed contract with future commands. + * Optional + */ + includeCreatedEventBlob?: boolean + } + /** Time */ + Time: + | { + Empty: components['schemas']['Empty9'] + } + | { + MinLedgerTimeAbs: components['schemas']['MinLedgerTimeAbs'] + } + | { + MinLedgerTimeRel: components['schemas']['MinLedgerTimeRel'] + } + /** TopologyEvent */ + TopologyEvent: { + event?: components['schemas']['TopologyEventEvent'] + } + /** TopologyEventEvent */ + TopologyEventEvent: + | { + Empty: components['schemas']['Empty7'] + } + | { + ParticipantAuthorizationAdded: components['schemas']['ParticipantAuthorizationAdded'] + } + | { + ParticipantAuthorizationChanged: components['schemas']['ParticipantAuthorizationChanged'] + } + | { + ParticipantAuthorizationRevoked: components['schemas']['ParticipantAuthorizationRevoked'] + } + /** + * TopologyFormat + * @description A format specifying which topology transactions to include and how to render them. + */ + TopologyFormat: { + /** + * @description Include participant authorization topology events in streams. + * Optional, if unset no participant authorization topology events are emitted in the stream. + */ + includeParticipantAuthorizationEvents?: components['schemas']['ParticipantAuthorizationTopologyFormat'] + } + /** + * TopologyStateFilter + * @description Filter the vetted packages by the participant and synchronizer that they are + * hosted on. + * + * Empty fields are ignored, such that a ``TopologyStateFilter`` without + * participant_ids and without synchronizer_ids matches a vetted package hosted + * on any participant and synchronizer. + * + * Non-empty fields specify candidate values of which at least one must match. + * If both fields are set then at least one candidate value must match from each + * field. + */ + TopologyStateFilter: { + /** + * @description If this list is non-empty, only vetted packages hosted on participants + * listed in this field match the filter. + * Query the current Ledger API's participant's ID via the public + * ``GetParticipantId`` command in ``PartyManagementService``. + */ + participantIds?: string[] + /** + * @description If this list is non-empty, only vetted packages from the topology state of + * the synchronizers in this list match the filter. + */ + synchronizerIds?: string[] + } + /** TopologyTransaction */ + TopologyTransaction: { + value?: components['schemas']['JsTopologyTransaction'] + } + /** TraceContext */ + TraceContext: { + /** @description https://www.w3.org/TR/trace-context/ */ + traceparent?: string + tracestate?: string + } + /** + * Transaction + * @description Filtered view of an on-ledger transaction's create and archive events. + */ + Transaction: { + value?: components['schemas']['JsTransaction'] + } + /** + * TransactionFilter + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Used both for filtering create and archive events as well as for filtering transaction trees. + */ + TransactionFilter: { + /** + * @description Each key must be a valid PartyIdString (as described in ``value.proto``). + * The interpretation of the filter depends on the transaction-shape being filtered: + * + * 1. For **transaction trees** (used in GetUpdateTreesResponse for backwards compatibility) all party keys used as + * wildcard filters, and all subtrees whose root has one of the listed parties as an informee are returned. + * If there are ``CumulativeFilter``s, those will control returned ``CreatedEvent`` fields where applicable, but will + * not be used for template/interface filtering. + * 2. For **ledger-effects** create and exercise events are returned, for which the witnesses include at least one of + * the listed parties and match the per-party filter. + * 3. For **transaction and active-contract-set streams** create and archive events are returned for all contracts whose + * stakeholders include at least one of the listed parties and match the per-party filter. + */ + filtersByParty?: components['schemas']['Map_Filters'] + /** + * @description Wildcard filters that apply to all the parties existing on the participant. The interpretation of the filters is the same + * with the per-party filter as described above. + */ + filtersForAnyParty?: components['schemas']['Filters'] + } + /** + * TransactionFormat + * @description A format that specifies what events to include in Daml transactions + * and what data to compute and include for them. + */ + TransactionFormat: { + /** @description Required */ + eventFormat: components['schemas']['EventFormat'] + /** + * @description What transaction shape to use for interpreting the filters of the event format. + * Required + * @enum {string} + */ + transactionShape: + | 'TRANSACTION_SHAPE_UNSPECIFIED' + | 'TRANSACTION_SHAPE_ACS_DELTA' + | 'TRANSACTION_SHAPE_LEDGER_EFFECTS' + } + /** + * TransactionTree + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Complete view of an on-ledger transaction. + */ + TransactionTree: { + value?: components['schemas']['JsTransactionTree'] + } + /** + * TreeEvent + * @description Provided for backwards compatibility, it will be removed in the Canton version 3.5.0. + * Each tree event message type below contains a ``witness_parties`` field which + * indicates the subset of the requested parties that can see the event + * in question. + * + * Note that transaction trees might contain events with + * _no_ witness parties, which were included simply because they were + * children of events which have witnesses. + */ + TreeEvent: + | { + CreatedTreeEvent: components['schemas']['CreatedTreeEvent'] + } + | { + ExercisedTreeEvent: components['schemas']['ExercisedTreeEvent'] + } + /** Tuple2_String_String */ + Tuple2_String_String: string[] + /** + * UnassignCommand + * @description Unassign a contract + */ + UnassignCommand: { + value?: components['schemas']['UnassignCommand1'] + } + /** + * UnassignCommand + * @description Unassign a contract + */ + UnassignCommand1: { + /** + * @description The ID of the contract the client wants to unassign. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description The ID of the source synchronizer + * Must be a valid synchronizer id + * Required + */ + source: string + /** + * @description The ID of the target synchronizer + * Must be a valid synchronizer id + * Required + */ + target: string + } + /** + * UnassignedEvent + * @description Records that a contract has been unassigned, and it becomes unusable on the source synchronizer + */ + UnassignedEvent: { + /** + * @description The ID of the unassignment. This needs to be used as an input for a assign ReassignmentCommand. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + reassignmentId: string + /** + * @description The ID of the reassigned contract. + * Must be a valid LedgerString (as described in ``value.proto``). + * Required + */ + contractId: string + /** + * @description The template of the reassigned contract. + * The identifier uses the package-id reference format. + * + * Required + */ + templateId: string + /** + * @description The ID of the source synchronizer + * Must be a valid synchronizer id + * Required + */ + source: string + /** + * @description The ID of the target synchronizer + * Must be a valid synchronizer id + * Required + */ + target: string + /** + * @description Party on whose behalf the unassign command was executed. + * Empty if the unassignment happened offline via the repair service. + * Must be a valid PartyIdString (as described in ``value.proto``). + * Optional + */ + submitter?: string + /** + * Format: int64 + * @description Each corresponding assigned and unassigned event has the same reassignment_counter. This strictly increases + * with each unassign command for the same contract. Creation of the contract corresponds to reassignment_counter + * equals zero. + * Required + */ + reassignmentCounter: number + /** + * @description Assignment exclusivity + * Before this time (measured on the target synchronizer), only the submitter of the unassignment can initiate the assignment + * Defined for reassigning participants. + * Optional + */ + assignmentExclusivity?: string + /** + * @description The parties that are notified of this event. + * Required + */ + witnessParties: string[] + /** + * @description The package name of the contract. + * Required + */ + packageName: string + /** + * Format: int64 + * @description The offset of origin. + * Offsets are managed by the participant nodes. + * Reassignments can thus NOT be assumed to have the same offsets on different participant nodes. + * Required, it is a valid absolute offset (positive integer) + */ + offset: number + /** + * Format: int32 + * @description The position of this event in the originating reassignment. + * Node IDs are not necessarily equal across participants, + * as these may see different projections/parts of reassignments. + * Required, must be valid node ID (non-negative integer) + */ + nodeId: number + } + /** UnknownFieldSet */ + UnknownFieldSet: { + fields: components['schemas']['Map_Int_Field'] + } + /** Unvet */ + Unvet: { + value: components['schemas']['Unvet1'] + } + /** Unvet */ + Unvet1: { + packages?: components['schemas']['VettedPackagesRef'][] + } + /** Update */ + Update: + | { + OffsetCheckpoint: components['schemas']['OffsetCheckpoint2'] + } + | { + Reassignment: components['schemas']['Reassignment'] + } + | { + TopologyTransaction: components['schemas']['TopologyTransaction'] + } + | { + Transaction: components['schemas']['Transaction'] + } + /** Update */ + Update1: + | { + OffsetCheckpoint: components['schemas']['OffsetCheckpoint3'] + } + | { + Reassignment: components['schemas']['Reassignment1'] + } + | { + TransactionTree: components['schemas']['TransactionTree'] + } + /** + * UpdateFormat + * @description A format specifying what updates to include and how to render them. + */ + UpdateFormat: { + /** + * @description Include Daml transactions in streams. + * Optional, if unset, no transactions are emitted in the stream. + */ + includeTransactions?: components['schemas']['TransactionFormat'] + /** + * @description Include (un)assignments in the stream. + * The events in the result take the shape TRANSACTION_SHAPE_ACS_DELTA. + * Optional, if unset, no (un)assignments are emitted in the stream. + */ + includeReassignments?: components['schemas']['EventFormat'] + /** + * @description Include topology events in streams. + * Optional, if unset no topology events are emitted in the stream. + */ + includeTopologyEvents?: components['schemas']['TopologyFormat'] + } + /** UpdateIdentityProviderConfigRequest */ + UpdateIdentityProviderConfigRequest: { + /** + * @description The identity provider config to update. + * Required, + * Modifiable + */ + identityProviderConfig: components['schemas']['IdentityProviderConfig'] + /** + * @description An update mask specifies how and which properties of the ``IdentityProviderConfig`` message are to be updated. + * An update mask consists of a set of update paths. + * A valid update path points to a field or a subfield relative to the ``IdentityProviderConfig`` message. + * A valid update mask must: + * + * 1. contain at least one update path, + * 2. contain only valid update paths. + * + * Fields that can be updated are marked as ``Modifiable``. + * For additional information see the documentation for standard protobuf3's ``google.protobuf.FieldMask``. + * Required + */ + updateMask: components['schemas']['FieldMask'] + } + /** UpdateIdentityProviderConfigResponse */ + UpdateIdentityProviderConfigResponse: { + /** @description Updated identity provider config */ + identityProviderConfig?: components['schemas']['IdentityProviderConfig'] + } + /** + * UpdatePartyDetailsRequest + * @description Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(party_details.identity_provider_id)`` + */ + UpdatePartyDetailsRequest: { + /** + * @description Party to be updated + * Required, + * Modifiable + */ + partyDetails: components['schemas']['PartyDetails'] + /** + * @description An update mask specifies how and which properties of the ``PartyDetails`` message are to be updated. + * An update mask consists of a set of update paths. + * A valid update path points to a field or a subfield relative to the ``PartyDetails`` message. + * A valid update mask must: + * + * 1. contain at least one update path, + * 2. contain only valid update paths. + * + * Fields that can be updated are marked as ``Modifiable``. + * An update path can also point to non-``Modifiable`` fields such as 'party' and 'local_metadata.resource_version' + * because they are used: + * + * 1. to identify the party details resource subject to the update, + * 2. for concurrent change control. + * + * An update path can also point to non-``Modifiable`` fields such as 'is_local' + * as long as the values provided in the update request match the server values. + * Examples of update paths: 'local_metadata.annotations', 'local_metadata'. + * For additional information see the documentation for standard protobuf3's ``google.protobuf.FieldMask``. + * For similar Ledger API see ``com.daml.ledger.api.v2.admin.UpdateUserRequest``. + * Required + */ + updateMask: components['schemas']['FieldMask'] + } + /** UpdatePartyDetailsResponse */ + UpdatePartyDetailsResponse: { + /** @description Updated party details */ + partyDetails?: components['schemas']['PartyDetails'] + } + /** + * UpdateUserIdentityProviderIdRequest + * @description Required authorization: ``HasRight(ParticipantAdmin)`` + */ + UpdateUserIdentityProviderIdRequest: { + /** @description User to update */ + userId?: string + /** @description Current identity provider ID of the user */ + sourceIdentityProviderId?: string + /** @description Target identity provider ID of the user */ + targetIdentityProviderId?: string + } + /** UpdateUserIdentityProviderIdResponse */ + UpdateUserIdentityProviderIdResponse: Record + /** + * UpdateUserRequest + * @description Required authorization: ``HasRight(ParticipantAdmin) OR IsAuthenticatedIdentityProviderAdmin(user.identity_provider_id)`` + */ + UpdateUserRequest: { + /** + * @description The user to update. + * Required, + * Modifiable + */ + user: components['schemas']['User'] + /** + * @description An update mask specifies how and which properties of the ``User`` message are to be updated. + * An update mask consists of a set of update paths. + * A valid update path points to a field or a subfield relative to the ``User`` message. + * A valid update mask must: + * + * 1. contain at least one update path, + * 2. contain only valid update paths. + * + * Fields that can be updated are marked as ``Modifiable``. + * An update path can also point to a non-``Modifiable`` fields such as 'id' and 'metadata.resource_version' + * because they are used: + * + * 1. to identify the user resource subject to the update, + * 2. for concurrent change control. + * + * Examples of valid update paths: 'primary_party', 'metadata', 'metadata.annotations'. + * For additional information see the documentation for standard protobuf3's ``google.protobuf.FieldMask``. + * For similar Ledger API see ``com.daml.ledger.api.v2.admin.UpdatePartyDetailsRequest``. + * Required + */ + updateMask: components['schemas']['FieldMask'] + } + /** UpdateUserResponse */ + UpdateUserResponse: { + /** @description Updated user */ + user?: components['schemas']['User'] + } + /** UpdateVettedPackagesRequest */ + UpdateVettedPackagesRequest: { + /** + * @description Changes to apply to the current vetting state of the participant on the + * specified synchronizer. The changes are applied in order. + * Any package not changed will keep their previous vetting state. + */ + changes?: components['schemas']['VettedPackagesChange'][] + /** + * @description If dry_run is true, then the changes are only prepared, but not applied. If + * a request would trigger an error when run (e.g. TOPOLOGY_DEPENDENCIES_NOT_VETTED), + * it will also trigger an error when dry_run. + * + * Use this flag to preview a change before applying it. + */ + dryRun?: boolean + /** + * @description If set, the requested changes will take place on the specified + * synchronizer. If synchronizer_id is unset and the participant is only + * connected to a single synchronizer, that synchronizer will be used by + * default. If synchronizer_id is unset and the participant is connected to + * multiple synchronizers, the request will error out with + * PACKAGE_SERVICE_CANNOT_AUTODETECT_SYNCHRONIZER. + * + * Optional + */ + synchronizerId?: string + /** + * @description The serial of the last ``VettedPackages`` topology transaction of this + * participant and on this synchronizer. + * + * Execution of the request fails if this is not correct. Use this to guard + * against concurrent changes. + * + * If left unspecified, no validation is done against the last transaction's + * serial. + * + * Optional + */ + expectedTopologySerial?: components['schemas']['PriorTopologySerial'] + /** + * @description Controls whether potentially unsafe vetting updates are allowed. + * + * Optional, defaults to FORCE_FLAG_UNSPECIFIED. + */ + updateVettedPackagesForceFlags?: ( + | 'UPDATE_VETTED_PACKAGES_FORCE_FLAG_UNSPECIFIED' + | 'UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_VET_INCOMPATIBLE_UPGRADES' + | 'UPDATE_VETTED_PACKAGES_FORCE_FLAG_ALLOW_UNVETTED_DEPENDENCIES' + )[] + } + /** UpdateVettedPackagesResponse */ + UpdateVettedPackagesResponse: { + /** + * @description All vetted packages on this participant and synchronizer, before the + * specified changes. Empty if no vetting state existed beforehand. + */ + pastVettedPackages?: components['schemas']['VettedPackages'] + /** @description All vetted packages on this participant and synchronizer, after the specified changes. */ + newVettedPackages?: components['schemas']['VettedPackages'] + } + /** + * UploadDarFileResponse + * @description A message that is received when the upload operation succeeded. + */ + UploadDarFileResponse: Record + /** + * User + * @description Users and rights + * ///////////////// + * Users are used to dynamically manage the rights given to Daml applications. + * They are stored and managed per participant node. + */ + User: { + /** + * @description The user identifier, which must be a non-empty string of at most 128 + * characters that are either alphanumeric ASCII characters or one of the symbols "@^$.!`-#+'~_|:()". + * Required + */ + id: string + /** + * @description The primary party as which this user reads and acts by default on the ledger + * *provided* it has the corresponding ``CanReadAs(primary_party)`` or + * ``CanActAs(primary_party)`` rights. + * Ledger API clients SHOULD set this field to a non-empty value for all users to + * enable the users to act on the ledger using their own Daml party. + * Users for participant administrators MAY have an associated primary party. + * Optional, + * Modifiable + */ + primaryParty?: string + /** + * @description When set, then the user is denied all access to the Ledger API. + * Otherwise, the user has access to the Ledger API as per the user's rights. + * Optional, + * Modifiable + */ + isDeactivated?: boolean + /** + * @description The metadata of this user. + * Note that the ``metadata.resource_version`` tracks changes to the properties described by the ``User`` message and not the user's rights. + * Optional, + * Modifiable + */ + metadata?: components['schemas']['ObjectMeta'] + /** + * @description The ID of the identity provider configured by ``Identity Provider Config`` + * Optional, if not set, assume the user is managed by the default identity provider. + */ + identityProviderId?: string + } + /** UserManagementFeature */ + UserManagementFeature: { + /** @description Whether the Ledger API server provides the user management service. */ + supported?: boolean + /** + * Format: int32 + * @description The maximum number of rights that can be assigned to a single user. + * Servers MUST support at least 100 rights per user. + * A value of 0 means that the server enforces no rights per user limit. + */ + maxRightsPerUser?: number + /** + * Format: int32 + * @description The maximum number of users the server can return in a single response (page). + * Servers MUST support at least a 100 users per page. + * A value of 0 means that the server enforces no page size limit. + */ + maxUsersPageSize?: number + } + /** Vet */ + Vet: { + value: components['schemas']['Vet1'] + } + /** Vet */ + Vet1: { + packages?: components['schemas']['VettedPackagesRef'][] + newValidFromInclusive?: string + newValidUntilExclusive?: string + } + /** + * VettedPackage + * @description A package that is vetting on a given participant and synchronizer, + * modelled after ``VettedPackage`` in `topology.proto `_, + * enriched with the package name and version. + */ + VettedPackage: { + /** @description Package ID of this package. Always present. */ + packageId?: string + /** + * @description The time from which this package is vetted. Empty if vetting time has no + * lower bound. + */ + validFromInclusive?: string + /** + * @description The time until which this package is vetted. Empty if vetting time has no + * upper bound. + */ + validUntilExclusive?: string + /** + * @description Name of this package. + * Only available if the package has been uploaded to the current participant. + * If unavailable, is empty string. + */ + packageName?: string + /** + * @description Version of this package. + * Only available if the package has been uploaded to the current participant. + * If unavailable, is empty string. + */ + packageVersion?: string + } + /** + * VettedPackages + * @description The list of packages vetted on a given participant and synchronizer, modelled + * after ``VettedPackages`` in `topology.proto `_. + * The list only contains packages that matched a filter in the query that + * originated it. + */ + VettedPackages: { + /** + * @description Sorted by package_name and package_version where known, and package_id as a + * last resort. + */ + packages?: components['schemas']['VettedPackage'][] + /** @description Participant on which these packages are vetted. Always present. */ + participantId?: string + /** @description Synchronizer on which these packages are vetted. Always present. */ + synchronizerId?: string + /** + * Format: int32 + * @description Serial of last ``VettedPackages`` topology transaction of this participant + * and on this synchronizer. Always present. + */ + topologySerial?: number + } + /** + * VettedPackagesChange + * @description A change to the set of vetted packages. + */ + VettedPackagesChange: { + operation?: components['schemas']['Operation'] + } + /** + * VettedPackagesRef + * @description A reference to identify one or more packages. + * + * A reference matches a package if its ``package_id`` matches the package's ID, + * its ``package_name`` matches the package's name, and its ``package_version`` + * matches the package's version. If an attribute in the reference is left + * unspecified (i.e. as an empty string), that attribute is treated as a + * wildcard. At a minimum, ``package_id`` or the ``package_name`` must be + * specified. + * + * If a reference does not match any package, the reference is considered + * unresolved and the entire update request is rejected. + */ + VettedPackagesRef: { + /** + * @description Package's package id must be the same as this field. + * Optional + */ + packageId?: string + /** + * @description Package's name must be the same as this field. + * Optional + */ + packageName?: string + /** + * @description Package's version must be the same as this field. + * Optional + */ + packageVersion?: string + } + /** + * WildcardFilter + * @description This filter matches all templates. + */ + WildcardFilter: { + value?: components['schemas']['WildcardFilter1'] + } + /** + * WildcardFilter + * @description This filter matches all templates. + */ + WildcardFilter1: { + /** + * @description Whether to include a ``created_event_blob`` in the returned ``CreatedEvent``. + * Use this to access the contract create event payload in your API client + * for submitting it as a disclosed contract with future commands. + * Optional + */ + includeCreatedEventBlob?: boolean + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} +export type $defs = Record +export interface operations { + 'postV2CommandsSubmit-and-wait': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsCommands'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SubmitAndWaitResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2CommandsSubmit-and-wait-for-transaction': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsSubmitAndWaitForTransactionRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsSubmitAndWaitForTransactionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2CommandsSubmit-and-wait-for-reassignment': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['SubmitAndWaitForReassignmentRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsSubmitAndWaitForReassignmentResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2CommandsSubmit-and-wait-for-transaction-tree': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsCommands'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsSubmitAndWaitForTransactionTreeResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2CommandsAsyncSubmit: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsCommands'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SubmitResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2CommandsAsyncSubmit-reassignment': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['SubmitReassignmentRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['SubmitReassignmentResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2CommandsCompletions: { + parameters: { + query?: { + /** @description maximum number of elements to return, this param is ignored if is bigger than server setting */ + limit?: number + /** @description timeout to complete and send result if no new elements are received (for open ended streams) */ + stream_idle_timeout_ms?: number + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CompletionStreamRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CompletionStreamResponse'][] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2EventsEvents-by-contract-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetEventsByContractIdRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetEventsByContractIdResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2Version: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetLedgerApiVersionResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2DarsValidate: { + parameters: { + query?: { + synchronizerId?: string + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/octet-stream': string + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter synchronizerId */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Dars: { + parameters: { + query?: { + vetAllPackages?: boolean + synchronizerId?: string + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/octet-stream': string + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UploadDarFileResponse'] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter vetAllPackages, Invalid value for: query parameter synchronizerId */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2Packages: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListPackagesResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Packages: { + parameters: { + query?: { + vetAllPackages?: boolean + synchronizerId?: string + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/octet-stream': string + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UploadDarFileResponse'] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter vetAllPackages, Invalid value for: query parameter synchronizerId */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2PackagesPackage-id': { + parameters: { + query?: never + header?: never + path: { + 'package-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + 'Canton-Package-Hash': string + [name: string]: unknown + } + content: { + 'application/octet-stream': string + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2PackagesPackage-idStatus': { + parameters: { + query?: never + header?: never + path: { + 'package-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetPackageStatusResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2Package-vetting': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['ListVettedPackagesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListVettedPackagesResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Package-vetting': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateVettedPackagesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateVettedPackagesResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2Parties: { + parameters: { + query?: { + 'identity-provider-id'?: string + 'filter-party'?: string + /** @description maximum number of elements in a returned page */ + pageSize?: number + /** @description token - to continue results from a given page, leave empty to start from the beginning of the list, obtain token from the result of previous page */ + pageToken?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListKnownPartiesResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter identity-provider-id, Invalid value for: query parameter filter-party, Invalid value for: query parameter pageSize, Invalid value for: query parameter pageToken */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Parties: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AllocatePartyRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AllocatePartyResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2PartiesExternalAllocate: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['AllocateExternalPartyRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['AllocateExternalPartyResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2PartiesParticipant-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetParticipantIdResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2PartiesParty: { + parameters: { + query?: { + 'identity-provider-id'?: string + parties?: string[] + } + header?: never + path: { + party: string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetPartiesResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter identity-provider-id, Invalid value for: query parameter parties */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + patchV2PartiesParty: { + parameters: { + query?: never + header?: never + path: { + party: string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdatePartyDetailsRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdatePartyDetailsResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2PartiesExternalGenerate-topology': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GenerateExternalPartyTopologyRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GenerateExternalPartyTopologyResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2StateActive-contracts': { + parameters: { + query?: { + /** @description maximum number of elements to return, this param is ignored if is bigger than server setting */ + limit?: number + /** @description timeout to complete and send result if no new elements are received (for open ended streams) */ + stream_idle_timeout_ms?: number + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetActiveContractsRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetActiveContractsResponse'][] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2StateConnected-synchronizers': { + parameters: { + query?: { + party?: string + participantId?: string + identityProviderId?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetConnectedSynchronizersResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter party, Invalid value for: query parameter participantId, Invalid value for: query parameter identityProviderId */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2StateLedger-end': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetLedgerEndResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2StateLatest-pruned-offsets': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetLatestPrunedOffsetsResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Updates: { + parameters: { + query?: { + /** @description maximum number of elements to return, this param is ignored if is bigger than server setting */ + limit?: number + /** @description timeout to complete and send result if no new elements are received (for open ended streams) */ + stream_idle_timeout_ms?: number + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetUpdatesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetUpdatesResponse'][] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2UpdatesFlats: { + parameters: { + query?: { + /** @description maximum number of elements to return, this param is ignored if is bigger than server setting */ + limit?: number + /** @description timeout to complete and send result if no new elements are received (for open ended streams) */ + stream_idle_timeout_ms?: number + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetUpdatesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetUpdatesResponse'][] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2UpdatesTrees: { + parameters: { + query?: { + /** @description maximum number of elements to return, this param is ignored if is bigger than server setting */ + limit?: number + /** @description timeout to complete and send result if no new elements are received (for open ended streams) */ + stream_idle_timeout_ms?: number + } + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetUpdatesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetUpdateTreesResponse'][] + } + } + /** @description Invalid value, Invalid value for: body, Invalid value for: query parameter limit, Invalid value for: query parameter stream_idle_timeout_ms */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2UpdatesTransaction-tree-by-offsetOffset': { + parameters: { + query?: { + parties?: string[] + } + header?: never + path: { + offset: number + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetTransactionTreeResponse'] + } + } + /** @description Invalid value, Invalid value for: path parameter offset, Invalid value for: query parameter parties */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2UpdatesTransaction-by-offset': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetTransactionByOffsetRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetTransactionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2UpdatesUpdate-by-offset': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetUpdateByOffsetRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetUpdateResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2UpdatesTransaction-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetTransactionByIdRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetTransactionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2UpdatesUpdate-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetUpdateByIdRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetUpdateResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2UpdatesTransaction-tree-by-idUpdate-id': { + parameters: { + query?: { + parties?: string[] + } + header?: never + path: { + 'update-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsGetTransactionTreeResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter parties */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2Users: { + parameters: { + query?: { + /** @description maximum number of elements in a returned page */ + pageSize?: number + /** @description token - to continue results from a given page, leave empty to start from the beginning of the list, obtain token from the result of previous page */ + pageToken?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListUsersResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter pageSize, Invalid value for: query parameter pageToken */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Users: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateUserRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CreateUserResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2UsersUser-id': { + parameters: { + query?: { + 'identity-provider-id'?: string + } + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetUserResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter identity-provider-id */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'deleteV2UsersUser-id': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': Record + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'patchV2UsersUser-id': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateUserRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateUserResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2Authenticated-user': { + parameters: { + query?: { + 'identity-provider-id'?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetUserResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter identity-provider-id */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2UsersUser-idRights': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListUserRightsResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2UsersUser-idRights': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GrantUserRightsRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GrantUserRightsResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'patchV2UsersUser-idRights': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['RevokeUserRightsRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['RevokeUserRightsResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'patchV2UsersUser-idIdentity-provider-id': { + parameters: { + query?: never + header?: never + path: { + 'user-id': string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateUserIdentityProviderIdRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateUserIdentityProviderIdResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getV2Idps: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ListIdentityProviderConfigsResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + postV2Idps: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['CreateIdentityProviderConfigRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['CreateIdentityProviderConfigResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2IdpsIdp-id': { + parameters: { + query?: never + header?: never + path: { + 'idp-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetIdentityProviderConfigResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'deleteV2IdpsIdp-id': { + parameters: { + query?: never + header?: never + path: { + 'idp-id': string + } + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['DeleteIdentityProviderConfigResponse'] + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'patchV2IdpsIdp-id': { + parameters: { + query?: never + header?: never + path: { + 'idp-id': string + } + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['UpdateIdentityProviderConfigRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['UpdateIdentityProviderConfigResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Interactive-submissionPrepare': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsPrepareSubmissionRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsPrepareSubmissionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Interactive-submissionExecute': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsExecuteSubmissionRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ExecuteSubmissionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Interactive-submissionExecuteandwait': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsExecuteSubmissionAndWaitRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['ExecuteSubmissionAndWaitResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Interactive-submissionExecuteandwaitfortransaction': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['JsExecuteSubmissionAndWaitForTransactionRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsExecuteSubmissionAndWaitForTransactionResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'getV2Interactive-submissionPreferred-package-version': { + parameters: { + query: { + parties?: string[] + 'package-name': string + vetting_valid_at?: string + 'synchronizer-id'?: string + } + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetPreferredPackageVersionResponse'] + } + } + /** @description Invalid value, Invalid value for: query parameter parties, Invalid value for: query parameter package-name, Invalid value for: query parameter vetting_valid_at, Invalid value for: query parameter synchronizer-id */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2Interactive-submissionPreferred-packages': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetPreferredPackagesRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetPreferredPackagesResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getLivez: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK: service is alive */ + 200: { + headers: { + [name: string]: unknown + } + content?: never + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + getReadyz: { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody?: never + responses: { + /** @description OK: readiness message */ + 200: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + /** @description Invalid value */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } + 'postV2ContractsContract-by-id': { + parameters: { + query?: never + header?: never + path?: never + cookie?: never + } + requestBody: { + content: { + 'application/json': components['schemas']['GetContractRequest'] + } + } + responses: { + 200: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['GetContractResponse'] + } + } + /** @description Invalid value, Invalid value for: body */ + 400: { + headers: { + [name: string]: unknown + } + content: { + 'text/plain': string + } + } + default: { + headers: { + [name: string]: unknown + } + content: { + 'application/json': components['schemas']['JsCantonError'] + } + } + } + } +} diff --git a/ccip-sdk/src/canton/client/index.ts b/ccip-sdk/src/canton/client/index.ts new file mode 100644 index 00000000..262dcd02 --- /dev/null +++ b/ccip-sdk/src/canton/client/index.ts @@ -0,0 +1,25 @@ +/** + * Canton Ledger API - Public exports + */ + +export { + type CantonClient, + type CantonClientConfig, + type Command, + type ConnectedSynchronizer, + type CreatedEvent, + type EventFormat, + type GetActiveContractsRequest, + type GetConnectedSynchronizersResponse, + type JsActiveContract, + type JsCantonError, + type JsCommands, + type JsGetActiveContractsResponse, + type JsSubmitAndWaitForTransactionResponse, + type SubmitAndWaitResponse, + type TemplateFilter, + type TransactionFilter, + type WildcardFilter, + CantonApiError, + createCantonClient, +} from './client.ts' diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts new file mode 100644 index 00000000..06f6c999 --- /dev/null +++ b/ccip-sdk/src/canton/index.ts @@ -0,0 +1,467 @@ +import type { BytesLike } from 'ethers' +import type { PickDeep } from 'type-fest' + +import { type ChainContext, type GetBalanceOpts, type LogFilter, Chain } from '../chain.ts' +import { + CCIPChainNotFoundError, + CCIPError, + CCIPErrorCode, + CCIPNotImplementedError, +} from '../errors/index.ts' +import type { ExtraArgs } from '../extra-args.ts' +import type { LeafHasher } from '../hasher/common.ts' +import { getMessagesInBatch } from '../requests.ts' +import { supportedChains } from '../supported-chains.ts' +import { + type CCIPExecution, + type CCIPMessage, + type CCIPRequest, + type ChainTransaction, + type CommitReport, + type ExecutionReceipt, + type Lane, + type Log_, + type NetworkInfo, + type OffchainTokenData, + type WithLogger, + ChainFamily, +} from '../types.ts' +import { networkInfo } from '../utils.ts' +import { type CantonClient, createCantonClient } from './client/index.ts' + +export type { CantonClient, CantonClientConfig } from './client/index.ts' + +const EDS_API_URL = '' + +/** + * Canton chain implementation supporting Canton Ledger networks. + * + */ +export class CantonChain extends Chain { + static { + supportedChains[ChainFamily.Canton] = CantonChain + } + static readonly family = ChainFamily.Canton + /** Canton uses 10 decimals (lf-coin micro-units) */ + static readonly decimals = 10 + + override readonly network: NetworkInfo + readonly provider: CantonClient + + /** + * Creates a new CantonChain instance. + * @param client - Canton Ledger API client. + * @param network - Network information for this chain. + * @param ctx - Context containing logger. + */ + constructor( + client: CantonClient, + network: NetworkInfo, + ctx?: ChainContext, + ) { + super(network, ctx) + this.provider = client + this.network = network + } + + /** + * Mapping from lower-cased synchronizer alias variants to their canonical Canton chain ID + * as it appears in selectors.ts (e.g. `canton:MainNet`). + */ + private static readonly SYNCHRONIZER_ALIAS_TO_CHAIN_ID: ReadonlyMap = new Map([ + ['localnet', 'canton:LocalNet'], + ['local', 'canton:LocalNet'], + ['canton-localnet', 'canton:LocalNet'], + ['devnet', 'canton:DevNet'], + ['dev', 'canton:DevNet'], + ['canton-devnet', 'canton:DevNet'], + ['testnet', 'canton:TestNet'], + ['test', 'canton:TestNet'], + ['canton-testnet', 'canton:TestNet'], + ['mainnet', 'canton:MainNet'], + ['main', 'canton:MainNet'], + ['canton-mainnet', 'canton:MainNet'], + ]) + + /** + * Detect the Canton network and instantiate a CantonChain. + * + * Network detection works by querying the connected synchronizers via + * `/v2/state/connected-synchronizers` and matching the `synchronizerAlias` of the + * first recognised synchronizer against the known Canton chain names. + * + * @throws {@link CCIPChainNotFoundError} if no connected synchronizer alias maps to a known Canton chain + */ + static async fromClient(client: CantonClient, ctx?: ChainContext): Promise { + const synchronizers = await client.getConnectedSynchronizers() + + // TODO: Check synchronizer returned aliases against known Canton chain names to determine the network. + for (const { synchronizerAlias } of synchronizers) { + const chainId = CantonChain.SYNCHRONIZER_ALIAS_TO_CHAIN_ID.get( + synchronizerAlias.toLowerCase(), + ) + if (chainId) { + return new CantonChain( + client, + networkInfo(chainId) as NetworkInfo, + ctx, + ) + } + } + + throw new CCIPChainNotFoundError( + synchronizers.length + ? `canton:${synchronizers.map((s) => s.synchronizerAlias).join(', ')}` + : 'no connected synchronizers', + ) + } + + /** + * Creates a CantonChain instance from a Canton Ledger JSON API URL. + * Verifies the connection and detects the network. + * + * @param url - Base URL for the Canton Ledger JSON API (e.g., http://localhost:7575). + * @param ctx - Context containing logger. + * @returns A new CantonChain instance. + * @throws {@link CCIPHttpError} if connection to the Canton Ledger JSON API fails + * @throws {@link CCIPNotImplementedError} if Canton network detection is not yet implemented + */ + static async fromUrl(url: string, ctx?: ChainContext): Promise { + const client = createCantonClient({ baseUrl: url }) + try { + const alive = await client.isAlive() + if (!alive) throw new CCIPNotImplementedError('Canton Ledger JSON API is not alive') + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + `Failed to connect to Canton Ledger API ${url}: ${message}`, + ) + } + return CantonChain.fromClient(client, ctx) + } + + /** + * {@inheritDoc Chain.getBlockTimestamp} + * @throws {@link CCIPNotImplementedError} for numeric blocks (Canton ledger uses offsets, not block numbers) + */ + async getBlockTimestamp(block: number | 'finalized'): Promise { + if (typeof block !== 'number') { + // For 'finalized', return current time as best approximation + return Math.floor(Date.now() / 1000) + } + // Canton ledger uses offset-based ordering, not block timestamps + throw new CCIPNotImplementedError( + `CantonChain.getBlockTimestamp: block ${block} — Canton uses ledger offsets, not block numbers`, + ) + } + + /** + * {@inheritDoc Chain.getTransaction} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTransaction(_hash: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getTransaction') + } + + /** + * {@inheritDoc Chain.getLogs} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + // eslint-disable-next-line require-yield + async *getLogs(_opts: LogFilter): AsyncIterableIterator { + throw new CCIPNotImplementedError('CantonChain.getLogs') + } + + /** + * {@inheritDoc Chain.getMessagesInBatch} + */ + override async getMessagesInBatch< + R extends PickDeep< + CCIPRequest, + 'lane' | `log.${'topics' | 'address' | 'blockNumber'}` | 'message.sequenceNumber' + >, + >( + request: R, + commit: Pick, + opts?: { page?: number }, + ): Promise { + return getMessagesInBatch(this, request, commit, opts) + } + + /** + * {@inheritDoc Chain.typeAndVersion} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async typeAndVersion( + _address: string, + ): Promise<[type: string, version: string, typeAndVersion: string, suffix?: string]> { + throw new CCIPNotImplementedError('CantonChain.typeAndVersion') + } + + /** + * {@inheritDoc Chain.getRouterForOnRamp} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getRouterForOnRamp') + } + + /** + * {@inheritDoc Chain.getRouterForOffRamp} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getRouterForOffRamp') + } + + /** + * {@inheritDoc Chain.getNativeTokenForRouter} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getNativeTokenForRouter(_router: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getNativeTokenForRouter') + } + + /** + * {@inheritDoc Chain.getOffRampsForRouter} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getOffRampsForRouter') + } + + /** + * {@inheritDoc Chain.getOnRampForRouter} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getOnRampForRouter') + } + + /** + * {@inheritDoc Chain.getOnRampForOffRamp} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getOnRampForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getOnRampForOffRamp') + } + + /** + * {@inheritDoc Chain.getCommitStoreForOffRamp} + * + * For Canton (CCIP v1.6+), the OffRamp itself serves as the commit store. + */ + async getCommitStoreForOffRamp(offRamp: string): Promise { + return Promise.resolve(offRamp) + } + + /** + * {@inheritDoc Chain.getTokenForTokenPool} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTokenForTokenPool(_tokenPool: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getTokenForTokenPool') + } + + /** + * {@inheritDoc Chain.getTokenInfo} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { + throw new CCIPNotImplementedError('CantonChain.getTokenInfo') + } + + /** + * {@inheritDoc Chain.getBalance} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getBalance(_opts: GetBalanceOpts): Promise { + throw new CCIPNotImplementedError('CantonChain.getBalance') + } + + /** + * {@inheritDoc Chain.getTokenAdminRegistryFor} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTokenAdminRegistryFor(_address: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getTokenAdminRegistryFor') + } + + /** + * {@inheritDoc Chain.getFee} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getFee(_opts: Parameters[0]): Promise { + throw new CCIPNotImplementedError('CantonChain.getFee') + } + + /** + * {@inheritDoc Chain.generateUnsignedSendMessage} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + override generateUnsignedSendMessage( + _opts: Parameters[0], + ): Promise { + return Promise.reject(new CCIPNotImplementedError('CantonChain.generateUnsignedSendMessage')) + } + + /** + * {@inheritDoc Chain.sendMessage} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async sendMessage(_opts: Parameters[0]): Promise { + throw new CCIPNotImplementedError('CantonChain.sendMessage') + } + + /** + * {@inheritDoc Chain.getOffchainTokenData} + */ + getOffchainTokenData(request: CCIPRequest): Promise { + return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) + } + + /** + * {@inheritDoc Chain.generateUnsignedExecuteReport} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + override generateUnsignedExecuteReport( + _opts: Parameters[0], + ): Promise { + return Promise.reject(new CCIPNotImplementedError('CantonChain.generateUnsignedExecuteReport')) + } + + /** + * {@inheritDoc Chain.executeReport} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async executeReport(_opts: Parameters[0]): Promise { + throw new CCIPNotImplementedError('CantonChain.executeReport') + } + + /** + * {@inheritDoc Chain.getSupportedTokens} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getSupportedTokens(_address: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getSupportedTokens') + } + + /** + * {@inheritDoc Chain.getRegistryTokenConfig} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getRegistryTokenConfig(_registry: string, _token: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getRegistryTokenConfig') + } + + /** + * {@inheritDoc Chain.getTokenPoolConfig} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTokenPoolConfig(_tokenPool: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getTokenPoolConfig') + } + + /** + * {@inheritDoc Chain.getTokenPoolRemotes} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getTokenPoolRemotes(_tokenPool: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getTokenPoolRemotes') + } + + /** + * {@inheritDoc Chain.getFeeTokens} + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + async getFeeTokens(_router: string): Promise { + throw new CCIPNotImplementedError('CantonChain.getFeeTokens') + } + + // ─── Static methods ─────────────────────────────────────────────────────── + + /** + * Try to decode a CCIP message from a Canton log/event. + * @returns undefined (Canton message format not yet supported) + */ + static decodeMessage(_log: Pick): CCIPMessage | undefined { + // TODO: implement Canton message decoding + return undefined + } + + /** + * Try to decode extra args serialized for Canton. + * @returns undefined (Canton extra args format not yet supported) + */ + static decodeExtraArgs(_extraArgs: BytesLike): undefined { + // TODO: implement Canton extra args decoding + return undefined + } + + /** + * Encode extraArgs for Canton. + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + static encodeExtraArgs(_extraArgs: ExtraArgs): string { + throw new CCIPNotImplementedError('CantonChain.encodeExtraArgs') + } + + /** + * Try to decode a commit report from a Canton log. + * @returns undefined (Canton commit format not yet supported) + */ + static decodeCommits(_log: Pick, _lane?: Lane): CommitReport[] | undefined { + // TODO: implement Canton commit report decoding + return undefined + } + + /** + * Try to decode an execution receipt from a Canton log. + * @returns undefined (Canton receipt format not yet supported) + */ + static decodeReceipt(_log: Pick): ExecutionReceipt | undefined { + // TODO: implement Canton execution receipt decoding + return undefined + } + + /** + * Receive bytes and try to decode as a Canton address (Daml party ID or contract ID). + * + * @param bytes - Bytes or string to convert. + * @returns Canton address string. + * @throws {@link CCIPNotImplementedError} if bytes cannot be decoded as a Canton address + */ + static getAddress(bytes: BytesLike): string { + if (typeof bytes === 'string') return bytes + // TODO: implement proper Canton address decoding from bytes + throw new CCIPNotImplementedError('CantonChain.getAddress: bytes-to-address decoding') + } + + /** + * Validates a transaction (update) ID format for Canton. + * Canton update IDs are base64-encoded strings. + */ + static isTxHash(v: unknown): v is string { + if (typeof v !== 'string' || v.length === 0) return false + // Canton update IDs are base64url-encoded strings, typically ~44 chars + return /^[A-Za-z0-9+/=_-]+$/.test(v) + } + + /** + * Gets the leaf hasher for Canton destination chains. + * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + */ + static getDestLeafHasher(_lane: Lane, _ctx?: WithLogger): LeafHasher { + throw new CCIPNotImplementedError('CantonChain.getDestLeafHasher') + } + + /** + * Build a message targeted at this Canton destination chain, populating missing fields. + */ + static override buildMessageForDest( + message: Parameters<(typeof Chain)['buildMessageForDest']>[0], + ) { + return Chain.buildMessageForDest(message) + } +} diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index d90b6642..6eeb87e3 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -440,6 +440,7 @@ export type UnsignedTx = { [ChainFamily.Aptos]: UnsignedAptosTx [ChainFamily.TON]: UnsignedTONTx [ChainFamily.Sui]: UnsignedSuiTx + [ChainFamily.Canton]: never // TODO [ChainFamily.Unknown]: never } diff --git a/ccip-sdk/src/errors/codes.ts b/ccip-sdk/src/errors/codes.ts index 83d85f97..fa73587b 100644 --- a/ccip-sdk/src/errors/codes.ts +++ b/ccip-sdk/src/errors/codes.ts @@ -167,6 +167,9 @@ export const CCIPErrorCode = { // Viem Adapter VIEM_ADAPTER_ERROR: 'VIEM_ADAPTER_ERROR', + + // Canton + CANTON_API_ERROR: 'CANTON_API_ERROR', } as const /** Union type of all error codes. */ diff --git a/ccip-sdk/src/selectors.ts b/ccip-sdk/src/selectors.ts index bda3f611..86fa14ce 100644 --- a/ccip-sdk/src/selectors.ts +++ b/ccip-sdk/src/selectors.ts @@ -1742,6 +1742,41 @@ const selectors: Selectors = { family: 'TON', }, // end:generate + + // generate: + // fetch('https://github.com/smartcontractkit/chain-selectors/raw/main/selectors_canton.yml') + // .then((res) => res.text()) + // .then((body) => require('yaml').parse(body, { intAsBigInt: true }).selectors) + // .then((obj) => Object.fromEntries(Object.entries(obj).map(([k, v]) => { + // if (!v.network_type) throw new Error(`Missing network_type for Canton chain ${k}`) + // return [`canton:${k}`, { ...v, family: 'CANTON', network_type: v.network_type.toUpperCase() }] + // }))) + // .then((obj) => [...require('util').inspect(obj).split('\n').slice(1, -1), ',']) + 'canton:LocalNet': { + selector: 8706591216959472610n, + name: 'canton-localnet', + network_type: 'TESTNET', + family: 'CANTON', + }, + 'canton:DevNet': { + selector: 10109143320554840099n, + name: 'canton-devnet', + network_type: 'TESTNET', + family: 'CANTON', + }, + 'canton:TestNet': { + selector: 9268731218649498074n, + name: 'canton-testnet', + network_type: 'TESTNET', + family: 'CANTON', + }, + 'canton:MainNet': { + selector: 2308837218439511688n, + name: 'canton-mainnet', + network_type: 'MAINNET', + family: 'CANTON', + }, + // end:generate } export default selectors diff --git a/ccip-sdk/src/types.ts b/ccip-sdk/src/types.ts index b33025f2..14a954e8 100644 --- a/ccip-sdk/src/types.ts +++ b/ccip-sdk/src/types.ts @@ -66,6 +66,7 @@ export const ChainFamily = { Aptos: 'APTOS', Sui: 'SUI', TON: 'TON', + Canton: 'CANTON', Unknown: 'UNKNOWN', } as const /** Type representing one of the supported chain families. */ @@ -98,7 +99,7 @@ type ChainFamilyWithId = F extends | typeof ChainFamily.EVM | typeof ChainFamily.TON ? { readonly family: F; readonly chainId: number } - : F extends typeof ChainFamily.Solana + : F extends typeof ChainFamily.Solana | typeof ChainFamily.Canton ? { readonly family: F; readonly chainId: string } : F extends typeof ChainFamily.Aptos | typeof ChainFamily.Sui ? { readonly family: F; readonly chainId: `${Lowercase}:${number}` } diff --git a/package-lock.json b/package-lock.json index 213a9326..3910b3fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,26 +63,33 @@ } }, "ccip-api-ref/node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } }, "ccip-api-ref/node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.3" } }, + "ccip-api-ref/node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, "ccip-cli": { "name": "@chainlink/ccip-cli", "version": "1.3.1", @@ -171,7 +178,8 @@ "ethers": "6.16.0", "got": "^11.8.6", "micro-memoize": "^5.1.1", - "type-fest": "^5.5.0", + "openapi-fetch": "^0.17.0", + "type-fest": "^5.4.4", "yaml": "2.8.2" }, "devDependencies": { @@ -591,6 +599,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2536,6 +2545,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -2558,6 +2568,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2667,6 +2678,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3088,6 +3100,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4234,6 +4247,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4530,6 +4544,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4675,6 +4690,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/types": "3.9.2", @@ -4720,6 +4736,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", "license": "MIT", + "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/utils": "3.9.2", @@ -7554,6 +7571,7 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", + "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -9230,6 +9248,7 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -9446,6 +9465,7 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -9593,6 +9613,7 @@ "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.63.0.tgz", "integrity": "sha512-uBc0WQNYVzjAwPvIazf0Ryhpv4nJd4dKIuHoj766gUdwe8sVzGM+TxKKKJETL70hh/mxACyUlR4tAwN0LWDNow==", "license": "MIT", + "peer": true, "peerDependencies": { "@ton/crypto": ">=3.2.0" } @@ -10155,6 +10176,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -10182,6 +10204,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -10579,6 +10602,7 @@ "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", @@ -11654,6 +11678,7 @@ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -11697,6 +11722,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11791,6 +11817,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11855,6 +11882,7 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.2.tgz", "integrity": "sha512-1K0wtDaRONwfhL4h8bbJ9qTjmY6rhGgRvvagXkMBsAOMNr+3Q2SffHECh9DIuNVrMA1JwA0zCwhyepgBZVakng==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/abtesting": "1.15.2", "@algolia/client-abtesting": "5.49.2", @@ -12762,6 +12790,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -14064,6 +14093,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14383,6 +14413,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -14804,6 +14835,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -17811,6 +17843,7 @@ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "license": "MIT", + "peer": true, "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -17870,6 +17903,7 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", "license": "MIT", + "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -23498,6 +23532,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-fetch": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", + "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + } + }, "node_modules/openapi-to-postmanv2": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.8.0.tgz", @@ -23534,6 +23577,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -23574,6 +23618,12 @@ "node": ">= 6" } }, + "node_modules/openapi-typescript-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", + "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", + "license": "MIT" + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", @@ -24480,6 +24530,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -25485,6 +25536,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -26153,6 +26205,7 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -26661,6 +26714,10 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, "engines": { "node": ">=0.10.0" } @@ -26670,6 +26727,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -26712,6 +26770,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -26772,6 +26831,7 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", + "peer": true, "dependencies": { "@types/react": "*" }, @@ -26849,6 +26909,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -26872,6 +26933,7 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -27035,7 +27097,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -27846,6 +27909,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -27974,6 +28038,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29309,6 +29374,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -29698,6 +29764,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -29864,10 +29931,11 @@ } }, "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.21.0", @@ -30093,6 +30161,7 @@ "integrity": "sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 18" }, @@ -30121,6 +30190,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -30581,6 +30651,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -31209,6 +31280,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -31951,6 +32023,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, From 299c8b117b0e65a613524738b93c2bee1d2f2a8c Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 24 Feb 2026 19:22:16 +0100 Subject: [PATCH 02/17] add explicit disclosures discovery clean comments minor refactor --- .../src/canton/explicit-disclosures/acs.ts | 325 ++++++++++++++++++ .../src/canton/explicit-disclosures/eds.ts | 229 ++++++++++++ .../src/canton/explicit-disclosures/index.ts | 25 ++ .../src/canton/explicit-disclosures/types.ts | 174 ++++++++++ ccip-sdk/src/canton/index.ts | 14 +- 5 files changed, 764 insertions(+), 3 deletions(-) create mode 100644 ccip-sdk/src/canton/explicit-disclosures/acs.ts create mode 100644 ccip-sdk/src/canton/explicit-disclosures/eds.ts create mode 100644 ccip-sdk/src/canton/explicit-disclosures/index.ts create mode 100644 ccip-sdk/src/canton/explicit-disclosures/types.ts diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts new file mode 100644 index 00000000..fa7283cd --- /dev/null +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -0,0 +1,325 @@ +import { keccak256, toUtf8Bytes } from 'ethers' + +import type { + AcsDisclosureConfig, + DisclosedContract, + DisclosureProvider, + ExecutionDisclosures, + SendDisclosures, +} from './types.ts' +import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' +import { + type CantonClient, + type EventFormat, + type GetActiveContractsRequest, + createCantonClient, +} from '../client/index.ts' + +/** + * Compute the Canton `InstanceAddress` from a contract's `instanceId` and its + * signatory party. Matches Go: `instanceID.RawInstanceAddress(party).InstanceAddress()`. + * + * Format: `keccak256(utf8("@"))` + */ +function computeInstanceAddress(instanceId: string, signatory: string): string { + return keccak256(toUtf8Bytes(`${instanceId}@${signatory}`)) +} + +function instanceAddressEquals(a: string, b: string): boolean { + return a.toLowerCase().replace(/^0x/, '') === b.toLowerCase().replace(/^0x/, '') +} + +/** + * Extract the `instanceId` string from a contract's `createArgument` object. + * Handles both verbose mode (direct field) and structured fields arrays. + */ +function extractInstanceId(createArgument: unknown): string | null { + if (!createArgument || typeof createArgument !== 'object') return null + const arg = createArgument as Record + + if ('instanceId' in arg && typeof arg['instanceId'] === 'string') { + return arg['instanceId'] + } + + if ('fields' in arg && Array.isArray(arg['fields'])) { + for (const field of arg['fields'] as Array>) { + if (field['label'] === 'instanceId') { + const val = field['value'] + if (typeof val === 'string') return val + if (val && typeof val === 'object' && 'text' in val) { + return (val as Record)['text'] as string + } + } + } + } + return null +} + +/** + * Build a wildcard `EventFormat` that returns all contracts belonging to `party`, + * including the opaque `createdEventBlob` needed for disclosure. + */ +function buildWildcardEventFormat(party: string): EventFormat { + return { + filtersByParty: { + [party]: { + cumulative: [ + { + identifierFilter: { + WildcardFilter: { + value: { includeCreatedEventBlob: true }, + }, + }, + }, + ], + }, + }, + verbose: true, + } +} + +/** + * The `ModuleName:EntityName` suffix used to identify each CCIP contract type + * without requiring a specific package ID. + */ +const CCIP_MODULE_ENTITIES = { + offRamp: 'CCIP.OffRamp:OffRamp', + globalConfig: 'CCIP.GlobalConfig:GlobalConfig', + tokenAdminRegistry: 'CCIP.TokenAdminRegistry:TokenAdminRegistry', + rmnRemote: 'CCIP.RMNRemote:RMNRemote', + committeeVerifier: 'CCIP.CommitteeVerifier:CommitteeVerifier', + perPartyRouter: 'CCIP.PerPartyRouter:PerPartyRouter', + onRamp: 'CCIP.OnRamp:OnRamp', + feeQuoter: 'CCIP.FeeQuoter:FeeQuoter', +} as const + +type CcipContractType = keyof typeof CCIP_MODULE_ENTITIES + +/** + * Internal per-contract entry in the ACS snapshot, enriched with instance + * address components for later matching. + */ +interface RichContractMatch { + contractId: string + templateId: string + createdEventBlob: string + synchronizerId: string + instanceId: string | null + signatory: string | null +} + +/** + * Query the ACS once with a wildcard filter and build a lookup map keyed by + * `"ModuleName:EntityName"`, preserving all fields needed for instance-address + * matching. + */ +async function fetchRichSnapshot( + client: CantonClient, + party: string, +): Promise> { + const { offset } = await client.getLedgerEnd() + const request: GetActiveContractsRequest = { + eventFormat: buildWildcardEventFormat(party), + verbose: false, + activeAtOffset: offset, + } + + const responses = await client.getActiveContracts(request) + const byModuleEntity = new Map() + + for (const response of responses) { + const entry = response.contractEntry + if (!entry || !('JsActiveContract' in entry)) continue + + const active = entry.JsActiveContract + const created = active.createdEvent + const parts = created.templateId.split(':') + if (parts.length < 3) continue + const moduleEntity = `${parts[1]}:${parts[2]}` + + const signatories = created.signatories + const rich: RichContractMatch = { + contractId: created.contractId, + templateId: created.templateId, + createdEventBlob: created.createdEventBlob ?? '', + synchronizerId: active.synchronizerId, + instanceId: extractInstanceId(created.createArgument), + signatory: signatories.length === 1 ? (signatories[0] ?? null) : null, + } + + const list = byModuleEntity.get(moduleEntity) ?? [] + list.push(rich) + byModuleEntity.set(moduleEntity, list) + } + + return byModuleEntity +} + +/** + * From a pre-fetched ACS snapshot, find the single contract matching the given + * contract type and instance address. + * + * @throws `CCIPError(CANTON_API_ERROR)` if no matching contract is found. + */ +function pickByInstanceAddress( + snapshot: Map, + contractType: CcipContractType, + targetInstanceAddress: string, +): DisclosedContract { + const moduleEntity = CCIP_MODULE_ENTITIES[contractType] + const candidates = snapshot.get(moduleEntity) ?? [] + + for (const c of candidates) { + if (!c.instanceId || !c.signatory) continue + if ( + instanceAddressEquals( + computeInstanceAddress(c.instanceId, c.signatory), + targetInstanceAddress, + ) + ) { + return { + templateId: c.templateId, + contractId: c.contractId, + createdEventBlob: c.createdEventBlob, + synchronizerId: c.synchronizerId, + } + } + } + + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton ACS: no active "${moduleEntity}" contract found at instance address ` + + `${targetInstanceAddress}. Verify the address is correct and the contract is ` + + `active for the configured party.`, + ) +} + +// --------------------------------------------------------------------------- +// AcsDisclosureProvider +// --------------------------------------------------------------------------- + +/** + * Disclosure provider that fetches `createdEventBlob`s directly from the Canton + * Ledger API Active Contract Set. + * + * A single wildcard ACS query is issued per `fetchExecutionDisclosures()` / + * `fetchSendDisclosures()` call (package-ID agnostic, matches the EDS strategy). + * + * @example + * ```ts + * const provider = new AcsDisclosureProvider(cantonClient, { + * jwt: '...', + * party: 'Alice::122...', + * instanceAddresses: { + * offRampAddress: '0xabc...', + * globalConfigAddress: '0xdef...', + * tokenAdminRegistryAddress: '0x123...', + * rmnRemoteAddress: '0x456...', + * perPartyRouterFactoryAddress: '0x789...', + * ccvAddresses: ['0xaaa...'], + * }, + * }) + * const disclosures = await provider.fetchExecutionDisclosures() + * ``` + * + * Use this provider when: + * - Direct Ledger API access is available. + * - A running EDS instance is not needed (local dev, integration tests). + * - Multiple CCVs or GlobalConfig / RMNRemote disclosures are required. + */ +export class AcsDisclosureProvider implements DisclosureProvider { + private readonly client: CantonClient + private readonly config: AcsDisclosureConfig + + /** + * Create an `AcsDisclosureProvider` from a pre-built Canton Ledger API client. + * + * @param client - Authenticated Canton Ledger API client (JWT already embedded). + * @param config - ACS provider configuration: party ID and contract instance addresses. + */ + constructor(client: CantonClient, config: AcsDisclosureConfig) { + this.client = client + this.config = config + } + + /** + * Convenience factory: create a provider directly from a Ledger API URL. + */ + static fromUrl( + ledgerApiUrl: string, + jwt: string, + config: AcsDisclosureConfig, + ): AcsDisclosureProvider { + const client = createCantonClient({ baseUrl: ledgerApiUrl, token: jwt }) + return new AcsDisclosureProvider(client, config) + } + + /** + * Fetch all contracts that must be disclosed for a `ccipExecute` command. + * + * Issues a single wildcard ACS query, then resolves OffRamp, GlobalConfig, + * TokenAdminRegistry, RMNRemote, and all CCVs by instance address. + */ + async fetchExecutionDisclosures(extraCcvAddresses: string[] = []): Promise { + const { party, instanceAddresses, additionalCcvAddresses = [] } = this.config + const snapshot = await fetchRichSnapshot(this.client, party) + + const offRamp = pickByInstanceAddress(snapshot, 'offRamp', instanceAddresses.offRampAddress) + const globalConfig = pickByInstanceAddress( + snapshot, + 'globalConfig', + instanceAddresses.globalConfigAddress, + ) + const tokenAdminRegistry = pickByInstanceAddress( + snapshot, + 'tokenAdminRegistry', + instanceAddresses.tokenAdminRegistryAddress, + ) + const rmnRemote = pickByInstanceAddress( + snapshot, + 'rmnRemote', + instanceAddresses.rmnRemoteAddress, + ) + + const allCcvAddresses = [ + ...new Set([ + ...instanceAddresses.ccvAddresses, + ...additionalCcvAddresses, + ...extraCcvAddresses, + ]), + ] + + const verifiers: DisclosedContract[] = allCcvAddresses.map((addr) => + pickByInstanceAddress(snapshot, 'committeeVerifier', addr), + ) + + return { offRamp, globalConfig, tokenAdminRegistry, rmnRemote, verifiers } + } + + /** + * Fetch all contracts that must be disclosed for a `ccipSend` command. + * + * Requires `routerAddress`, `onRampAddress`, and `feeQuoterAddress` to be + * provided in `instanceAddresses`. + */ + async fetchSendDisclosures(): Promise { + const { party, instanceAddresses } = this.config + const { routerAddress, onRampAddress, feeQuoterAddress } = instanceAddresses + + if (!routerAddress || !onRampAddress || !feeQuoterAddress) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + 'Canton ACS: fetchSendDisclosures requires routerAddress, onRampAddress, and ' + + 'feeQuoterAddress to be set in the instanceAddresses configuration.', + ) + } + + const snapshot = await fetchRichSnapshot(this.client, party) + + const router = pickByInstanceAddress(snapshot, 'perPartyRouter', routerAddress) + const onRamp = pickByInstanceAddress(snapshot, 'onRamp', onRampAddress) + const feeQuoter = pickByInstanceAddress(snapshot, 'feeQuoter', feeQuoterAddress) + + return { router, onRamp, feeQuoter } + } +} diff --git a/ccip-sdk/src/canton/explicit-disclosures/eds.ts b/ccip-sdk/src/canton/explicit-disclosures/eds.ts new file mode 100644 index 00000000..aff34d6c --- /dev/null +++ b/ccip-sdk/src/canton/explicit-disclosures/eds.ts @@ -0,0 +1,229 @@ +import type { + DisclosedContract, + DisclosureProvider, + EdsDisclosureConfig, + ExecutionDisclosures, + SendDisclosures, +} from './types.ts' +import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' + +/** Structured Daml template identifier as returned by the EDS. */ +interface EdsTemplateID { + packageId: string + moduleName: string + entityName: string +} + +/** + * A single contract as returned by the EDS. + * `createdEventBlob` is base64-encoded. + */ +interface EdsDisclosedContract { + contractId: string + instanceId: string + templateId: EdsTemplateID + createdEventBlob: string + synchronizerId?: string +} + +// TODO: Add EdsCCIPExecuteDisclosures interface here once the EDS execute endpoint +// returns globalConfig and rmnRemote fields. + +/** EDS `/disclosures/send` response body. */ +interface EdsCCIPSendDisclosures { + environmentId: string + contracts: { + router: EdsDisclosedContract | null + onRamp: EdsDisclosedContract | null + feeQuoter: EdsDisclosedContract | null + } +} + +/** EDS `/health` response body. */ +interface EdsHealthResponse { + status: string + ledgerApiConnected: boolean + environments: string[] +} + +/** EDS error response body. */ +interface EdsErrorResponse { + error: string + code?: string + details?: string +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Convert an EDS `DisclosedContract` to the SDK's internal `DisclosedContract`. + * The EDS uses a structured `templateId`; the SDK uses a flat colon-delimited string. + */ +function edsContractToSdk(c: EdsDisclosedContract): DisclosedContract { + return { + templateId: `${c.templateId.packageId}:${c.templateId.moduleName}:${c.templateId.entityName}`, + contractId: c.contractId, + createdEventBlob: c.createdEventBlob, + synchronizerId: c.synchronizerId, + } +} + +async function edsGet(url: string, timeoutMs: number): Promise { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + let response: Response + try { + response = await fetch(url, { signal: controller.signal }) + } catch (err) { + const msg = err instanceof Error ? err.message : String(err) + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS request failed for ${url}: ${msg}. Ensure the EDS is running and reachable.`, + { cause: err instanceof Error ? err : undefined }, + ) + } finally { + clearTimeout(timer) + } + + if (!response.ok) { + let detail = '' + try { + const body = (await response.json()) as EdsErrorResponse + detail = ` [${body.code ?? response.status}] ${body.error}` + } catch { + detail = ` HTTP ${response.status}` + } + throw new CCIPError(CCIPErrorCode.CANTON_API_ERROR, `EDS${detail} — URL: ${url}`) + } + + return response.json() as Promise +} + +// --------------------------------------------------------------------------- +// EdsDisclosureProvider +// --------------------------------------------------------------------------- + +/** + * Disclosure provider that fetches `createdEventBlob`s from a running EDS instance. + * + * This is the recommended approach for deployments where CCIP contracts are owned + * by a different party than the message executor — the EDS holds credentials + * allowing it to read those contracts on the user's behalf. + * + * @example + * ```ts + * const provider = new EdsDisclosureProvider({ + * edsBaseUrl: 'http://eds.internal:8090', + * environmentId: 'testnet', + * }) + * + * // Optional: verify connectivity before use + * await provider.checkHealth() + * + * const disclosures = await provider.fetchSendDisclosures() + * ``` + * + * @remarks + * **Placeholder**: `fetchExecutionDisclosures()` currently throws because the EDS + * execute endpoint does not yet return `globalConfig` or `rmnRemote`. Use + * `AcsDisclosureProvider` for execution until the EDS is extended. + */ +export class EdsDisclosureProvider implements DisclosureProvider { + private readonly edsBaseUrl: string + private readonly environmentId: string + private readonly timeoutMs: number + + /** + * Create an `EdsDisclosureProvider` from an EDS connection configuration. + * + * @param config - EDS connection configuration. + */ + constructor(config: EdsDisclosureConfig) { + this.edsBaseUrl = config.edsBaseUrl.replace(/\/$/, '') + this.environmentId = config.environmentId + this.timeoutMs = config.timeoutMs ?? 10_000 + } + + /** + * Verify that the EDS is reachable and has the configured environment loaded. + * + * @throws `CCIPError(CANTON_API_ERROR)` if the EDS is unreachable or the + * environment is not listed in the health response. + */ + async checkHealth(): Promise { + const url = `${this.edsBaseUrl}/api/v1/health` + const health = await edsGet(url, this.timeoutMs) + + if (health.status !== 'healthy') { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS reports unhealthy status: "${health.status}"`, + ) + } + + if (!health.environments.includes(this.environmentId)) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS does not have environment "${this.environmentId}" loaded. ` + + `Available: ${health.environments.join(', ') || '(none)'}`, + ) + } + } + + /** + * Fetch all contracts that must be disclosed for a `ccipExecute` command. + * + * @throws Always — the EDS execute endpoint does not yet return `globalConfig` + * or `rmnRemote`. This method is a placeholder pending EDS extension. + * Use `AcsDisclosureProvider` for execution. + */ + fetchExecutionDisclosures(_extraCcvAddresses: string[] = []): Promise { + // TODO: Remove this throw and uncomment mapExecuteDisclosures() once the EDS + // CCIPExecuteContracts struct is extended with globalConfig and rmnRemote fields. + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + 'EdsDisclosureProvider.fetchExecutionDisclosures is not yet available: the EDS execute ' + + 'endpoint does not return "globalConfig" or "rmnRemote". Use AcsDisclosureProvider.', + ) + } + + /** + * Fetch all contracts that must be disclosed for a `ccipSend` command. + * + * Calls `GET /api/v1/ccip/ENVIRONMENT_ID/disclosures/send`. + */ + async fetchSendDisclosures(): Promise { + const url = `${this.edsBaseUrl}/api/v1/ccip/${encodeURIComponent(this.environmentId)}/disclosures/send` + const eds = await edsGet(url, this.timeoutMs) + return this.mapSendDisclosures(eds) + } + + /** Map the EDS send response to the SDK `SendDisclosures` shape. */ + private mapSendDisclosures(eds: EdsCCIPSendDisclosures): SendDisclosures { + const { router, onRamp, feeQuoter } = eds.contracts + + if (!router) this.missingContract('router', 'send') + if (!onRamp) this.missingContract('onRamp', 'send') + if (!feeQuoter) this.missingContract('feeQuoter', 'send') + + // TypeScript narrows router/onRamp/feeQuoter to non-null after the missingContract() guards above + // because missingContract() returns `never` — execution only reaches here when all three are set. + return { + router: edsContractToSdk(router), + onRamp: edsContractToSdk(onRamp), + feeQuoter: edsContractToSdk(feeQuoter), + } + } + + /** Throw a descriptive error for a missing contract in an EDS response. */ + private missingContract(field: string, operation: string): never { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS returned null for "${field}" in the ${operation} disclosures response ` + + `(environment "${this.environmentId}"). ` + + `Verify the EDS environments.yaml configuration contains a "${field}" instance ID.`, + ) + } +} diff --git a/ccip-sdk/src/canton/explicit-disclosures/index.ts b/ccip-sdk/src/canton/explicit-disclosures/index.ts new file mode 100644 index 00000000..073a31d5 --- /dev/null +++ b/ccip-sdk/src/canton/explicit-disclosures/index.ts @@ -0,0 +1,25 @@ +/** + * Canton Explicit Disclosures — public exports + * + * Provides two `DisclosureProvider` implementations: + * - `AcsDisclosureProvider` direct Canton Ledger API ACS queries (package-ID agnostic) + * - `EdsDisclosureProvider` Explicit Disclosure Service REST API (placeholder) + * + * Import the provider that matches your deployment: + * ```ts + * import { AcsDisclosureProvider } from '@chainlink/ccip-sdk/canton/explicit-disclosures' + * ``` + */ + +export { AcsDisclosureProvider } from './acs.ts' +export { EdsDisclosureProvider } from './eds.ts' +export type { + AcsDisclosureConfig, + CCIPContractInstanceAddresses, + DisclosedContract, + DisclosureProvider, + EdsDisclosureConfig, + ExecutionDisclosures, + SendDisclosures, +} from './types.ts' +export { executionDisclosuresToArray, sendDisclosuresToArray } from './types.ts' diff --git a/ccip-sdk/src/canton/explicit-disclosures/types.ts b/ccip-sdk/src/canton/explicit-disclosures/types.ts new file mode 100644 index 00000000..52c16bff --- /dev/null +++ b/ccip-sdk/src/canton/explicit-disclosures/types.ts @@ -0,0 +1,174 @@ +/** + * Explicit Disclosures — shared types + * + * Canton requires attaching `createdEventBlob` for every contract referenced by a + * command submission. The types below model the contracts needed for each CCIP + * operation (execute vs. send) and the two strategies for obtaining them: + * + * - AcsDisclosureProvider — queries the Canton Ledger API ACS directly. + * - EdsDisclosureProvider — calls the Explicit Disclosure Service (EDS) REST API. + */ + +// --------------------------------------------------------------------------- +// Core contract descriptor +// --------------------------------------------------------------------------- + +/** + * A single disclosed contract, ready to be embedded in a Canton command submission + * as an element of `JsCommands.disclosedContracts`. + */ +export interface DisclosedContract { + /** Full Daml template ID string, e.g. `":CCIP.OffRamp:OffRamp"` */ + templateId: string + /** Daml contract ID */ + contractId: string + /** Opaque base64/hex blob obtained from the ACS `createdEvent.createdEventBlob` field */ + createdEventBlob: string + /** Synchronizer from which the contract was read (required for multi-synchronizer Canton deployments) */ + synchronizerId?: string +} + +// --------------------------------------------------------------------------- +// Grouped disclosure shapes per CCIP operation +// --------------------------------------------------------------------------- + +/** + * All disclosed contracts required to submit a `ccipExecute` command on Canton. + * Matches the Go `ExecutionDisclosures` struct in `eds.go`. + */ +export interface ExecutionDisclosures { + offRamp: DisclosedContract + globalConfig: DisclosedContract + tokenAdminRegistry: DisclosedContract + rmnRemote: DisclosedContract + /** One entry per CommitteeVerifier (CCV) referenced by the execute command */ + verifiers: DisclosedContract[] +} + +/** + * All disclosed contracts required to submit a `ccipSend` command on Canton. + * Matches the Go `CCIPSendContracts` struct in EDS `types.go`. + */ +export interface SendDisclosures { + router: DisclosedContract + onRamp: DisclosedContract + feeQuoter: DisclosedContract +} + +// --------------------------------------------------------------------------- +// Contract instance addresses (needed by the ACS provider) +// --------------------------------------------------------------------------- + +/** + * Hex-encoded keccak256 instance addresses for all CCIP contracts that must be + * disclosed when executing a CCIP message on Canton. + * + * These are **not** Daml contract IDs — they are derived from the contract's + * `instanceId` field and its signatory party via: + * `keccak256(utf8("@"))` + * + * Required when using `AcsDisclosureProvider`. Not needed for `EdsDisclosureProvider`. + */ +export interface CCIPContractInstanceAddresses { + offRampAddress: string + globalConfigAddress: string + tokenAdminRegistryAddress: string + rmnRemoteAddress: string + perPartyRouterFactoryAddress: string + /** An address for each CommitteeVerifier (CCV) that will be referenced in execution */ + ccvAddresses: string[] + /** PerPartyRouter instance address (send path) */ + routerAddress?: string + /** OnRamp instance address (send path) */ + onRampAddress?: string + /** FeeQuoter instance address (send path) */ + feeQuoterAddress?: string +} + +// --------------------------------------------------------------------------- +// Provider configuration +// --------------------------------------------------------------------------- + +/** + * Configuration for the ACS-based disclosure provider. + * Requires direct access to the Canton Ledger API and the full set of contract + * instance addresses. + */ +export interface AcsDisclosureConfig { + /** Canton party ID acting on behalf of the user */ + party: string + /** Hex-encoded instance addresses for all CCIP contracts */ + instanceAddresses: CCIPContractInstanceAddresses + /** Additional CCV instance addresses to merge in at call time */ + additionalCcvAddresses?: string[] +} + +/** + * Configuration for the EDS-based disclosure provider. + * Requires only the EDS base URL and environment ID — no direct ledger access needed. + */ +export interface EdsDisclosureConfig { + /** Base URL of the running EDS instance, e.g. `http://eds-host:8090` */ + edsBaseUrl: string + /** + * Environment identifier as configured in the EDS `environments.yaml`, + * e.g. `"localnet"`, `"testnet"`, `"mainnet"`. + */ + environmentId: string + /** Optional request timeout in milliseconds (default: 10_000) */ + timeoutMs?: number +} + +// --------------------------------------------------------------------------- +// Provider interface +// --------------------------------------------------------------------------- + +/** + * Implemented by both `AcsDisclosureProvider` and `EdsDisclosureProvider`. + * The SDK calls these methods internally before building a Canton command — the + * user never needs to interact with this interface directly. + * + * An optional `additionalDisclosures` callback can be set on the provider to + * merge extra contracts (e.g. a user-deployed `CCIPReceiver`) into every command. + */ +export interface DisclosureProvider { + /** + * Fetch all contracts that must be disclosed for a `ccipExecute` command. + * @param extraCcvAddresses - Optional extra CCV instance addresses to resolve + * on top of those already known to the provider. + */ + fetchExecutionDisclosures(extraCcvAddresses?: string[]): Promise + + /** + * Fetch all contracts that must be disclosed for a `ccipSend` command. + */ + fetchSendDisclosures(): Promise +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Flatten an `ExecutionDisclosures` object into the flat array expected by + * `JsCommands.disclosedContracts`. + */ +export function executionDisclosuresToArray( + disclosures: ExecutionDisclosures, +): DisclosedContract[] { + return [ + disclosures.offRamp, + disclosures.globalConfig, + disclosures.tokenAdminRegistry, + disclosures.rmnRemote, + ...disclosures.verifiers, + ] +} + +/** + * Flatten a `SendDisclosures` object into the flat array expected by + * `JsCommands.disclosedContracts`. + */ +export function sendDisclosuresToArray(disclosures: SendDisclosures): DisclosedContract[] { + return [disclosures.router, disclosures.onRamp, disclosures.feeQuoter] +} diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 06f6c999..b243c391 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -28,11 +28,10 @@ import { } from '../types.ts' import { networkInfo } from '../utils.ts' import { type CantonClient, createCantonClient } from './client/index.ts' +import type { DisclosureProvider } from './explicit-disclosures/types.ts' export type { CantonClient, CantonClientConfig } from './client/index.ts' -const EDS_API_URL = '' - /** * Canton chain implementation supporting Canton Ledger networks. * @@ -47,21 +46,25 @@ export class CantonChain extends Chain { override readonly network: NetworkInfo readonly provider: CantonClient + readonly disclosureProvider: DisclosureProvider /** * Creates a new CantonChain instance. * @param client - Canton Ledger API client. + * @param disclosureProvider - Provider used for explicit disclosures. * @param network - Network information for this chain. * @param ctx - Context containing logger. */ constructor( client: CantonClient, + disclosureProvider: DisclosureProvider, network: NetworkInfo, ctx?: ChainContext, ) { super(network, ctx) this.provider = client this.network = network + this.disclosureProvider = disclosureProvider } /** @@ -92,7 +95,11 @@ export class CantonChain extends Chain { * * @throws {@link CCIPChainNotFoundError} if no connected synchronizer alias maps to a known Canton chain */ - static async fromClient(client: CantonClient, ctx?: ChainContext): Promise { + static async fromClient( + client: CantonClient, + disclosureProvider: DisclosureProvider, + ctx?: ChainContext, + ): Promise { const synchronizers = await client.getConnectedSynchronizers() // TODO: Check synchronizer returned aliases against known Canton chain names to determine the network. @@ -103,6 +110,7 @@ export class CantonChain extends Chain { if (chainId) { return new CantonChain( client, + disclosureProvider, networkInfo(chainId) as NetworkInfo, ctx, ) From 6db5ed4a5589b5a190110d86a5aee8074bed8595 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:17:36 +0100 Subject: [PATCH 03/17] fix acs provider. first impl of eds refactor eds remove open api client cleanup comment --- ccip-sdk/src/canton/client/client.ts | 265 +++++++++++------- ccip-sdk/src/canton/client/index.ts | 1 - .../src/canton/explicit-disclosures/acs.ts | 182 ++++-------- .../src/canton/explicit-disclosures/eds.ts | 251 ++++++++--------- .../src/canton/explicit-disclosures/index.ts | 25 +- .../src/canton/explicit-disclosures/types.ts | 161 ----------- ccip-sdk/src/canton/index.ts | 162 +++++++++-- package-lock.json | 16 -- 8 files changed, 455 insertions(+), 608 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index b67bab59..ce39b761 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -1,6 +1,4 @@ -import createClient from 'openapi-fetch' - -import type { components, paths } from './generated/ledger-api.ts' +import type { components } from './generated/ledger-api.ts' import { CCIPError } from '../../errors/CCIPError.ts' import { CCIPErrorCode } from '../../errors/codes.ts' @@ -39,31 +37,23 @@ export interface CantonClientConfig { * Create a typed Canton Ledger API client */ export function createCantonClient(config: CantonClientConfig) { - const client = createClient({ - baseUrl: config.baseUrl, - headers: config.token ? { Authorization: `Bearer ${config.token}` } : undefined, - }) + const baseUrl = config.baseUrl.replace(/\/$/, '') + const headers = buildHeaders(config.token) + const timeoutMs = config.timeout ?? 30_000 return { - /** - * Raw openapi-fetch client for advanced usage - */ - raw: client, - /** * Submit a command and wait for completion * @returns The update ID and completion offset */ async submitAndWait(commands: JsCommands): Promise { - const { data, error } = await client.POST('/v2/commands/submit-and-wait', { - body: commands, - }) - - if (error || !data) { - throw new CantonApiError('submitAndWait failed', error) - } - - return data + return ledgerPost( + baseUrl, + '/v2/commands/submit-and-wait', + headers, + timeoutMs, + commands, + ) }, /** @@ -74,18 +64,13 @@ export function createCantonClient(config: CantonClientConfig) { commands: JsCommands, eventFormat?: EventFormat, ): Promise { - const { data, error } = await client.POST('/v2/commands/submit-and-wait-for-transaction', { - body: { - commands, - eventFormat, - }, - }) - - if (error || !data) { - throw new CantonApiError('submitAndWaitForTransaction failed', error) - } - - return data + return ledgerPost( + baseUrl, + '/v2/commands/submit-and-wait-for-transaction', + headers, + timeoutMs, + { commands, eventFormat }, + ) }, /** @@ -96,30 +81,28 @@ export function createCantonClient(config: CantonClientConfig) { request: GetActiveContractsRequest, options?: { limit?: number }, ): Promise { - const { data, error } = await client.POST('/v2/state/active-contracts', { - body: request, - params: { - query: options?.limit ? { limit: options.limit } : undefined, - }, - }) - - if (error || !data) { - throw new CantonApiError('getActiveContracts failed', error) - } - - return data + const queryParams = + options?.limit !== undefined ? { limit: String(options.limit) } : undefined + return ledgerPost( + baseUrl, + '/v2/state/active-contracts', + headers, + timeoutMs, + request, + queryParams, + ) }, /** * Get the current ledger end offset */ async getLedgerEnd(): Promise<{ offset: number }> { - const { data, error } = await client.GET('/v2/state/ledger-end') - - if (error || !data) { - throw new CantonApiError('getLedgerEnd failed', error) - } - + const data = await ledgerGet<{ offset?: number }>( + baseUrl, + '/v2/state/ledger-end', + headers, + timeoutMs, + ) return { offset: data.offset ?? 0 } }, @@ -127,16 +110,14 @@ export function createCantonClient(config: CantonClientConfig) { * List known parties on the participant */ async listParties(options?: { filterParty?: string }) { - const { data, error } = await client.GET('/v2/parties', { - params: { - query: options?.filterParty ? { 'filter-party': options.filterParty } : undefined, - }, - }) - - if (error || !data) { - throw new CantonApiError('listParties failed', error) - } - + const queryParams = options?.filterParty ? { 'filter-party': options.filterParty } : undefined + const data = await ledgerGet<{ partyDetails?: unknown[] }>( + baseUrl, + '/v2/parties', + headers, + timeoutMs, + queryParams, + ) return data.partyDetails }, @@ -144,12 +125,12 @@ export function createCantonClient(config: CantonClientConfig) { * Get the participant ID */ async getParticipantId(): Promise { - const { data, error } = await client.GET('/v2/parties/participant-id') - - if (error || !data) { - throw new CantonApiError('getParticipantId failed', error) - } - + const data = await ledgerGet<{ participantId?: string }>( + baseUrl, + '/v2/parties/participant-id', + headers, + timeoutMs, + ) return data.participantId ?? '' }, @@ -157,12 +138,12 @@ export function createCantonClient(config: CantonClientConfig) { * Get the list of synchronizers the participant is currently connected to */ async getConnectedSynchronizers(): Promise { - const { data, error } = await client.GET('/v2/state/connected-synchronizers') - - if (error || !data) { - throw new CantonApiError('getConnectedSynchronizers failed', error) - } - + const data = await ledgerGet<{ connectedSynchronizers?: ConnectedSynchronizer[] }>( + baseUrl, + '/v2/state/connected-synchronizers', + headers, + timeoutMs, + ) return data.connectedSynchronizers ?? [] }, @@ -171,8 +152,8 @@ export function createCantonClient(config: CantonClientConfig) { */ async isAlive(): Promise { try { - const { error } = await client.GET('/livez') - return !error + await ledgerRequest('GET', baseUrl, '/livez', headers, timeoutMs) + return true } catch { return false } @@ -183,8 +164,8 @@ export function createCantonClient(config: CantonClientConfig) { */ async isReady(): Promise { try { - const { data, error } = await client.GET('/readyz') - return !error && data !== undefined + await ledgerRequest('GET', baseUrl, '/readyz', headers, timeoutMs) + return true } catch { return false } @@ -197,48 +178,49 @@ export function createCantonClient(config: CantonClientConfig) { * @returns The full update with all events */ async getUpdateById(updateId: string, party: string): Promise { - const { data, error } = await client.POST('/v2/updates/update-by-id', { - body: { - updateId, - updateFormat: { - includeTransactions: { - eventFormat: { - filtersByParty: { - [party]: { - cumulative: [ - { - identifierFilter: { - WildcardFilter: { - value: { - includeCreatedEventBlob: false, - }, + return ledgerPost(baseUrl, '/v2/updates/update-by-id', headers, timeoutMs, { + updateId, + updateFormat: { + includeTransactions: { + eventFormat: { + filtersByParty: { + [party]: { + cumulative: [ + { + identifierFilter: { + WildcardFilter: { + value: { + includeCreatedEventBlob: false, }, }, }, - ], - }, + }, + ], }, - verbose: true, }, - transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS', + verbose: true, }, + transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS', }, }, }) - - if (error || !data) { - throw new CantonApiError('getUpdateById failed', error) - } - - return data }, } } +/** + * Type alias for the Canton client instance + */ +export type CantonClient = ReturnType + +// --------------------------------------------------------------------------- +// Error class +// --------------------------------------------------------------------------- + /** * Custom error class for Canton API errors */ -export class CantonApiError extends CCIPError { +class CantonApiError extends CCIPError { override readonly name = 'CantonApiError' constructor(message: string, error: unknown, statusCode?: number) { @@ -268,7 +250,76 @@ export class CantonApiError extends CCIPError { } } -/** - * Type alias for the Canton client instance - */ -export type CantonClient = ReturnType +function buildHeaders(token?: string): Record { + const headers: Record = { 'Content-Type': 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + return headers +} + +async function parseErrorBody(response: Response): Promise { + try { + return await response.json() + } catch { + return `HTTP ${response.status}` + } +} + +async function ledgerRequest( + method: 'GET' | 'POST', + baseUrl: string, + path: string, + headers: Record, + timeoutMs: number, + options?: { body?: unknown; queryParams?: Record }, +): Promise { + const url = new URL(path, baseUrl) + if (options?.queryParams) { + for (const [key, value] of Object.entries(options.queryParams)) { + url.searchParams.set(key, value) + } + } + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + let response: Response + try { + response = await fetch(url.toString(), { + method, + headers, + body: options?.body !== undefined ? JSON.stringify(options.body) : undefined, + signal: controller.signal, + }) + } catch (err) { + throw new CantonApiError(`${method} ${path} failed`, err) + } finally { + clearTimeout(timer) + } + if (!response.ok) { + throw new CantonApiError( + `${method} ${path} failed`, + await parseErrorBody(response), + response.status, + ) + } + return response.json() as Promise +} + +async function ledgerGet( + baseUrl: string, + path: string, + headers: Record, + timeoutMs: number, + queryParams?: Record, +): Promise { + return ledgerRequest('GET', baseUrl, path, headers, timeoutMs, { queryParams }) +} + +async function ledgerPost( + baseUrl: string, + path: string, + headers: Record, + timeoutMs: number, + body: unknown, + queryParams?: Record, +): Promise { + return ledgerRequest('POST', baseUrl, path, headers, timeoutMs, { body, queryParams }) +} diff --git a/ccip-sdk/src/canton/client/index.ts b/ccip-sdk/src/canton/client/index.ts index 262dcd02..f40be8f1 100644 --- a/ccip-sdk/src/canton/client/index.ts +++ b/ccip-sdk/src/canton/client/index.ts @@ -20,6 +20,5 @@ export { type TemplateFilter, type TransactionFilter, type WildcardFilter, - CantonApiError, createCantonClient, } from './client.ts' diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts index fa7283cd..cdc58c94 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -1,13 +1,5 @@ -import { keccak256, toUtf8Bytes } from 'ethers' - -import type { - AcsDisclosureConfig, - DisclosedContract, - DisclosureProvider, - ExecutionDisclosures, - SendDisclosures, -} from './types.ts' -import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' +import type { DisclosedContract } from './types.ts' +import { CCIPError, CCIPErrorCode, CCIPNotImplementedError } from '../../errors/index.ts' import { type CantonClient, type EventFormat, @@ -15,20 +7,6 @@ import { createCantonClient, } from '../client/index.ts' -/** - * Compute the Canton `InstanceAddress` from a contract's `instanceId` and its - * signatory party. Matches Go: `instanceID.RawInstanceAddress(party).InstanceAddress()`. - * - * Format: `keccak256(utf8("@"))` - */ -function computeInstanceAddress(instanceId: string, signatory: string): string { - return keccak256(toUtf8Bytes(`${instanceId}@${signatory}`)) -} - -function instanceAddressEquals(a: string, b: string): boolean { - return a.toLowerCase().replace(/^0x/, '') === b.toLowerCase().replace(/^0x/, '') -} - /** * Extract the `instanceId` string from a contract's `createArgument` object. * Handles both verbose mode (direct field) and structured fields arrays. @@ -83,14 +61,8 @@ function buildWildcardEventFormat(party: string): EventFormat { * without requiring a specific package ID. */ const CCIP_MODULE_ENTITIES = { - offRamp: 'CCIP.OffRamp:OffRamp', - globalConfig: 'CCIP.GlobalConfig:GlobalConfig', - tokenAdminRegistry: 'CCIP.TokenAdminRegistry:TokenAdminRegistry', - rmnRemote: 'CCIP.RMNRemote:RMNRemote', - committeeVerifier: 'CCIP.CommitteeVerifier:CommitteeVerifier', perPartyRouter: 'CCIP.PerPartyRouter:PerPartyRouter', - onRamp: 'CCIP.OnRamp:OnRamp', - feeQuoter: 'CCIP.FeeQuoter:FeeQuoter', + ccipReceiver: 'CCIP.CCIPReceiver:CCIPReceiver', } as const type CcipContractType = keyof typeof CCIP_MODULE_ENTITIES @@ -118,12 +90,14 @@ async function fetchRichSnapshot( party: string, ): Promise> { const { offset } = await client.getLedgerEnd() + const request: GetActiveContractsRequest = { eventFormat: buildWildcardEventFormat(party), verbose: false, activeAtOffset: offset, } + // Note: this may be inefficient and if the party has many active contracts. const responses = await client.getActiveContracts(request) const byModuleEntity = new Map() @@ -156,27 +130,24 @@ async function fetchRichSnapshot( } /** - * From a pre-fetched ACS snapshot, find the single contract matching the given - * contract type and instance address. + * From a pre-fetched ACS snapshot, return the single active contract of the given type + * whose signatory matches `party`. + * + * Used for contracts (like `CCIPReceiver`) whose instance IDs are generated with a random + * suffix at deployment time and therefore cannot be derived from the party ID. * * @throws `CCIPError(CANTON_API_ERROR)` if no matching contract is found. */ -function pickByInstanceAddress( +function pickBySignatory( snapshot: Map, contractType: CcipContractType, - targetInstanceAddress: string, + party: string, ): DisclosedContract { const moduleEntity = CCIP_MODULE_ENTITIES[contractType] const candidates = snapshot.get(moduleEntity) ?? [] for (const c of candidates) { - if (!c.instanceId || !c.signatory) continue - if ( - instanceAddressEquals( - computeInstanceAddress(c.instanceId, c.signatory), - targetInstanceAddress, - ) - ) { + if (c.signatory === party) { return { templateId: c.templateId, contractId: c.contractId, @@ -188,46 +159,41 @@ function pickByInstanceAddress( throw new CCIPError( CCIPErrorCode.CANTON_API_ERROR, - `Canton ACS: no active "${moduleEntity}" contract found at instance address ` + - `${targetInstanceAddress}. Verify the address is correct and the contract is ` + - `active for the configured party.`, + `Canton ACS: no active "${moduleEntity}" contract found with signatory "${party}". ` + + `Verify the party is correct and the contract is active.`, ) } -// --------------------------------------------------------------------------- -// AcsDisclosureProvider -// --------------------------------------------------------------------------- +/** + * Configuration for the ACS-based disclosure provider. + * Requires direct access to the Canton Ledger API and the full set of contract + * instance addresses. + */ +export type AcsDisclosureConfig = { + /** Canton party ID acting on behalf of the user */ + party: string +} + +/** + * Same party disclosed contracts required to submit a `ccipExecute` command on Canton. + */ +export type AcsExecutionDisclosures = { + perPartyRouter: DisclosedContract + ccipReceiver: DisclosedContract +} + +/** + * Same party disclosed contracts required to submit a `ccipSend` command on Canton. + */ +export type AcsSendDisclosures = never // not implemented yet /** * Disclosure provider that fetches `createdEventBlob`s directly from the Canton * Ledger API Active Contract Set. * - * A single wildcard ACS query is issued per `fetchExecutionDisclosures()` / - * `fetchSendDisclosures()` call (package-ID agnostic, matches the EDS strategy). - * - * @example - * ```ts - * const provider = new AcsDisclosureProvider(cantonClient, { - * jwt: '...', - * party: 'Alice::122...', - * instanceAddresses: { - * offRampAddress: '0xabc...', - * globalConfigAddress: '0xdef...', - * tokenAdminRegistryAddress: '0x123...', - * rmnRemoteAddress: '0x456...', - * perPartyRouterFactoryAddress: '0x789...', - * ccvAddresses: ['0xaaa...'], - * }, - * }) - * const disclosures = await provider.fetchExecutionDisclosures() - * ``` - * - * Use this provider when: - * - Direct Ledger API access is available. - * - A running EDS instance is not needed (local dev, integration tests). - * - Multiple CCVs or GlobalConfig / RMNRemote disclosures are required. + * Use this provider to access disclosures available in the same party */ -export class AcsDisclosureProvider implements DisclosureProvider { +export class AcsDisclosureProvider { private readonly client: CantonClient private readonly config: AcsDisclosureConfig @@ -235,7 +201,7 @@ export class AcsDisclosureProvider implements DisclosureProvider { * Create an `AcsDisclosureProvider` from a pre-built Canton Ledger API client. * * @param client - Authenticated Canton Ledger API client (JWT already embedded). - * @param config - ACS provider configuration: party ID and contract instance addresses. + * @param config - ACS provider configuration: party ID */ constructor(client: CantonClient, config: AcsDisclosureConfig) { this.client = client @@ -256,70 +222,24 @@ export class AcsDisclosureProvider implements DisclosureProvider { /** * Fetch all contracts that must be disclosed for a `ccipExecute` command. - * - * Issues a single wildcard ACS query, then resolves OffRamp, GlobalConfig, - * TokenAdminRegistry, RMNRemote, and all CCVs by instance address. */ - async fetchExecutionDisclosures(extraCcvAddresses: string[] = []): Promise { - const { party, instanceAddresses, additionalCcvAddresses = [] } = this.config - const snapshot = await fetchRichSnapshot(this.client, party) - - const offRamp = pickByInstanceAddress(snapshot, 'offRamp', instanceAddresses.offRampAddress) - const globalConfig = pickByInstanceAddress( - snapshot, - 'globalConfig', - instanceAddresses.globalConfigAddress, - ) - const tokenAdminRegistry = pickByInstanceAddress( - snapshot, - 'tokenAdminRegistry', - instanceAddresses.tokenAdminRegistryAddress, - ) - const rmnRemote = pickByInstanceAddress( - snapshot, - 'rmnRemote', - instanceAddresses.rmnRemoteAddress, - ) + async fetchExecutionDisclosures(): Promise { + const snapshot = await fetchRichSnapshot(this.client, this.config.party) - const allCcvAddresses = [ - ...new Set([ - ...instanceAddresses.ccvAddresses, - ...additionalCcvAddresses, - ...extraCcvAddresses, - ]), - ] + const existingRouter = pickBySignatory(snapshot, 'perPartyRouter', this.config.party) + const existingReceiver = pickBySignatory(snapshot, 'ccipReceiver', this.config.party) - const verifiers: DisclosedContract[] = allCcvAddresses.map((addr) => - pickByInstanceAddress(snapshot, 'committeeVerifier', addr), - ) - - return { offRamp, globalConfig, tokenAdminRegistry, rmnRemote, verifiers } + return { + perPartyRouter: existingRouter, + ccipReceiver: existingReceiver, + } } /** * Fetch all contracts that must be disclosed for a `ccipSend` command. - * - * Requires `routerAddress`, `onRampAddress`, and `feeQuoterAddress` to be - * provided in `instanceAddresses`. */ - async fetchSendDisclosures(): Promise { - const { party, instanceAddresses } = this.config - const { routerAddress, onRampAddress, feeQuoterAddress } = instanceAddresses - - if (!routerAddress || !onRampAddress || !feeQuoterAddress) { - throw new CCIPError( - CCIPErrorCode.CANTON_API_ERROR, - 'Canton ACS: fetchSendDisclosures requires routerAddress, onRampAddress, and ' + - 'feeQuoterAddress to be set in the instanceAddresses configuration.', - ) - } - - const snapshot = await fetchRichSnapshot(this.client, party) - - const router = pickByInstanceAddress(snapshot, 'perPartyRouter', routerAddress) - const onRamp = pickByInstanceAddress(snapshot, 'onRamp', onRampAddress) - const feeQuoter = pickByInstanceAddress(snapshot, 'feeQuoter', feeQuoterAddress) - - return { router, onRamp, feeQuoter } + async fetchSendDisclosures(): Promise { + await Promise.resolve() // placeholder for potential future implementation + throw new CCIPNotImplementedError('AcsDisclosureProvider.fetchSendDisclosures') } } diff --git a/ccip-sdk/src/canton/explicit-disclosures/eds.ts b/ccip-sdk/src/canton/explicit-disclosures/eds.ts index aff34d6c..1731dcb7 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/eds.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/eds.ts @@ -1,55 +1,79 @@ -import type { - DisclosedContract, - DisclosureProvider, - EdsDisclosureConfig, - ExecutionDisclosures, - SendDisclosures, -} from './types.ts' +import type { DisclosedContract } from './types.ts' import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' -/** Structured Daml template identifier as returned by the EDS. */ -interface EdsTemplateID { - packageId: string - moduleName: string - entityName: string +/** + * Configuration for the EDS-based disclosure provider. + * Requires only the EDS base URL — no direct ledger access needed. + */ +interface EdsDisclosureConfig { + /** Base URL of the running EDS instance, e.g. `http://eds-host:8090` */ + edsBaseUrl: string + /** Optional request timeout in milliseconds (default: 10_000) */ + timeoutMs?: number +} + +/** + * The context returned by the EDS for a send or execute request. + * + * Corresponds to `ChoiceContext` in the EDS OpenAPI spec. + * - `choiceContextData` must be included as additional data when exercising the Canton choice. + * - `disclosedContracts` must be attached to the Canton command submission. + */ +interface EdsChoiceContext { + /** Additional opaque data required when exercising the Canton choice. */ + choiceContextData: unknown + /** Contracts that must be explicitly disclosed in the command submission. */ + disclosedContracts: DisclosedContract[] +} + +/** + * Result of a `fetchPerPartyRouterFactoryDisclosures()` call. + * + * Corresponds to `CCIPPerPartyRouterFactoryResponse` in the EDS OpenAPI spec. + */ +interface EdsPerPartyRouterFactoryResult { + /** The Contract ID of the PerPartyRouterFactory. */ + perPartyRouterFactoryId: string + /** Disclosures for all contracts required to instantiate a PerPartyRouter. */ + disclosedContracts: DisclosedContract[] } /** - * A single contract as returned by the EDS. - * `createdEventBlob` is base64-encoded. + * A single disclosed contract as returned by the EDS API. + * `templateId` is already a flat colon-delimited string (`":Module:Entity"`). */ -interface EdsDisclosedContract { +interface EdsApiDisclosedContract { + templateId: string contractId: string - instanceId: string - templateId: EdsTemplateID createdEventBlob: string - synchronizerId?: string + synchronizerId: string } -// TODO: Add EdsCCIPExecuteDisclosures interface here once the EDS execute endpoint -// returns globalConfig and rmnRemote fields. +/** `ChoiceContext` object returned from send/execute endpoints. */ +interface EdsApiChoiceContext { + choiceContextData: unknown + disclosedContracts: EdsApiDisclosedContract[] +} -/** EDS `/disclosures/send` response body. */ -interface EdsCCIPSendDisclosures { - environmentId: string - contracts: { - router: EdsDisclosedContract | null - onRamp: EdsDisclosedContract | null - feeQuoter: EdsDisclosedContract | null - } +/** Response body of `POST /ccip/v1/message/send`. */ +interface EdsCCIPSendResponse { + choiceContext: EdsApiChoiceContext +} + +/** Response body of `POST /ccip/v1/message/execute`. */ +interface EdsCCIPExecuteResponse { + choiceContext: EdsApiChoiceContext } -/** EDS `/health` response body. */ -interface EdsHealthResponse { - status: string - ledgerApiConnected: boolean - environments: string[] +/** Response body of `POST /ccip/v1/perPartyRouter/factory`. */ +interface EdsPerPartyRouterFactoryResponse { + perPartyRouterFactoryId: string + disclosedContracts: EdsApiDisclosedContract[] } /** EDS error response body. */ interface EdsErrorResponse { error: string - code?: string details?: string } @@ -57,25 +81,33 @@ interface EdsErrorResponse { // Helpers // --------------------------------------------------------------------------- -/** - * Convert an EDS `DisclosedContract` to the SDK's internal `DisclosedContract`. - * The EDS uses a structured `templateId`; the SDK uses a flat colon-delimited string. - */ -function edsContractToSdk(c: EdsDisclosedContract): DisclosedContract { +function edsContractToSdk(c: EdsApiDisclosedContract): DisclosedContract { return { - templateId: `${c.templateId.packageId}:${c.templateId.moduleName}:${c.templateId.entityName}`, + templateId: c.templateId, contractId: c.contractId, createdEventBlob: c.createdEventBlob, synchronizerId: c.synchronizerId, } } -async function edsGet(url: string, timeoutMs: number): Promise { +function rawContextToSdk(raw: EdsApiChoiceContext): EdsChoiceContext { + return { + choiceContextData: raw.choiceContextData, + disclosedContracts: raw.disclosedContracts.map(edsContractToSdk), + } +} + +async function edsPost(url: string, body: unknown, timeoutMs: number): Promise { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeoutMs) let response: Response try { - response = await fetch(url, { signal: controller.signal }) + response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + signal: controller.signal, + }) } catch (err) { const msg = err instanceof Error ? err.message : String(err) throw new CCIPError( @@ -90,8 +122,8 @@ async function edsGet(url: string, timeoutMs: number): Promise { if (!response.ok) { let detail = '' try { - const body = (await response.json()) as EdsErrorResponse - detail = ` [${body.code ?? response.status}] ${body.error}` + const errBody = (await response.json()) as EdsErrorResponse + detail = ` ${errBody.error}${errBody.details ? `: ${errBody.details}` : ''}` } catch { detail = ` HTTP ${response.status}` } @@ -101,38 +133,12 @@ async function edsGet(url: string, timeoutMs: number): Promise { return response.json() as Promise } -// --------------------------------------------------------------------------- -// EdsDisclosureProvider -// --------------------------------------------------------------------------- - /** - * Disclosure provider that fetches `createdEventBlob`s from a running EDS instance. - * - * This is the recommended approach for deployments where CCIP contracts are owned - * by a different party than the message executor — the EDS holds credentials - * allowing it to read those contracts on the user's behalf. - * - * @example - * ```ts - * const provider = new EdsDisclosureProvider({ - * edsBaseUrl: 'http://eds.internal:8090', - * environmentId: 'testnet', - * }) - * - * // Optional: verify connectivity before use - * await provider.checkHealth() - * - * const disclosures = await provider.fetchSendDisclosures() - * ``` - * - * @remarks - * **Placeholder**: `fetchExecutionDisclosures()` currently throws because the EDS - * execute endpoint does not yet return `globalConfig` or `rmnRemote`. Use - * `AcsDisclosureProvider` for execution until the EDS is extended. + * Disclosure provider that fetches explicit disclosures from a running EDS instance + * using the CCIP Explicit Disclosure API */ -export class EdsDisclosureProvider implements DisclosureProvider { +export class EdsDisclosureProvider { private readonly edsBaseUrl: string - private readonly environmentId: string private readonly timeoutMs: number /** @@ -142,88 +148,57 @@ export class EdsDisclosureProvider implements DisclosureProvider { */ constructor(config: EdsDisclosureConfig) { this.edsBaseUrl = config.edsBaseUrl.replace(/\/$/, '') - this.environmentId = config.environmentId this.timeoutMs = config.timeoutMs ?? 10_000 } /** - * Verify that the EDS is reachable and has the configured environment loaded. + * Fetch the explicit disclosures required to send a CCIP message to Canton. * - * @throws `CCIPError(CANTON_API_ERROR)` if the EDS is unreachable or the - * environment is not listed in the health response. + * Calls `POST /ccip/v1/message/send`. + * + * @param destChain - Destination chain selector (integer). + * @returns `EdsChoiceContext` containing the `choiceContextData` and all + * `disclosedContracts` that must be attached to the Canton command submission. */ - async checkHealth(): Promise { - const url = `${this.edsBaseUrl}/api/v1/health` - const health = await edsGet(url, this.timeoutMs) - - if (health.status !== 'healthy') { - throw new CCIPError( - CCIPErrorCode.CANTON_API_ERROR, - `EDS reports unhealthy status: "${health.status}"`, - ) - } - - if (!health.environments.includes(this.environmentId)) { - throw new CCIPError( - CCIPErrorCode.CANTON_API_ERROR, - `EDS does not have environment "${this.environmentId}" loaded. ` + - `Available: ${health.environments.join(', ') || '(none)'}`, - ) - } + async fetchSendDisclosures(destChain: number): Promise { + const url = `${this.edsBaseUrl}/ccip/v1/message/send` + const resp = await edsPost(url, { destChain }, this.timeoutMs) + return rawContextToSdk(resp.choiceContext) } /** - * Fetch all contracts that must be disclosed for a `ccipExecute` command. + * Fetch the explicit disclosures required to execute a CCIP message on Canton. + * + * Calls `POST /ccip/v1/message/execute`. * - * @throws Always — the EDS execute endpoint does not yet return `globalConfig` - * or `rmnRemote`. This method is a placeholder pending EDS extension. - * Use `AcsDisclosureProvider` for execution. + * @param sourceChain - Source chain selector (integer). + * @returns `EdsChoiceContext` containing the `choiceContextData` and all + * `disclosedContracts` that must be attached to the Canton command submission. */ - fetchExecutionDisclosures(_extraCcvAddresses: string[] = []): Promise { - // TODO: Remove this throw and uncomment mapExecuteDisclosures() once the EDS - // CCIPExecuteContracts struct is extended with globalConfig and rmnRemote fields. - throw new CCIPError( - CCIPErrorCode.CANTON_API_ERROR, - 'EdsDisclosureProvider.fetchExecutionDisclosures is not yet available: the EDS execute ' + - 'endpoint does not return "globalConfig" or "rmnRemote". Use AcsDisclosureProvider.', - ) + async fetchExecutionDisclosures(sourceChain: number): Promise { + const url = `${this.edsBaseUrl}/ccip/v1/message/execute` + const resp = await edsPost(url, { sourceChain }, this.timeoutMs) + return rawContextToSdk(resp.choiceContext) } /** - * Fetch all contracts that must be disclosed for a `ccipSend` command. + * Fetch the explicit disclosures required to instantiate a PerPartyRouter + * using the PerPartyRouterFactory. + * + * Calls `POST /ccip/v1/perPartyRouter/factory`. * - * Calls `GET /api/v1/ccip/ENVIRONMENT_ID/disclosures/send`. + * @param partyID - The Daml party ID of the caller. + * @returns `EdsPerPartyRouterFactoryResult` containing the factory contract ID + * and all `disclosedContracts` needed to instantiate a PerPartyRouter. */ - async fetchSendDisclosures(): Promise { - const url = `${this.edsBaseUrl}/api/v1/ccip/${encodeURIComponent(this.environmentId)}/disclosures/send` - const eds = await edsGet(url, this.timeoutMs) - return this.mapSendDisclosures(eds) - } - - /** Map the EDS send response to the SDK `SendDisclosures` shape. */ - private mapSendDisclosures(eds: EdsCCIPSendDisclosures): SendDisclosures { - const { router, onRamp, feeQuoter } = eds.contracts - - if (!router) this.missingContract('router', 'send') - if (!onRamp) this.missingContract('onRamp', 'send') - if (!feeQuoter) this.missingContract('feeQuoter', 'send') - - // TypeScript narrows router/onRamp/feeQuoter to non-null after the missingContract() guards above - // because missingContract() returns `never` — execution only reaches here when all three are set. + async fetchPerPartyRouterFactoryDisclosures( + partyID: string, + ): Promise { + const url = `${this.edsBaseUrl}/ccip/v1/perPartyRouter/factory` + const resp = await edsPost(url, { partyID }, this.timeoutMs) return { - router: edsContractToSdk(router), - onRamp: edsContractToSdk(onRamp), - feeQuoter: edsContractToSdk(feeQuoter), + perPartyRouterFactoryId: resp.perPartyRouterFactoryId, + disclosedContracts: resp.disclosedContracts.map(edsContractToSdk), } } - - /** Throw a descriptive error for a missing contract in an EDS response. */ - private missingContract(field: string, operation: string): never { - throw new CCIPError( - CCIPErrorCode.CANTON_API_ERROR, - `EDS returned null for "${field}" in the ${operation} disclosures response ` + - `(environment "${this.environmentId}"). ` + - `Verify the EDS environments.yaml configuration contains a "${field}" instance ID.`, - ) - } } diff --git a/ccip-sdk/src/canton/explicit-disclosures/index.ts b/ccip-sdk/src/canton/explicit-disclosures/index.ts index 073a31d5..94aa5289 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/index.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/index.ts @@ -1,25 +1,4 @@ -/** - * Canton Explicit Disclosures — public exports - * - * Provides two `DisclosureProvider` implementations: - * - `AcsDisclosureProvider` direct Canton Ledger API ACS queries (package-ID agnostic) - * - `EdsDisclosureProvider` Explicit Disclosure Service REST API (placeholder) - * - * Import the provider that matches your deployment: - * ```ts - * import { AcsDisclosureProvider } from '@chainlink/ccip-sdk/canton/explicit-disclosures' - * ``` - */ - export { AcsDisclosureProvider } from './acs.ts' +export type { AcsDisclosureConfig, AcsExecutionDisclosures } from './acs.ts' export { EdsDisclosureProvider } from './eds.ts' -export type { - AcsDisclosureConfig, - CCIPContractInstanceAddresses, - DisclosedContract, - DisclosureProvider, - EdsDisclosureConfig, - ExecutionDisclosures, - SendDisclosures, -} from './types.ts' -export { executionDisclosuresToArray, sendDisclosuresToArray } from './types.ts' +export type { DisclosedContract } from './types.ts' diff --git a/ccip-sdk/src/canton/explicit-disclosures/types.ts b/ccip-sdk/src/canton/explicit-disclosures/types.ts index 52c16bff..f016d3aa 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/types.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/types.ts @@ -1,21 +1,5 @@ -/** - * Explicit Disclosures — shared types - * - * Canton requires attaching `createdEventBlob` for every contract referenced by a - * command submission. The types below model the contracts needed for each CCIP - * operation (execute vs. send) and the two strategies for obtaining them: - * - * - AcsDisclosureProvider — queries the Canton Ledger API ACS directly. - * - EdsDisclosureProvider — calls the Explicit Disclosure Service (EDS) REST API. - */ - -// --------------------------------------------------------------------------- -// Core contract descriptor -// --------------------------------------------------------------------------- - /** * A single disclosed contract, ready to be embedded in a Canton command submission - * as an element of `JsCommands.disclosedContracts`. */ export interface DisclosedContract { /** Full Daml template ID string, e.g. `":CCIP.OffRamp:OffRamp"` */ @@ -27,148 +11,3 @@ export interface DisclosedContract { /** Synchronizer from which the contract was read (required for multi-synchronizer Canton deployments) */ synchronizerId?: string } - -// --------------------------------------------------------------------------- -// Grouped disclosure shapes per CCIP operation -// --------------------------------------------------------------------------- - -/** - * All disclosed contracts required to submit a `ccipExecute` command on Canton. - * Matches the Go `ExecutionDisclosures` struct in `eds.go`. - */ -export interface ExecutionDisclosures { - offRamp: DisclosedContract - globalConfig: DisclosedContract - tokenAdminRegistry: DisclosedContract - rmnRemote: DisclosedContract - /** One entry per CommitteeVerifier (CCV) referenced by the execute command */ - verifiers: DisclosedContract[] -} - -/** - * All disclosed contracts required to submit a `ccipSend` command on Canton. - * Matches the Go `CCIPSendContracts` struct in EDS `types.go`. - */ -export interface SendDisclosures { - router: DisclosedContract - onRamp: DisclosedContract - feeQuoter: DisclosedContract -} - -// --------------------------------------------------------------------------- -// Contract instance addresses (needed by the ACS provider) -// --------------------------------------------------------------------------- - -/** - * Hex-encoded keccak256 instance addresses for all CCIP contracts that must be - * disclosed when executing a CCIP message on Canton. - * - * These are **not** Daml contract IDs — they are derived from the contract's - * `instanceId` field and its signatory party via: - * `keccak256(utf8("@"))` - * - * Required when using `AcsDisclosureProvider`. Not needed for `EdsDisclosureProvider`. - */ -export interface CCIPContractInstanceAddresses { - offRampAddress: string - globalConfigAddress: string - tokenAdminRegistryAddress: string - rmnRemoteAddress: string - perPartyRouterFactoryAddress: string - /** An address for each CommitteeVerifier (CCV) that will be referenced in execution */ - ccvAddresses: string[] - /** PerPartyRouter instance address (send path) */ - routerAddress?: string - /** OnRamp instance address (send path) */ - onRampAddress?: string - /** FeeQuoter instance address (send path) */ - feeQuoterAddress?: string -} - -// --------------------------------------------------------------------------- -// Provider configuration -// --------------------------------------------------------------------------- - -/** - * Configuration for the ACS-based disclosure provider. - * Requires direct access to the Canton Ledger API and the full set of contract - * instance addresses. - */ -export interface AcsDisclosureConfig { - /** Canton party ID acting on behalf of the user */ - party: string - /** Hex-encoded instance addresses for all CCIP contracts */ - instanceAddresses: CCIPContractInstanceAddresses - /** Additional CCV instance addresses to merge in at call time */ - additionalCcvAddresses?: string[] -} - -/** - * Configuration for the EDS-based disclosure provider. - * Requires only the EDS base URL and environment ID — no direct ledger access needed. - */ -export interface EdsDisclosureConfig { - /** Base URL of the running EDS instance, e.g. `http://eds-host:8090` */ - edsBaseUrl: string - /** - * Environment identifier as configured in the EDS `environments.yaml`, - * e.g. `"localnet"`, `"testnet"`, `"mainnet"`. - */ - environmentId: string - /** Optional request timeout in milliseconds (default: 10_000) */ - timeoutMs?: number -} - -// --------------------------------------------------------------------------- -// Provider interface -// --------------------------------------------------------------------------- - -/** - * Implemented by both `AcsDisclosureProvider` and `EdsDisclosureProvider`. - * The SDK calls these methods internally before building a Canton command — the - * user never needs to interact with this interface directly. - * - * An optional `additionalDisclosures` callback can be set on the provider to - * merge extra contracts (e.g. a user-deployed `CCIPReceiver`) into every command. - */ -export interface DisclosureProvider { - /** - * Fetch all contracts that must be disclosed for a `ccipExecute` command. - * @param extraCcvAddresses - Optional extra CCV instance addresses to resolve - * on top of those already known to the provider. - */ - fetchExecutionDisclosures(extraCcvAddresses?: string[]): Promise - - /** - * Fetch all contracts that must be disclosed for a `ccipSend` command. - */ - fetchSendDisclosures(): Promise -} - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -/** - * Flatten an `ExecutionDisclosures` object into the flat array expected by - * `JsCommands.disclosedContracts`. - */ -export function executionDisclosuresToArray( - disclosures: ExecutionDisclosures, -): DisclosedContract[] { - return [ - disclosures.offRamp, - disclosures.globalConfig, - disclosures.tokenAdminRegistry, - disclosures.rmnRemote, - ...disclosures.verifiers, - ] -} - -/** - * Flatten a `SendDisclosures` object into the flat array expected by - * `JsCommands.disclosedContracts`. - */ -export function sendDisclosuresToArray(disclosures: SendDisclosures): DisclosedContract[] { - return [disclosures.router, disclosures.onRamp, disclosures.feeQuoter] -} diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index b243c391..deff40ed 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -1,7 +1,16 @@ import type { BytesLike } from 'ethers' import type { PickDeep } from 'type-fest' -import { type ChainContext, type GetBalanceOpts, type LogFilter, Chain } from '../chain.ts' +import { + type ChainContext, + type GetBalanceOpts, + type LogFilter, + type RegistryTokenConfig, + type TokenInfo, + type TokenPoolConfig, + type TokenPoolRemote, + Chain, +} from '../chain.ts' import { CCIPChainNotFoundError, CCIPError, @@ -16,19 +25,24 @@ import { type CCIPExecution, type CCIPMessage, type CCIPRequest, + type CCIPVerifications, + type ChainLog, type ChainTransaction, type CommitReport, type ExecutionReceipt, type Lane, - type Log_, type NetworkInfo, type OffchainTokenData, + type VerifierResult, type WithLogger, + CCIPVersion, ChainFamily, } from '../types.ts' import { networkInfo } from '../utils.ts' import { type CantonClient, createCantonClient } from './client/index.ts' -import type { DisclosureProvider } from './explicit-disclosures/types.ts' +import { type AcsDisclosureConfig, AcsDisclosureProvider } from './explicit-disclosures/acs.ts' +import { CCV_INDEXER_URL } from '../evm/const.ts' +import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' export type { CantonClient, CantonClientConfig } from './client/index.ts' @@ -46,25 +60,33 @@ export class CantonChain extends Chain { override readonly network: NetworkInfo readonly provider: CantonClient - readonly disclosureProvider: DisclosureProvider + readonly acsDisclosureProvider: AcsDisclosureProvider + readonly edsDisclosureProvider: EdsDisclosureProvider + readonly indexerUrl: string /** * Creates a new CantonChain instance. * @param client - Canton Ledger API client. - * @param disclosureProvider - Provider used for explicit disclosures. + * @param acsDisclosureProvider - ACS-based disclosure provider. + * @param edsDisclosureProvider - EDS-based disclosure provider. + * @param indexerUrl - Base URL of the CCV indexer service. * @param network - Network information for this chain. * @param ctx - Context containing logger. */ constructor( client: CantonClient, - disclosureProvider: DisclosureProvider, + acsDisclosureProvider: AcsDisclosureProvider, + edsDisclosureProvider: EdsDisclosureProvider, + indexerUrl: string, network: NetworkInfo, ctx?: ChainContext, ) { super(network, ctx) this.provider = client this.network = network - this.disclosureProvider = disclosureProvider + this.acsDisclosureProvider = acsDisclosureProvider + this.edsDisclosureProvider = edsDisclosureProvider + this.indexerUrl = indexerUrl } /** @@ -84,6 +106,7 @@ export class CantonChain extends Chain { ['mainnet', 'canton:MainNet'], ['main', 'canton:MainNet'], ['canton-mainnet', 'canton:MainNet'], + ['global', 'canton:DevNet'], ]) /** @@ -97,7 +120,9 @@ export class CantonChain extends Chain { */ static async fromClient( client: CantonClient, - disclosureProvider: DisclosureProvider, + acsDisclosureProvider: AcsDisclosureProvider, + edsDisclosureProvider: EdsDisclosureProvider, + indexerUrl = CCV_INDEXER_URL, ctx?: ChainContext, ): Promise { const synchronizers = await client.getConnectedSynchronizers() @@ -110,7 +135,9 @@ export class CantonChain extends Chain { if (chainId) { return new CantonChain( client, - disclosureProvider, + acsDisclosureProvider, + edsDisclosureProvider, + indexerUrl, networkInfo(chainId) as NetworkInfo, ctx, ) @@ -146,7 +173,13 @@ export class CantonChain extends Chain { `Failed to connect to Canton Ledger API ${url}: ${message}`, ) } - return CantonChain.fromClient(client, ctx) + const defaultACSConfig: AcsDisclosureConfig = { + party: 'ccip-sdk-party', + } + // TODO: These need to come from input + const acsDisclosureProvider = new AcsDisclosureProvider(client, defaultACSConfig) + const edsDisclosureProvider = new EdsDisclosureProvider({ edsBaseUrl: url }) + return CantonChain.fromClient(client, acsDisclosureProvider, edsDisclosureProvider, '', ctx) } /** @@ -177,7 +210,7 @@ export class CantonChain extends Chain { * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ // eslint-disable-next-line require-yield - async *getLogs(_opts: LogFilter): AsyncIterableIterator { + async *getLogs(_opts: LogFilter): AsyncIterableIterator { throw new CCIPNotImplementedError('CantonChain.getLogs') } @@ -212,6 +245,7 @@ export class CantonChain extends Chain { * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ async getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { + // TODO: Contract discovery can come from EDS. throw new CCIPNotImplementedError('CantonChain.getRouterForOnRamp') } @@ -248,18 +282,13 @@ export class CantonChain extends Chain { } /** - * {@inheritDoc Chain.getOnRampForOffRamp} + * {@inheritDoc Chain.getOnRampsForOffRamp} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getOnRampForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { - throw new CCIPNotImplementedError('CantonChain.getOnRampForOffRamp') + async getOnRampsForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + throw new CCIPNotImplementedError('CantonChain.getOnRampsForOffRamp') } - /** - * {@inheritDoc Chain.getCommitStoreForOffRamp} - * - * For Canton (CCIP v1.6+), the OffRamp itself serves as the commit store. - */ async getCommitStoreForOffRamp(offRamp: string): Promise { return Promise.resolve(offRamp) } @@ -325,7 +354,7 @@ export class CantonChain extends Chain { /** * {@inheritDoc Chain.getOffchainTokenData} */ - getOffchainTokenData(request: CCIPRequest): Promise { + override getOffchainTokenData(request: CCIPRequest): Promise { return Promise.resolve(request.message.tokenAmounts.map(() => undefined)) } @@ -333,8 +362,8 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.generateUnsignedExecuteReport} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - override generateUnsignedExecuteReport( - _opts: Parameters[0], + override generateUnsignedExecute( + _opts: Parameters[0], ): Promise { return Promise.reject(new CCIPNotImplementedError('CantonChain.generateUnsignedExecuteReport')) } @@ -343,15 +372,83 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.executeReport} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async executeReport(_opts: Parameters[0]): Promise { + async execute(opts: Parameters[0]): Promise { + // TODO throw new CCIPNotImplementedError('CantonChain.executeReport') } + /** + * Fetches CCV verification results for a CCIP message from the Canton indexer. + * @param opts - Options including the CCIP request containing the message ID to query. + * @returns CCIPVerifications with verification policy and individual verifier results. + */ + override async getVerifications( + opts: Parameters[0], + ): Promise { + const { request } = opts + if (request.lane.version < CCIPVersion.V2_0) { + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + `CantonChain.getVerifications: CCIP versions below v2.0 are not supported in Canton (request lane version: ${request.lane.version})`, + ) + } + + const url = `${this.indexerUrl}/v1/verifierresults/${request.message.messageId}` + const res = await fetch(url) + if (!res.ok) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton indexer responded with ${res.status} for message ${request.message.messageId}`, + ) + } + + const json = (await res.json()) as { + success: boolean + results: Array<{ + verifierResult: { + message_ccv_addresses: string[] + ccv_data: string + timestamp: string + verifier_source_address: string + verifier_dest_address: string + } + }> + messageID: string + } + + if (!json.success) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton indexer returned success=false for message ${request.message.messageId}`, + ) + } + + // message_ccv_addresses is a message-level property — identical across all results. + // Use the first result's list as requiredCCVs; fall back to empty if no results yet. + const requiredCCVs: string[] = json.results[0]?.verifierResult.message_ccv_addresses ?? [] + + const verifications: VerifierResult[] = json.results.map(({ verifierResult: vr }) => ({ + ccvData: vr.ccv_data, + sourceAddress: vr.verifier_source_address, + destAddress: vr.verifier_dest_address, + timestamp: vr.timestamp ? Math.floor(new Date(vr.timestamp).getTime() / 1000) : undefined, + })) + + return { + verificationPolicy: { + requiredCCVs, + optionalCCVs: [], + optionalThreshold: 0, + }, + verifications, + } + } + /** * {@inheritDoc Chain.getSupportedTokens} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getSupportedTokens(_address: string): Promise { + async getSupportedTokens(_address: string, _opts?: { page?: number }): Promise { throw new CCIPNotImplementedError('CantonChain.getSupportedTokens') } @@ -359,7 +456,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getRegistryTokenConfig} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getRegistryTokenConfig(_registry: string, _token: string): Promise { + async getRegistryTokenConfig(_registry: string, _token: string): Promise { throw new CCIPNotImplementedError('CantonChain.getRegistryTokenConfig') } @@ -367,7 +464,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenPoolConfig} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenPoolConfig(_tokenPool: string): Promise { + async getTokenPoolConfig(_tokenPool: string): Promise { throw new CCIPNotImplementedError('CantonChain.getTokenPoolConfig') } @@ -375,7 +472,10 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenPoolRemotes} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenPoolRemotes(_tokenPool: string): Promise { + async getTokenPoolRemotes( + _tokenPool: string, + _remoteChainSelector?: bigint, + ): Promise> { throw new CCIPNotImplementedError('CantonChain.getTokenPoolRemotes') } @@ -383,7 +483,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getFeeTokens} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getFeeTokens(_router: string): Promise { + async getFeeTokens(_router: string): Promise> { throw new CCIPNotImplementedError('CantonChain.getFeeTokens') } @@ -393,7 +493,7 @@ export class CantonChain extends Chain { * Try to decode a CCIP message from a Canton log/event. * @returns undefined (Canton message format not yet supported) */ - static decodeMessage(_log: Pick): CCIPMessage | undefined { + static decodeMessage(_log: Pick): CCIPMessage | undefined { // TODO: implement Canton message decoding return undefined } @@ -419,7 +519,7 @@ export class CantonChain extends Chain { * Try to decode a commit report from a Canton log. * @returns undefined (Canton commit format not yet supported) */ - static decodeCommits(_log: Pick, _lane?: Lane): CommitReport[] | undefined { + static decodeCommits(_log: Pick, _lane?: Lane): CommitReport[] | undefined { // TODO: implement Canton commit report decoding return undefined } @@ -428,7 +528,7 @@ export class CantonChain extends Chain { * Try to decode an execution receipt from a Canton log. * @returns undefined (Canton receipt format not yet supported) */ - static decodeReceipt(_log: Pick): ExecutionReceipt | undefined { + static decodeReceipt(_log: Pick): ExecutionReceipt | undefined { // TODO: implement Canton execution receipt decoding return undefined } diff --git a/package-lock.json b/package-lock.json index 3910b3fa..7ec05277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -178,7 +178,6 @@ "ethers": "6.16.0", "got": "^11.8.6", "micro-memoize": "^5.1.1", - "openapi-fetch": "^0.17.0", "type-fest": "^5.4.4", "yaml": "2.8.2" }, @@ -23532,15 +23531,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-fetch": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", - "integrity": "sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==", - "license": "MIT", - "dependencies": { - "openapi-typescript-helpers": "^0.1.0" - } - }, "node_modules/openapi-to-postmanv2": { "version": "5.8.0", "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.8.0.tgz", @@ -23618,12 +23608,6 @@ "node": ">= 6" } }, - "node_modules/openapi-typescript-helpers": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.1.0.tgz", - "integrity": "sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==", - "license": "MIT" - }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", From 83711abed46a877cb1d7d720eaa4042dc4e18282 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 3 Mar 2026 13:45:56 +0100 Subject: [PATCH 04/17] refactor eds and add manual exec --- .../src/canton/explicit-disclosures/eds.ts | 163 +++++++-- .../src/canton/explicit-disclosures/index.ts | 9 + .../src/canton/explicit-disclosures/types.ts | 2 +- ccip-sdk/src/canton/index.ts | 328 +++++++++++++++++- ccip-sdk/src/canton/types.ts | 35 ++ ccip-sdk/src/chain.ts | 3 +- 6 files changed, 504 insertions(+), 36 deletions(-) create mode 100644 ccip-sdk/src/canton/types.ts diff --git a/ccip-sdk/src/canton/explicit-disclosures/eds.ts b/ccip-sdk/src/canton/explicit-disclosures/eds.ts index 1731dcb7..f9a6800b 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/eds.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/eds.ts @@ -5,7 +5,7 @@ import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' * Configuration for the EDS-based disclosure provider. * Requires only the EDS base URL — no direct ledger access needed. */ -interface EdsDisclosureConfig { +export interface EdsDisclosureConfig { /** Base URL of the running EDS instance, e.g. `http://eds-host:8090` */ edsBaseUrl: string /** Optional request timeout in milliseconds (default: 10_000) */ @@ -19,19 +19,77 @@ interface EdsDisclosureConfig { * - `choiceContextData` must be included as additional data when exercising the Canton choice. * - `disclosedContracts` must be attached to the Canton command submission. */ -interface EdsChoiceContext { +export interface EdsChoiceContext { /** Additional opaque data required when exercising the Canton choice. */ choiceContextData: unknown /** Contracts that must be explicitly disclosed in the command submission. */ disclosedContracts: DisclosedContract[] } +/** + * The result of an explicit disclosure lookup for a single contract. + * + * Corresponds to `OptionalDisclosure` in the EDS OpenAPI spec. + * - If the EDS can serve a disclosure directly, `disclosedContract` is populated. + * - If the contract's owner registered it with the global EDS registry but this + * EDS cannot serve it, `registeredContract` points to the owning EDS. + */ +export interface OptionalDisclosure { + /** The disclosed contract, if this EDS can serve it. */ + disclosedContract?: DisclosedContract + /** Redirect information when the disclosure must be fetched from another EDS. */ + registeredContract?: RegisteredContract +} + +/** + * Information about a contract registered with the global EDS registry whose + * disclosure must be fetched from a different EDS instance. + * + * Corresponds to `RegisteredContract` in the EDS OpenAPI spec. + */ +export interface RegisteredContract { + /** The party ID of the contract owner. */ + owner: string + /** The URL of the EDS that can serve an explicit disclosure for this contract. */ + edsURL: string +} + +/** + * Result of a `fetchSendDisclosures()` call. + * + * Corresponds to `CCIPSendResponse` in the EDS OpenAPI spec. + */ +export interface EdsSendResult { + /** Choice context (data + disclosed contracts) for the Canton command submission. */ + choiceContext: EdsChoiceContext + /** + * Per-CCV disclosure results, keyed by the CCV InstanceAddress. + * Each entry is either a locally-resolved disclosure or a redirect to another EDS. + */ + ccvs: Record +} + +/** + * Result of a `fetchExecutionDisclosures()` call. + * + * Corresponds to `CCIPExecuteResponse` in the EDS OpenAPI spec. + */ +export interface EdsExecuteResult { + /** Choice context (data + disclosed contracts) for the Canton command submission. */ + choiceContext: EdsChoiceContext + /** + * Per-CCV disclosure results, keyed by the CCV InstanceAddress. + * Each entry is either a locally-resolved disclosure or a redirect to another EDS. + */ + ccvs: Record +} + /** * Result of a `fetchPerPartyRouterFactoryDisclosures()` call. * * Corresponds to `CCIPPerPartyRouterFactoryResponse` in the EDS OpenAPI spec. */ -interface EdsPerPartyRouterFactoryResult { +export interface EdsPerPartyRouterFactoryResult { /** The Contract ID of the PerPartyRouterFactory. */ perPartyRouterFactoryId: string /** Disclosures for all contracts required to instantiate a PerPartyRouter. */ @@ -49,6 +107,12 @@ interface EdsApiDisclosedContract { synchronizerId: string } +/** `OptionalDisclosure` as returned by the EDS API. */ +interface EdsApiOptionalDisclosure { + disclosedContract?: EdsApiDisclosedContract + registeredContract?: { owner: string; edsURL: string } +} + /** `ChoiceContext` object returned from send/execute endpoints. */ interface EdsApiChoiceContext { choiceContextData: unknown @@ -58,11 +122,13 @@ interface EdsApiChoiceContext { /** Response body of `POST /ccip/v1/message/send`. */ interface EdsCCIPSendResponse { choiceContext: EdsApiChoiceContext + ccvs: Record } /** Response body of `POST /ccip/v1/message/execute`. */ interface EdsCCIPExecuteResponse { choiceContext: EdsApiChoiceContext + ccvs: Record } /** Response body of `POST /ccip/v1/perPartyRouter/factory`. */ @@ -90,6 +156,27 @@ function edsContractToSdk(c: EdsApiDisclosedContract): DisclosedContract { } } +function rawOptionalDisclosureToSdk(raw: EdsApiOptionalDisclosure): OptionalDisclosure { + const result: OptionalDisclosure = {} + if (raw.disclosedContract) { + result.disclosedContract = edsContractToSdk(raw.disclosedContract) + } + if (raw.registeredContract) { + result.registeredContract = raw.registeredContract + } + return result +} + +function rawCcvsToSdk( + raw: Record, +): Record { + const result: Record = {} + for (const [key, value] of Object.entries(raw)) { + result[key] = rawOptionalDisclosureToSdk(value) + } + return result +} + function rawContextToSdk(raw: EdsApiChoiceContext): EdsChoiceContext { return { choiceContextData: raw.choiceContextData, @@ -97,17 +184,12 @@ function rawContextToSdk(raw: EdsApiChoiceContext): EdsChoiceContext { } } -async function edsPost(url: string, body: unknown, timeoutMs: number): Promise { +async function edsFetch(url: string, init: RequestInit, timeoutMs: number): Promise { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeoutMs) let response: Response try { - response = await fetch(url, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - signal: controller.signal, - }) + response = await fetch(url, { ...init, signal: controller.signal }) } catch (err) { const msg = err instanceof Error ? err.message : String(err) throw new CCIPError( @@ -133,6 +215,22 @@ async function edsPost(url: string, body: unknown, timeoutMs: number): Promis return response.json() as Promise } +async function edsPost(url: string, body: unknown, timeoutMs: number): Promise { + return edsFetch( + url, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }, + timeoutMs, + ) +} + +async function edsGet(url: string, timeoutMs: number): Promise { + return edsFetch(url, { method: 'GET' }, timeoutMs) +} + /** * Disclosure provider that fetches explicit disclosures from a running EDS instance * using the CCIP Explicit Disclosure API @@ -156,14 +254,17 @@ export class EdsDisclosureProvider { * * Calls `POST /ccip/v1/message/send`. * - * @param destChain - Destination chain selector (integer). - * @returns `EdsChoiceContext` containing the `choiceContextData` and all - * `disclosedContracts` that must be attached to the Canton command submission. + * @param ccvs - InstanceAddresses of all CCVs that should verify the message. + * @returns `EdsSendResult` containing the `choiceContext` and the per-CCV + * disclosure map (`ccvs`). */ - async fetchSendDisclosures(destChain: number): Promise { + async fetchSendDisclosures(ccvs: string[]): Promise { const url = `${this.edsBaseUrl}/ccip/v1/message/send` - const resp = await edsPost(url, { destChain }, this.timeoutMs) - return rawContextToSdk(resp.choiceContext) + const resp = await edsPost(url, { ccvs }, this.timeoutMs) + return { + choiceContext: rawContextToSdk(resp.choiceContext), + ccvs: rawCcvsToSdk(resp.ccvs), + } } /** @@ -171,14 +272,18 @@ export class EdsDisclosureProvider { * * Calls `POST /ccip/v1/message/execute`. * - * @param sourceChain - Source chain selector (integer). - * @returns `EdsChoiceContext` containing the `choiceContextData` and all - * `disclosedContracts` that must be attached to the Canton command submission. + * @param messageID - The message ID of the CCIP message to be executed. + * @param ccvs - InstanceAddresses of all CCVs that should verify the message. + * @returns `EdsExecuteResult` containing the `choiceContext` and the per-CCV + * disclosure map (`ccvs`). */ - async fetchExecutionDisclosures(sourceChain: number): Promise { + async fetchExecutionDisclosures(messageID: string, ccvs: string[]): Promise { const url = `${this.edsBaseUrl}/ccip/v1/message/execute` - const resp = await edsPost(url, { sourceChain }, this.timeoutMs) - return rawContextToSdk(resp.choiceContext) + const resp = await edsPost(url, { messageID, ccvs }, this.timeoutMs) + return { + choiceContext: rawContextToSdk(resp.choiceContext), + ccvs: rawCcvsToSdk(resp.ccvs), + } } /** @@ -201,4 +306,18 @@ export class EdsDisclosureProvider { disclosedContracts: resp.disclosedContracts.map(edsContractToSdk), } } + + /** + * Fetch the explicit disclosure for a single contract by its InstanceAddress. + * + * Calls `GET /ccip/v1/disclosure/{instanceAddress}`. + * + * @param instanceAddress - The InstanceAddress of the contract. + * @returns The `DisclosedContract` for the requested contract. + */ + async fetchDisclosure(instanceAddress: string): Promise { + const url = `${this.edsBaseUrl}/ccip/v1/disclosure/${encodeURIComponent(instanceAddress)}` + const resp = await edsGet(url, this.timeoutMs) + return edsContractToSdk(resp) + } } diff --git a/ccip-sdk/src/canton/explicit-disclosures/index.ts b/ccip-sdk/src/canton/explicit-disclosures/index.ts index 94aa5289..ccda13ba 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/index.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/index.ts @@ -1,4 +1,13 @@ export { AcsDisclosureProvider } from './acs.ts' export type { AcsDisclosureConfig, AcsExecutionDisclosures } from './acs.ts' export { EdsDisclosureProvider } from './eds.ts' +export type { + EdsChoiceContext, + EdsDisclosureConfig, + EdsExecuteResult, + EdsPerPartyRouterFactoryResult, + EdsSendResult, + OptionalDisclosure, + RegisteredContract, +} from './eds.ts' export type { DisclosedContract } from './types.ts' diff --git a/ccip-sdk/src/canton/explicit-disclosures/types.ts b/ccip-sdk/src/canton/explicit-disclosures/types.ts index f016d3aa..9fdc7d93 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/types.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/types.ts @@ -9,5 +9,5 @@ export interface DisclosedContract { /** Opaque base64/hex blob obtained from the ACS `createdEvent.createdEventBlob` field */ createdEventBlob: string /** Synchronizer from which the contract was read (required for multi-synchronizer Canton deployments) */ - synchronizerId?: string + synchronizerId: string } diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index deff40ed..316a16d3 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -16,6 +16,7 @@ import { CCIPError, CCIPErrorCode, CCIPNotImplementedError, + CCIPWalletInvalidError, } from '../errors/index.ts' import type { ExtraArgs } from '../extra-args.ts' import type { LeafHasher } from '../hasher/common.ts' @@ -37,14 +38,19 @@ import { type WithLogger, CCIPVersion, ChainFamily, + ExecutionState, } from '../types.ts' import { networkInfo } from '../utils.ts' -import { type CantonClient, createCantonClient } from './client/index.ts' +import { type CantonClient, type JsCommands, createCantonClient } from './client/index.ts' import { type AcsDisclosureConfig, AcsDisclosureProvider } from './explicit-disclosures/acs.ts' +import type { DisclosedContract } from './explicit-disclosures/types.ts' import { CCV_INDEXER_URL } from '../evm/const.ts' import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' +import { type UnsignedCantonTx, isCantonWallet } from './types.ts' export type { CantonClient, CantonClientConfig } from './client/index.ts' +export type { CantonWallet, UnsignedCantonTx } from './types.ts' +export { isCantonWallet } from './types.ts' /** * Canton chain implementation supporting Canton Ledger networks. @@ -359,22 +365,209 @@ export class CantonChain extends Chain { } /** - * {@inheritDoc Chain.generateUnsignedExecuteReport} - * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) - */ - override generateUnsignedExecute( - _opts: Parameters[0], - ): Promise { - return Promise.reject(new CCIPNotImplementedError('CantonChain.generateUnsignedExecuteReport')) + * Builds a Canton `JsCommands` payload that exercises the `Execute` choice on + * the caller's `CCIPReceiver` contract. The command includes: + * + * 1. **ACS disclosures** – same-party contracts (`PerPartyRouter`, + * `CCIPReceiver`) fetched via {@link AcsDisclosureProvider}. + * 2. **EDS disclosures** – cross-party contracts (OffRamp, GlobalConfig, + * TokenAdminRegistry, RMNRemote, CCVs) fetched via + * {@link EdsDisclosureProvider}. + * 3. **Choice argument** – assembled from the encoded CCIP message, + * verification data, and the opaque `choiceContextData` returned by the + * EDS. + * + * @param opts - Must use the `offRamp` + `input` variant of {@link ExecuteOpts}. + * `input` must contain `encodedMessage` and `verifications` (CCIP v2.0). + * `payer` is the Daml party ID used for `actAs`. + * @returns An {@link UnsignedCantonTx} wrapping the ready-to-submit + * `JsCommands`. + */ + override async generateUnsignedExecute( + opts: Parameters[0], + ): Promise { + // --- validate opts shape --- + if (!('offRamp' in opts) || !('input' in opts)) { + throw new CCIPNotImplementedError( + 'CantonChain.generateUnsignedExecute: messageId-based execution is not supported; ' + + 'provide offRamp + input instead', + ) + } + + const { input, payer } = opts + if (!payer) { + throw new CCIPError( + CCIPErrorCode.WALLET_INVALID, + 'CantonChain.generateUnsignedExecute: payer (party ID) is required', + ) + } + + // v2.0 input shape: { encodedMessage, verifications } + if (!('encodedMessage' in input) || !('verifications' in input)) { + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + 'CantonChain.generateUnsignedExecute: only CCIP v2.0 ExecutionInput ' + + '(encodedMessage + verifications) is supported', + ) + } + + const { encodedMessage, verifications } = input as { + encodedMessage: string + verifications: Pick[] + } + + this.logger.debug('CantonChain.generateUnsignedExecute: fetching ACS disclosures') + // Step 1 — Fetch same-party disclosures (PerPartyRouter + CCIPReceiver) + const acsDisclosures = await this.acsDisclosureProvider.fetchExecutionDisclosures() + + // Step 2 — Fetch cross-party disclosures from EDS + // The EDS needs the CCV instance addresses so it can return the right + // CCV disclosures alongside the infrastructure contracts. + const ccvAddresses = verifications.map((v) => v.destAddress) + + // Derive a message ID for the EDS request. + // For now, we use the first 66 chars of encodedMessage as a stand-in; + // in practice this should be the keccak256 of the encoded message or + // extracted from decodable headers. + const messageIdForEds = stripHexPrefix(encodedMessage).slice(0, 64) + + this.logger.debug( + `CantonChain.generateUnsignedExecute: fetching EDS disclosures for ${ccvAddresses.length} CCVs`, + ) + const edsResult = await this.edsDisclosureProvider.fetchExecutionDisclosures( + messageIdForEds, + ccvAddresses, + ) + + // Step 3 — Build CCV inputs: pair each verification with its CCV contract ID + const ccvInputs = verifications.map((v) => { + const ccvDisclosure = edsResult.ccvs[v.destAddress] + if (!ccvDisclosure?.disclosedContract) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS did not return a disclosure for CCV at ${v.destAddress}`, + ) + } + return { + ccvCid: ccvDisclosure.disclosedContract.contractId, + verifierResults: stripHexPrefix(String(v.ccvData)), + } + }) + + // Step 4 — Extract CCV disclosed contracts + const ccvDisclosedContracts: DisclosedContract[] = verifications + .map((v) => edsResult.ccvs[v.destAddress]?.disclosedContract) + .filter((dc): dc is DisclosedContract => dc !== undefined) + + // Step 5 — Assemble the Execute choice argument. + // The `choiceContextData` from EDS is an opaque blob that the Canton + // runtime expects as part of the choice argument — it contains + // contract IDs for OffRamp, GlobalConfig, etc. + const contextData = + edsResult.choiceContext.choiceContextData != null && + typeof edsResult.choiceContext.choiceContextData === 'object' + ? (edsResult.choiceContext.choiceContextData as Record) + : {} + const choiceArgument: Record = { + ...contextData, + routerCid: acsDisclosures.perPartyRouter.contractId, + encodedMessage: stripHexPrefix(String(encodedMessage)), + tokenTransfer: null, + ccvInputs, + additionalRequiredCCVs: [], + } + + // Step 6 — Merge all disclosed contracts + const allDisclosed: DisclosedContract[] = [ + acsDisclosures.perPartyRouter, + acsDisclosures.ccipReceiver, + ...edsResult.choiceContext.disclosedContracts, + ...ccvDisclosedContracts, + ] + + // Step 7 — Build the ExerciseCommand + const exerciseCommand = { + ExerciseCommand: { + templateId: acsDisclosures.ccipReceiver.templateId, + contractId: acsDisclosures.ccipReceiver.contractId, + choice: 'Execute', + choiceArgument, + }, + } + + // Step 8 — Assemble JsCommands + const jsCommands: JsCommands = { + commands: [exerciseCommand], + commandId: `ccip-execute-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + actAs: [payer], + disclosedContracts: allDisclosed.map((dc) => ({ + templateId: dc.templateId, + contractId: dc.contractId, + createdEventBlob: dc.createdEventBlob, + synchronizerId: dc.synchronizerId, + })), + } + + this.logger.debug( + `CantonChain.generateUnsignedExecute: built command with ${allDisclosed.length} disclosed contracts`, + ) + + return { + family: ChainFamily.Canton, + commands: jsCommands, + } } /** - * {@inheritDoc Chain.executeReport} - * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + * Executes a CCIP message on Canton by: + * 1. Validating the wallet as a {@link CantonWallet}. + * 2. Building the unsigned command via {@link generateUnsignedExecute}. + * 3. Submitting the command to the Canton Ledger API. + * 4. Parsing the resulting transaction into a {@link CCIPExecution}. + * + * @throws {@link CCIPWalletInvalidError} if wallet is not a valid {@link CantonWallet} + * @throws {@link CCIPError} if the Ledger API submission or result parsing fails */ async execute(opts: Parameters[0]): Promise { - // TODO - throw new CCIPNotImplementedError('CantonChain.executeReport') + const { wallet } = opts + if (!isCantonWallet(wallet)) { + throw new CCIPWalletInvalidError(wallet) + } + + // Build the unsigned command + const unsigned = await this.generateUnsignedExecute({ + ...opts, + payer: wallet.party, + }) + + this.logger.debug('CantonChain.execute: submitting command to Ledger API') + + // Submit and wait for the full transaction (so we get events back) + const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) + const txRecord = response.transaction as Record + const updateId: string = typeof txRecord.updateId === 'string' ? txRecord.updateId : '' + const recordTime: string = typeof txRecord.recordTime === 'string' ? txRecord.recordTime : '' + + this.logger.debug(`CantonChain.execute: submitted, updateId=${updateId}`) + + // Parse execution receipt from the transaction events + const receipt = parseCantonExecutionReceipt(response.transaction, updateId) + const timestamp = recordTime + ? Math.floor(new Date(recordTime).getTime() / 1000) + : Math.floor(Date.now() / 1000) + + // Build a synthetic ChainLog — Canton doesn't have EVM-style logs, but the + // SDK contract expects a ChainLog in the CCIPExecution. + const log: ChainLog = { + topics: [], + index: 0, + address: '', + blockNumber: 0, + transactionHash: updateId, + data: response.transaction as Record, + } + + return { receipt, log, timestamp } } /** @@ -573,3 +766,114 @@ export class CantonChain extends Chain { return Chain.buildMessageForDest(message) } } + +// --------------------------------------------------------------------------- +// Module-private helpers +// --------------------------------------------------------------------------- + +/** + * Strip the `0x` prefix from a hex string. + * Canton / Daml expects hex values without the prefix. + */ +function stripHexPrefix(hex: string): string { + return hex.startsWith('0x') ? hex.slice(2) : hex +} + +/** + * Walk a Canton transaction response and extract an {@link ExecutionReceipt}. + * + * Canton transaction events are nested inside the `transaction` object + * returned by `submitAndWaitForTransaction`. The structure varies but + * generally contains arrays of `createdEvents` and `exercisedEvents`. + * We look for an event whose `templateId` contains + * `ExecutionStateChanged` and extract the relevant fields. + * + * If no matching event is found we return a minimal receipt with + * {@link ExecutionState.Success} (the command succeeded if we got this far). + */ +function parseCantonExecutionReceipt(transaction: unknown, updateId: string): ExecutionReceipt { + // Attempt to extract events from the transaction + const events = extractEventsFromTransaction(transaction) + + for (const event of events) { + const templateId = + typeof event === 'object' && event !== null && 'templateId' in event + ? String((event as Record).templateId) + : '' + + if (templateId.includes('ExecutionStateChanged')) { + const payload = + 'createArgument' in (event as Record) + ? ((event as Record).createArgument as Record) + : (event as Record) + + const msgId = payload['messageId'] + const seqNum = payload['sequenceNumber'] + const srcChain = payload['sourceChainSelector'] + const retData = payload['returnData'] + return { + messageId: typeof msgId === 'string' ? msgId : updateId, + sequenceNumber: BigInt( + typeof seqNum === 'string' || typeof seqNum === 'number' ? seqNum : 0, + ), + state: mapExecutionState(payload['state']), + sourceChainSelector: srcChain != null ? BigInt(srcChain as string | number) : undefined, + returnData: typeof retData === 'string' ? retData : undefined, + } + } + } + + // Fallback — the command completed successfully but we couldn't locate the + // specific ExecutionStateChanged event (e.g. different event format). + return { + messageId: updateId, + sequenceNumber: 0n, + state: ExecutionState.Success, + } +} + +/** + * Recursively extract event-like objects from a Canton transaction tree. + */ +function extractEventsFromTransaction(obj: unknown): unknown[] { + const results: unknown[] = [] + if (!obj || typeof obj !== 'object') return results + + const record = obj as Record + + // Direct event arrays + for (const key of ['createdEvents', 'exercisedEvents', 'events']) { + if (Array.isArray(record[key])) { + results.push(...(record[key] as unknown[])) + } + } + + // Nested transaction/eventsById + if (record['eventsById'] && typeof record['eventsById'] === 'object') { + results.push(...Object.values(record['eventsById'] as Record)) + } + + // Recurse into known wrapper keys + for (const key of ['transaction', 'rootEventIds', 'JsTransaction']) { + if (record[key] && typeof record[key] === 'object' && !Array.isArray(record[key])) { + results.push(...extractEventsFromTransaction(record[key])) + } + } + + return results +} + +/** + * Map a Canton execution state value to the SDK {@link ExecutionState}. + */ +function mapExecutionState(state: unknown): ExecutionState { + if (state === undefined || state === null) return ExecutionState.Success + + const s = typeof state === 'string' ? state.toLowerCase() : `${state as string | number}` + + if (s === 'success' || s === '2') return ExecutionState.Success + if (s === 'failed' || s === '3') return ExecutionState.Failed + if (s === 'inprogress' || s === 'in_progress' || s === '1') return ExecutionState.InProgress + + return ExecutionState.Success +} diff --git a/ccip-sdk/src/canton/types.ts b/ccip-sdk/src/canton/types.ts new file mode 100644 index 00000000..8042b0e4 --- /dev/null +++ b/ccip-sdk/src/canton/types.ts @@ -0,0 +1,35 @@ +import type { ChainFamily } from '../types.ts' +import type { JsCommands } from './client/index.ts' + +/** + * A Canton "wallet" identifies the acting party and optionally overrides the + * bearer token used for Ledger API authentication. + */ +export interface CantonWallet { + /** Daml party ID used for `actAs` in command submissions. */ + party: string +} + +/** + * Type-guard for {@link CantonWallet}. + */ +export function isCantonWallet(v: unknown): v is CantonWallet { + return ( + typeof v === 'object' && + v !== null && + 'party' in v && + typeof (v as CantonWallet).party === 'string' && + (v as CantonWallet).party.length > 0 + ) +} + +/** + * An unsigned Canton transaction wraps a `JsCommands` object ready to be + * submitted to the Canton Ledger API via `submitAndWait` / + * `submitAndWaitForTransaction`. + */ +export interface UnsignedCantonTx { + family: typeof ChainFamily.Canton + /** The Canton command payload ready for submission. */ + commands: JsCommands +} diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index 6eeb87e3..fd388b2b 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -3,6 +3,7 @@ import type { PickDeep, SetOptional } from 'type-fest' import { type LaneLatencyResponse, CCIPAPIClient } from './api/index.ts' import type { UnsignedAptosTx } from './aptos/types.ts' +import type { UnsignedCantonTx } from './canton/types.ts' import { getOnchainCommitReport } from './commits.ts' import { CCIPApiClientNotAvailableError, @@ -440,7 +441,7 @@ export type UnsignedTx = { [ChainFamily.Aptos]: UnsignedAptosTx [ChainFamily.TON]: UnsignedTONTx [ChainFamily.Sui]: UnsignedSuiTx - [ChainFamily.Canton]: never // TODO + [ChainFamily.Canton]: UnsignedCantonTx [ChainFamily.Unknown]: never } From 5e2a00992d64db4fbc23a509e0d3ce8a1455d9bb Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:49:04 +0100 Subject: [PATCH 05/17] add canton config for chain class --- ccip-sdk/src/canton/client/client.ts | 10 +++---- .../src/canton/explicit-disclosures/acs.ts | 2 +- ccip-sdk/src/canton/index.ts | 27 +++++++++++++------ ccip-sdk/src/chain.ts | 26 +++++++++++++++++- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index ce39b761..bd6d4e39 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -27,8 +27,8 @@ export type GetConnectedSynchronizersResponse = export interface CantonClientConfig { /** Base URL of the Canton JSON Ledger API (e.g., http://localhost:7575) */ baseUrl: string - /** Optional bearer token for authentication */ - token?: string + /** JWT for authentication */ + jwt: string /** Request timeout in milliseconds */ timeout?: number } @@ -38,7 +38,7 @@ export interface CantonClientConfig { */ export function createCantonClient(config: CantonClientConfig) { const baseUrl = config.baseUrl.replace(/\/$/, '') - const headers = buildHeaders(config.token) + const headers = buildHeaders(config.jwt) const timeoutMs = config.timeout ?? 30_000 return { @@ -250,9 +250,9 @@ class CantonApiError extends CCIPError { } } -function buildHeaders(token?: string): Record { +function buildHeaders(jwt?: string): Record { const headers: Record = { 'Content-Type': 'application/json' } - if (token) headers['Authorization'] = `Bearer ${token}` + if (jwt) headers['Authorization'] = `Bearer ${jwt}` return headers } diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts index cdc58c94..97a8ec00 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -216,7 +216,7 @@ export class AcsDisclosureProvider { jwt: string, config: AcsDisclosureConfig, ): AcsDisclosureProvider { - const client = createCantonClient({ baseUrl: ledgerApiUrl, token: jwt }) + const client = createCantonClient({ baseUrl: ledgerApiUrl, jwt }) return new AcsDisclosureProvider(client, config) } diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 316a16d3..9ed50865 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -168,7 +168,14 @@ export class CantonChain extends Chain { * @throws {@link CCIPNotImplementedError} if Canton network detection is not yet implemented */ static async fromUrl(url: string, ctx?: ChainContext): Promise { - const client = createCantonClient({ baseUrl: url }) + // Check that ctx has the necessary cantonConfig + if (!ctx || !ctx.cantonConfig || typeof ctx.cantonConfig.jwt !== 'string') { + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + 'CantonChain.fromUrl: ctx.cantonConfig is required', + ) + } + const client = createCantonClient({ baseUrl: url, jwt: ctx.cantonConfig.jwt }) try { const alive = await client.isAlive() if (!alive) throw new CCIPNotImplementedError('Canton Ledger JSON API is not alive') @@ -179,13 +186,17 @@ export class CantonChain extends Chain { `Failed to connect to Canton Ledger API ${url}: ${message}`, ) } - const defaultACSConfig: AcsDisclosureConfig = { - party: 'ccip-sdk-party', - } - // TODO: These need to come from input - const acsDisclosureProvider = new AcsDisclosureProvider(client, defaultACSConfig) - const edsDisclosureProvider = new EdsDisclosureProvider({ edsBaseUrl: url }) - return CantonChain.fromClient(client, acsDisclosureProvider, edsDisclosureProvider, '', ctx) + const acsDisclosureProvider = new AcsDisclosureProvider(client, { + party: ctx.cantonConfig.party, + }) + const edsDisclosureProvider = new EdsDisclosureProvider({ edsBaseUrl: ctx.cantonConfig.edsUrl }) + return CantonChain.fromClient( + client, + acsDisclosureProvider, + edsDisclosureProvider, + ctx.cantonConfig.indexerUrl ?? '', + ctx, + ) } /** diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index fd388b2b..b32d21e9 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -131,7 +131,7 @@ export type ChainContext = WithLogger & { * Default: DEFAULT_API_RETRY_CONFIG */ apiRetryConfig?: ApiRetryConfig -} +} & WithCantonConfig /** * Configuration for retry behavior with exponential backoff. @@ -161,6 +161,30 @@ export const DEFAULT_API_RETRY_CONFIG: Required = { respectRetryAfterHint: true, } +/** + * An options object which may have Canton configuration + */ +export type WithCantonConfig = { + cantonConfig?: CantonConfig +} + +/** + * Configuration for connecting to a Canton Ledger API and fetch CCIP disclosures. + */ +export type CantonConfig = { + /** Party identifier for the Canton Ledger API. */ + party: string + + /** JSON Web Token for authentication with the Canton Ledger API. */ + jwt: string + + /** Base URL for the EDS (Explicit Disclosure Service) API. */ + edsUrl: string + + /** Optional base URL for a transaction indexer to fetch CCV verifications; if not provided, default URL will be used. */ + indexerUrl?: string +} + /** * Filter options for getLogs queries across chains. */ From 529f4df95882965caf9f8b0ef540a813644a4811 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:22:41 +0100 Subject: [PATCH 06/17] fix submission --- ccip-sdk/src/canton/client/client.ts | 4 ++++ ccip-sdk/src/canton/index.ts | 31 ++++++++-------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index bd6d4e39..05d826a0 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -300,6 +300,10 @@ async function ledgerRequest( response.status, ) } + const contentLength = response.headers.get('content-length') + if (response.status === 204 || contentLength === '0') { + return undefined as T + } return response.json() as Promise } diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 9ed50865..9c16f38c 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -427,7 +427,6 @@ export class CantonChain extends Chain { verifications: Pick[] } - this.logger.debug('CantonChain.generateUnsignedExecute: fetching ACS disclosures') // Step 1 — Fetch same-party disclosures (PerPartyRouter + CCIPReceiver) const acsDisclosures = await this.acsDisclosureProvider.fetchExecutionDisclosures() @@ -442,9 +441,6 @@ export class CantonChain extends Chain { // extracted from decodable headers. const messageIdForEds = stripHexPrefix(encodedMessage).slice(0, 64) - this.logger.debug( - `CantonChain.generateUnsignedExecute: fetching EDS disclosures for ${ccvAddresses.length} CCVs`, - ) const edsResult = await this.edsDisclosureProvider.fetchExecutionDisclosures( messageIdForEds, ccvAddresses, @@ -459,10 +455,12 @@ export class CantonChain extends Chain { `EDS did not return a disclosure for CCV at ${v.destAddress}`, ) } - return { + const entry = { ccvCid: ccvDisclosure.disclosedContract.contractId, verifierResults: stripHexPrefix(String(v.ccvData)), + ccvExtraContext: { values: {} }, } + return entry }) // Step 4 — Extract CCV disclosed contracts @@ -472,15 +470,10 @@ export class CantonChain extends Chain { // Step 5 — Assemble the Execute choice argument. // The `choiceContextData` from EDS is an opaque blob that the Canton - // runtime expects as part of the choice argument — it contains - // contract IDs for OffRamp, GlobalConfig, etc. - const contextData = - edsResult.choiceContext.choiceContextData != null && - typeof edsResult.choiceContext.choiceContextData === 'object' - ? (edsResult.choiceContext.choiceContextData as Record) - : {} + // runtime expects under the `context` field of the Execute choice — it + // contains contract IDs for OffRamp, GlobalConfig, etc. const choiceArgument: Record = { - ...contextData, + context: edsResult.choiceContext.choiceContextData ?? {}, routerCid: acsDisclosures.perPartyRouter.contractId, encodedMessage: stripHexPrefix(String(encodedMessage)), tokenTransfer: null, @@ -519,10 +512,6 @@ export class CantonChain extends Chain { })), } - this.logger.debug( - `CantonChain.generateUnsignedExecute: built command with ${allDisclosed.length} disclosed contracts`, - ) - return { family: ChainFamily.Canton, commands: jsCommands, @@ -551,18 +540,16 @@ export class CantonChain extends Chain { payer: wallet.party, }) - this.logger.debug('CantonChain.execute: submitting command to Ledger API') - // Submit and wait for the full transaction (so we get events back) const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) + const txRecord = response.transaction as Record const updateId: string = typeof txRecord.updateId === 'string' ? txRecord.updateId : '' const recordTime: string = typeof txRecord.recordTime === 'string' ? txRecord.recordTime : '' - this.logger.debug(`CantonChain.execute: submitted, updateId=${updateId}`) - // Parse execution receipt from the transaction events const receipt = parseCantonExecutionReceipt(response.transaction, updateId) + const timestamp = recordTime ? Math.floor(new Date(recordTime).getTime() / 1000) : Math.floor(Date.now() / 1000) @@ -583,7 +570,7 @@ export class CantonChain extends Chain { /** * Fetches CCV verification results for a CCIP message from the Canton indexer. - * @param opts - Options including the CCIP request containing the message ID to query. + * @param opts - Options that should only include the CCIP request with the message ID to query. * @returns CCIPVerifications with verification policy and individual verifier results. */ override async getVerifications( From 85e56ba3070ea24ed9629836be105461ecbcf84f Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:30:16 +0100 Subject: [PATCH 07/17] fix lint --- ccip-sdk/src/canton/client/client.ts | 22 +++++++- .../canton/client/generated/ledger-api.d.ts | 1 + ccip-sdk/src/canton/index.ts | 52 +++++++++---------- 3 files changed, 47 insertions(+), 28 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index 05d826a0..79612da6 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -2,22 +2,36 @@ import type { components } from './generated/ledger-api.ts' import { CCIPError } from '../../errors/CCIPError.ts' import { CCIPErrorCode } from '../../errors/codes.ts' -// Re-export useful types from the generated schema +/** Commands to submit to the ledger */ export type JsCommands = components['schemas']['JsCommands'] +/** A single command */ export type Command = components['schemas']['Command'] +/** Response from submit-and-wait operation */ export type SubmitAndWaitResponse = components['schemas']['SubmitAndWaitResponse'] +/** Full transaction response including all events */ export type JsSubmitAndWaitForTransactionResponse = components['schemas']['JsSubmitAndWaitForTransactionResponse'] +/** Request to get active contracts */ export type GetActiveContractsRequest = components['schemas']['GetActiveContractsRequest'] +/** Response containing active contracts */ export type JsGetActiveContractsResponse = components['schemas']['JsGetActiveContractsResponse'] +/** An active contract on the ledger */ export type JsActiveContract = components['schemas']['JsActiveContract'] +/** An event created by a contract */ export type CreatedEvent = components['schemas']['CreatedEvent'] +/** Error returned by Canton API */ export type JsCantonError = components['schemas']['JsCantonError'] +/** Filter for transactions */ export type TransactionFilter = components['schemas']['TransactionFilter'] +/** Format for events in responses */ export type EventFormat = components['schemas']['EventFormat'] +/** Filter for templates */ export type TemplateFilter = components['schemas']['TemplateFilter'] +/** Wildcard filter for matching patterns */ export type WildcardFilter = components['schemas']['WildcardFilter'] +/** Information about a connected synchronizer */ export type ConnectedSynchronizer = components['schemas']['ConnectedSynchronizer'] +/** Response containing connected synchronizers */ export type GetConnectedSynchronizersResponse = components['schemas']['GetConnectedSynchronizersResponse'] @@ -223,6 +237,12 @@ export type CantonClient = ReturnType class CantonApiError extends CCIPError { override readonly name = 'CantonApiError' + /** + * Creates a new CantonApiError instance + * @param message - The error message + * @param error - The underlying error object or details + * @param statusCode - Optional HTTP status code + */ constructor(message: string, error: unknown, statusCode?: number) { const context: Record = {} let fullMessage = message diff --git a/ccip-sdk/src/canton/client/generated/ledger-api.d.ts b/ccip-sdk/src/canton/client/generated/ledger-api.d.ts index b1e6d785..b880c50c 100644 --- a/ccip-sdk/src/canton/client/generated/ledger-api.d.ts +++ b/ccip-sdk/src/canton/client/generated/ledger-api.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable */ /** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 9c16f38c..d1194948 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -42,7 +42,7 @@ import { } from '../types.ts' import { networkInfo } from '../utils.ts' import { type CantonClient, type JsCommands, createCantonClient } from './client/index.ts' -import { type AcsDisclosureConfig, AcsDisclosureProvider } from './explicit-disclosures/acs.ts' +import { AcsDisclosureProvider } from './explicit-disclosures/acs.ts' import type { DisclosedContract } from './explicit-disclosures/types.ts' import { CCV_INDEXER_URL } from '../evm/const.ts' import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' @@ -203,12 +203,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getBlockTimestamp} * @throws {@link CCIPNotImplementedError} for numeric blocks (Canton ledger uses offsets, not block numbers) */ - async getBlockTimestamp(block: number | 'finalized'): Promise { - if (typeof block !== 'number') { - // For 'finalized', return current time as best approximation - return Math.floor(Date.now() / 1000) - } - // Canton ledger uses offset-based ordering, not block timestamps + getBlockTimestamp(block: number | 'finalized'): Promise { throw new CCIPNotImplementedError( `CantonChain.getBlockTimestamp: block ${block} — Canton uses ledger offsets, not block numbers`, ) @@ -218,7 +213,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTransaction} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTransaction(_hash: string): Promise { + getTransaction(_hash: string): Promise { throw new CCIPNotImplementedError('CantonChain.getTransaction') } @@ -227,7 +222,7 @@ export class CantonChain extends Chain { * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ // eslint-disable-next-line require-yield - async *getLogs(_opts: LogFilter): AsyncIterableIterator { + getLogs(_opts: LogFilter): AsyncIterableIterator { throw new CCIPNotImplementedError('CantonChain.getLogs') } @@ -251,7 +246,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.typeAndVersion} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async typeAndVersion( + typeAndVersion( _address: string, ): Promise<[type: string, version: string, typeAndVersion: string, suffix?: string]> { throw new CCIPNotImplementedError('CantonChain.typeAndVersion') @@ -261,7 +256,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getRouterForOnRamp} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { + getRouterForOnRamp(_onRamp: string, _destChainSelector: bigint): Promise { // TODO: Contract discovery can come from EDS. throw new CCIPNotImplementedError('CantonChain.getRouterForOnRamp') } @@ -270,7 +265,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getRouterForOffRamp} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + getRouterForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { throw new CCIPNotImplementedError('CantonChain.getRouterForOffRamp') } @@ -278,7 +273,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getNativeTokenForRouter} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getNativeTokenForRouter(_router: string): Promise { + getNativeTokenForRouter(_router: string): Promise { throw new CCIPNotImplementedError('CantonChain.getNativeTokenForRouter') } @@ -286,7 +281,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getOffRampsForRouter} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { + getOffRampsForRouter(_router: string, _sourceChainSelector: bigint): Promise { throw new CCIPNotImplementedError('CantonChain.getOffRampsForRouter') } @@ -294,7 +289,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getOnRampForRouter} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { + getOnRampForRouter(_router: string, _destChainSelector: bigint): Promise { throw new CCIPNotImplementedError('CantonChain.getOnRampForRouter') } @@ -302,10 +297,13 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getOnRampsForOffRamp} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getOnRampsForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { + getOnRampsForOffRamp(_offRamp: string, _sourceChainSelector: bigint): Promise { throw new CCIPNotImplementedError('CantonChain.getOnRampsForOffRamp') } + /** + * {@inheritDoc Chain.getCommitStoreForOffRamp} + */ async getCommitStoreForOffRamp(offRamp: string): Promise { return Promise.resolve(offRamp) } @@ -314,7 +312,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenForTokenPool} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenForTokenPool(_tokenPool: string): Promise { + getTokenForTokenPool(_tokenPool: string): Promise { throw new CCIPNotImplementedError('CantonChain.getTokenForTokenPool') } @@ -322,7 +320,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenInfo} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { + getTokenInfo(_token: string): Promise<{ symbol: string; decimals: number }> { throw new CCIPNotImplementedError('CantonChain.getTokenInfo') } @@ -330,7 +328,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getBalance} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getBalance(_opts: GetBalanceOpts): Promise { + getBalance(_opts: GetBalanceOpts): Promise { throw new CCIPNotImplementedError('CantonChain.getBalance') } @@ -338,7 +336,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenAdminRegistryFor} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenAdminRegistryFor(_address: string): Promise { + getTokenAdminRegistryFor(_address: string): Promise { throw new CCIPNotImplementedError('CantonChain.getTokenAdminRegistryFor') } @@ -346,7 +344,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getFee} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getFee(_opts: Parameters[0]): Promise { + getFee(_opts: Parameters[0]): Promise { throw new CCIPNotImplementedError('CantonChain.getFee') } @@ -364,7 +362,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.sendMessage} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async sendMessage(_opts: Parameters[0]): Promise { + sendMessage(_opts: Parameters[0]): Promise { throw new CCIPNotImplementedError('CantonChain.sendMessage') } @@ -639,7 +637,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getSupportedTokens} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getSupportedTokens(_address: string, _opts?: { page?: number }): Promise { + getSupportedTokens(_address: string, _opts?: { page?: number }): Promise { throw new CCIPNotImplementedError('CantonChain.getSupportedTokens') } @@ -647,7 +645,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getRegistryTokenConfig} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getRegistryTokenConfig(_registry: string, _token: string): Promise { + getRegistryTokenConfig(_registry: string, _token: string): Promise { throw new CCIPNotImplementedError('CantonChain.getRegistryTokenConfig') } @@ -655,7 +653,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenPoolConfig} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenPoolConfig(_tokenPool: string): Promise { + getTokenPoolConfig(_tokenPool: string): Promise { throw new CCIPNotImplementedError('CantonChain.getTokenPoolConfig') } @@ -663,7 +661,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getTokenPoolRemotes} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getTokenPoolRemotes( + getTokenPoolRemotes( _tokenPool: string, _remoteChainSelector?: bigint, ): Promise> { @@ -674,7 +672,7 @@ export class CantonChain extends Chain { * {@inheritDoc Chain.getFeeTokens} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - async getFeeTokens(_router: string): Promise> { + getFeeTokens(_router: string): Promise> { throw new CCIPNotImplementedError('CantonChain.getFeeTokens') } From 4ece5e798dea298de008559465f8bb29943330e9 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:41:38 +0100 Subject: [PATCH 08/17] refactor acs querying --- .../canton/explicit-disclosures/acs.test.ts | 142 ++++++++++++++++++ .../src/canton/explicit-disclosures/acs.ts | 108 +++++++++---- ccip-sdk/src/canton/index.ts | 1 + 3 files changed, 219 insertions(+), 32 deletions(-) create mode 100644 ccip-sdk/src/canton/explicit-disclosures/acs.test.ts diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts new file mode 100644 index 00000000..8087811f --- /dev/null +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts @@ -0,0 +1,142 @@ +import assert from 'node:assert/strict' +import { describe, it } from 'node:test' + +import { AcsDisclosureProvider } from './acs.ts' +import type { CantonClient, JsGetActiveContractsResponse } from '../client/index.ts' + +// --------------------------------------------------------------------------- +// Fixtures +// --------------------------------------------------------------------------- + +const PARTY = 'party1::aabbcc' +const SYNC_ID = 'synchronizer::001122' + +/** + * Build a minimal ACS response entry for a given template / contract. + * `templateId` must use the ledger format: `::`. + */ +function makeAcsEntry( + templateId: string, + contractId: string, + createdEventBlob: string, + signatory: string, + instanceId?: string, +): JsGetActiveContractsResponse { + return { + contractEntry: { + JsActiveContract: { + synchronizerId: SYNC_ID, + createdEvent: { + templateId, + contractId, + createdEventBlob, + signatories: [signatory], + createArgument: instanceId ? { instanceId } : {}, + // remaining CreatedEvent fields — not used by fetchRichSnapshot + observers: [], + witnesses: [], + offset: 1, + nodeId: 0, + packageName: '', + interfaceViews: [], + agreementText: '', + }, + }, + }, + } as unknown as JsGetActiveContractsResponse +} + +const ROUTER_CONTRACT_ID = 'router-cid-001' +const ROUTER_BLOB = 'router-blob' +const ROUTER_TEMPLATE_ID = 'pkg-router:CCIP.PerPartyRouter:PerPartyRouter' + +const RECEIVER_CONTRACT_ID = 'receiver-cid-002' +const RECEIVER_BLOB = 'receiver-blob' +const RECEIVER_TEMPLATE_ID = 'pkg-receiver:CCIP.CCIPReceiver:CCIPReceiver' + +/** ACS snapshot returned by the stub for the default two-contract scenario */ +const DEFAULT_ACS: JsGetActiveContractsResponse[] = [ + makeAcsEntry(ROUTER_TEMPLATE_ID, ROUTER_CONTRACT_ID, ROUTER_BLOB, PARTY), + makeAcsEntry(RECEIVER_TEMPLATE_ID, RECEIVER_CONTRACT_ID, RECEIVER_BLOB, PARTY), +] + +// --------------------------------------------------------------------------- +// Stub factory +// --------------------------------------------------------------------------- + +/** + * Build a `CantonClient` stub whose `getLedgerEnd` and `getActiveContracts` + * return the provided fixture data. All other methods are left unimplemented. + */ +function makeStubClient(acsEntries: JsGetActiveContractsResponse[]): CantonClient { + return { + getLedgerEnd: async () => ({ offset: 42 }), + getActiveContracts: async () => acsEntries, + } as unknown as CantonClient +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('canton/acs', () => { + it('fetchExecutionDisclosures returns PerPartyRouter and CCIPReceiver by signatory', async () => { + const provider = new AcsDisclosureProvider(makeStubClient(DEFAULT_ACS), { party: PARTY }) + + const disclosures = await provider.fetchExecutionDisclosures() + + assert.equal(disclosures.perPartyRouter.contractId, ROUTER_CONTRACT_ID) + assert.equal(disclosures.perPartyRouter.templateId, ROUTER_TEMPLATE_ID) + assert.equal(disclosures.perPartyRouter.createdEventBlob, ROUTER_BLOB) + assert.equal(disclosures.perPartyRouter.synchronizerId, SYNC_ID) + + assert.equal(disclosures.ccipReceiver.contractId, RECEIVER_CONTRACT_ID) + assert.equal(disclosures.ccipReceiver.templateId, RECEIVER_TEMPLATE_ID) + assert.equal(disclosures.ccipReceiver.createdEventBlob, RECEIVER_BLOB) + assert.equal(disclosures.ccipReceiver.synchronizerId, SYNC_ID) + }) + + it('fetchExecutionDisclosures resolves the receiver by contract ID when receiverCid is provided', async () => { + // An extra "other" contract with the same template — ensures lookup is by ID, not signatory + const OTHER_CID = 'other-receiver-cid-999' + const OTHER_BLOB = 'other-receiver-blob' + const entries = [ + ...DEFAULT_ACS, + makeAcsEntry(RECEIVER_TEMPLATE_ID, OTHER_CID, OTHER_BLOB, 'other-party::ff'), + ] + + const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) + + const disclosures = await provider.fetchExecutionDisclosures(OTHER_CID) + + // Router still resolved by signatory + assert.equal(disclosures.perPartyRouter.contractId, ROUTER_CONTRACT_ID) + + // Receiver resolved by explicit contract ID, not by signatory match + assert.equal(disclosures.ccipReceiver.contractId, OTHER_CID) + assert.equal(disclosures.ccipReceiver.createdEventBlob, OTHER_BLOB) + assert.equal(disclosures.ccipReceiver.synchronizerId, SYNC_ID) + }) + + it('fetchExecutionDisclosures throws when no matching PerPartyRouter is found', async () => { + const entries = [makeAcsEntry(RECEIVER_TEMPLATE_ID, RECEIVER_CONTRACT_ID, RECEIVER_BLOB, PARTY)] + + const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) + + await assert.rejects( + () => provider.fetchExecutionDisclosures(), + /PerPartyRouter/, + 'should throw mentioning PerPartyRouter', + ) + }) + + it('fetchExecutionDisclosures throws when receiverCid does not match any active contract', async () => { + const provider = new AcsDisclosureProvider(makeStubClient(DEFAULT_ACS), { party: PARTY }) + + await assert.rejects( + () => provider.fetchExecutionDisclosures('nonexistent-cid'), + /nonexistent-cid/, + 'should throw mentioning the missing contract ID', + ) + }) +}) diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts index 97a8ec00..16487bfe 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -34,39 +34,48 @@ function extractInstanceId(createArgument: unknown): string | null { } /** - * Build a wildcard `EventFormat` that returns all contracts belonging to `party`, - * including the opaque `createdEventBlob` needed for disclosure. + * Metadata for each CCIP contract type needed for ACS filtering: + * - `templateId`: package-name reference used directly in the `TemplateFilter` so the + * server only returns contracts of that exact template (no client-side scan). + * - `moduleEntity`: the `ModuleName:EntityName` suffix extracted from the full template ID + * string returned by the ledger, used to key the result map. */ -function buildWildcardEventFormat(party: string): EventFormat { +const CCIP_TEMPLATES = { + perPartyRouter: { + templateId: '#ccip-perpartyrouter:CCIP.PerPartyRouter:PerPartyRouter', + moduleEntity: 'CCIP.PerPartyRouter:PerPartyRouter', + }, + ccipReceiver: { + templateId: '#ccip-receiver:CCIP.CCIPReceiver:CCIPReceiver', + moduleEntity: 'CCIP.CCIPReceiver:CCIPReceiver', + }, +} as const + +type CcipContractType = keyof typeof CCIP_TEMPLATES + +/** + * Build a targeted `EventFormat` that requests only the specific CCIP contract + * templates needed, including the `createdEventBlob` required for disclosure. + * Using explicit `TemplateFilter`s instead of a wildcard avoids pulling every + * active contract for the party over the wire. + */ +function buildTargetedEventFormat(party: string): EventFormat { return { filtersByParty: { [party]: { - cumulative: [ - { - identifierFilter: { - WildcardFilter: { - value: { includeCreatedEventBlob: true }, - }, + cumulative: Object.values(CCIP_TEMPLATES).map(({ templateId }) => ({ + identifierFilter: { + TemplateFilter: { + value: { templateId, includeCreatedEventBlob: true }, }, }, - ], + })), }, }, verbose: true, } } -/** - * The `ModuleName:EntityName` suffix used to identify each CCIP contract type - * without requiring a specific package ID. - */ -const CCIP_MODULE_ENTITIES = { - perPartyRouter: 'CCIP.PerPartyRouter:PerPartyRouter', - ccipReceiver: 'CCIP.CCIPReceiver:CCIPReceiver', -} as const - -type CcipContractType = keyof typeof CCIP_MODULE_ENTITIES - /** * Internal per-contract entry in the ACS snapshot, enriched with instance * address components for later matching. @@ -81,7 +90,7 @@ interface RichContractMatch { } /** - * Query the ACS once with a wildcard filter and build a lookup map keyed by + * Query the ACS once with targeted template filters and build a lookup map keyed by * `"ModuleName:EntityName"`, preserving all fields needed for instance-address * matching. */ @@ -92,12 +101,11 @@ async function fetchRichSnapshot( const { offset } = await client.getLedgerEnd() const request: GetActiveContractsRequest = { - eventFormat: buildWildcardEventFormat(party), + eventFormat: buildTargetedEventFormat(party), verbose: false, activeAtOffset: offset, } - // Note: this may be inefficient and if the party has many active contracts. const responses = await client.getActiveContracts(request) const byModuleEntity = new Map() @@ -129,6 +137,39 @@ async function fetchRichSnapshot( return byModuleEntity } +/** + * From a pre-fetched ACS snapshot, return the contract whose `contractId` matches + * `cid`, regardless of template type. + * + * Used when the caller already knows the exact contract ID (e.g. a `CCIPReceiver` + * whose CID was persisted at deployment time) and template identity is irrelevant. + * + * @throws `CCIPError(CANTON_API_ERROR)` if no matching contract is found. + */ +function pickByContractId( + snapshot: Map, + cid: string, +): DisclosedContract { + for (const contracts of snapshot.values()) { + for (const c of contracts) { + if (c.contractId === cid) { + return { + templateId: c.templateId, + contractId: c.contractId, + createdEventBlob: c.createdEventBlob, + synchronizerId: c.synchronizerId, + } + } + } + } + + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton ACS: no active contract found with contractId "${cid}". ` + + `Verify the contract ID is correct and the contract is active.`, + ) +} + /** * From a pre-fetched ACS snapshot, return the single active contract of the given type * whose signatory matches `party`. @@ -143,7 +184,7 @@ function pickBySignatory( contractType: CcipContractType, party: string, ): DisclosedContract { - const moduleEntity = CCIP_MODULE_ENTITIES[contractType] + const { moduleEntity } = CCIP_TEMPLATES[contractType] const candidates = snapshot.get(moduleEntity) ?? [] for (const c of candidates) { @@ -222,17 +263,20 @@ export class AcsDisclosureProvider { /** * Fetch all contracts that must be disclosed for a `ccipExecute` command. + * + * @param receiverCid - When provided, the `CCIPReceiver` disclosure is resolved + * by contract ID rather than by signatory, making the lookup independent of + * the contract's template type. */ - async fetchExecutionDisclosures(): Promise { + async fetchExecutionDisclosures(receiverCid?: string): Promise { const snapshot = await fetchRichSnapshot(this.client, this.config.party) - const existingRouter = pickBySignatory(snapshot, 'perPartyRouter', this.config.party) - const existingReceiver = pickBySignatory(snapshot, 'ccipReceiver', this.config.party) + const perPartyRouter = pickBySignatory(snapshot, 'perPartyRouter', this.config.party) + const ccipReceiver = receiverCid + ? pickByContractId(snapshot, receiverCid) + : pickBySignatory(snapshot, 'ccipReceiver', this.config.party) - return { - perPartyRouter: existingRouter, - ccipReceiver: existingReceiver, - } + return { perPartyRouter, ccipReceiver } } /** diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index d1194948..5a108bed 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -426,6 +426,7 @@ export class CantonChain extends Chain { } // Step 1 — Fetch same-party disclosures (PerPartyRouter + CCIPReceiver) + // TODO: This should include receiverCid when provided. We need to figure out how to get that from the input or opts. const acsDisclosures = await this.acsDisclosureProvider.fetchExecutionDisclosures() // Step 2 — Fetch cross-party disclosures from EDS From 91710642b0668e9675ade3d24a1d7a2661127ce6 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Wed, 11 Mar 2026 15:46:40 +0100 Subject: [PATCH 09/17] add build artifacts --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- package-lock.json | 34 +++------------------------------- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 35a1364f..d4a20b27 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -11,7 +11,7 @@ import { Format } from './commands/index.ts' util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.3.1-d5fea83' +const VERSION = '1.1.1-f915d74' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 739b8ec4..5eb7a6eb 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -60,7 +60,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.3.1-d5fea83' +export const SDK_VERSION = '1.1.1-f915d74' // generate:end /** SDK telemetry header name */ diff --git a/package-lock.json b/package-lock.json index 7ec05277..bd4a56ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ }, "ccip-api-ref": { "name": "@chainlink/ccip-api-ref", - "version": "1.3.1", + "version": "1.1.1", "dependencies": { "@chainlink/ccip-sdk": "*", "@chainlink/design-system": "^0.2.8", @@ -386,6 +386,7 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.49.2.tgz", "integrity": "sha512-y1IOpG6OSmTpGg/CT0YBb/EAhR2nsC18QWp9Jy8HO9iGySpcwaTvs5kHa17daP3BMTwWyaX9/1tDTDQshZzXdg==", "license": "MIT", + "peer": true, "dependencies": { "@algolia/client-common": "5.49.2", "@algolia/requester-browser-xhr": "5.49.2", @@ -13226,6 +13227,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -29726,36 +29728,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", From ff2bd2ac41641b3f9b2d6468f2e3138e82179c49 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:19:24 +0100 Subject: [PATCH 10/17] add canton send --- ccip-sdk/src/canton/client/client.ts | 60 ++- ccip-sdk/src/canton/events.ts | 398 +++++++++++++++ .../src/canton/explicit-disclosures/acs.ts | 73 ++- ccip-sdk/src/canton/index.ts | 464 +++++++++++++----- ccip-sdk/src/canton/token-metadata/client.ts | 174 +++++++ .../src/canton/transfer-instruction/client.ts | 186 +++++++ ccip-sdk/src/canton/types.ts | 77 +++ ccip-sdk/src/chain.ts | 6 + ccip-sdk/src/extra-args.ts | 2 + 9 files changed, 1287 insertions(+), 153 deletions(-) create mode 100644 ccip-sdk/src/canton/events.ts create mode 100644 ccip-sdk/src/canton/token-metadata/client.ts create mode 100644 ccip-sdk/src/canton/transfer-instruction/client.ts diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index 79612da6..a259e2cd 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -61,7 +61,7 @@ export function createCantonClient(config: CantonClientConfig) { * @returns The update ID and completion offset */ async submitAndWait(commands: JsCommands): Promise { - return ledgerPost( + return post( baseUrl, '/v2/commands/submit-and-wait', headers, @@ -78,7 +78,7 @@ export function createCantonClient(config: CantonClientConfig) { commands: JsCommands, eventFormat?: EventFormat, ): Promise { - return ledgerPost( + return post( baseUrl, '/v2/commands/submit-and-wait-for-transaction', headers, @@ -97,7 +97,7 @@ export function createCantonClient(config: CantonClientConfig) { ): Promise { const queryParams = options?.limit !== undefined ? { limit: String(options.limit) } : undefined - return ledgerPost( + return post( baseUrl, '/v2/state/active-contracts', headers, @@ -111,7 +111,7 @@ export function createCantonClient(config: CantonClientConfig) { * Get the current ledger end offset */ async getLedgerEnd(): Promise<{ offset: number }> { - const data = await ledgerGet<{ offset?: number }>( + const data = await get<{ offset?: number }>( baseUrl, '/v2/state/ledger-end', headers, @@ -125,7 +125,7 @@ export function createCantonClient(config: CantonClientConfig) { */ async listParties(options?: { filterParty?: string }) { const queryParams = options?.filterParty ? { 'filter-party': options.filterParty } : undefined - const data = await ledgerGet<{ partyDetails?: unknown[] }>( + const data = await get<{ partyDetails?: unknown[] }>( baseUrl, '/v2/parties', headers, @@ -139,7 +139,7 @@ export function createCantonClient(config: CantonClientConfig) { * Get the participant ID */ async getParticipantId(): Promise { - const data = await ledgerGet<{ participantId?: string }>( + const data = await get<{ participantId?: string }>( baseUrl, '/v2/parties/participant-id', headers, @@ -152,7 +152,7 @@ export function createCantonClient(config: CantonClientConfig) { * Get the list of synchronizers the participant is currently connected to */ async getConnectedSynchronizers(): Promise { - const data = await ledgerGet<{ connectedSynchronizers?: ConnectedSynchronizer[] }>( + const data = await get<{ connectedSynchronizers?: ConnectedSynchronizer[] }>( baseUrl, '/v2/state/connected-synchronizers', headers, @@ -166,10 +166,12 @@ export function createCantonClient(config: CantonClientConfig) { */ async isAlive(): Promise { try { - await ledgerRequest('GET', baseUrl, '/livez', headers, timeoutMs) + await request('GET', baseUrl, '/livez', headers, timeoutMs) return true - } catch { - return false + } catch (e) { + console.log(`Ledger API is not alive at ${baseUrl}/livez:`, e) + throw new CantonApiError('Ledger API is not alive', e) + // return false } }, @@ -178,7 +180,7 @@ export function createCantonClient(config: CantonClientConfig) { */ async isReady(): Promise { try { - await ledgerRequest('GET', baseUrl, '/readyz', headers, timeoutMs) + await request('GET', baseUrl, '/readyz', headers, timeoutMs) return true } catch { return false @@ -192,7 +194,7 @@ export function createCantonClient(config: CantonClientConfig) { * @returns The full update with all events */ async getUpdateById(updateId: string, party: string): Promise { - return ledgerPost(baseUrl, '/v2/updates/update-by-id', headers, timeoutMs, { + return post(baseUrl, '/v2/updates/update-by-id', headers, timeoutMs, { updateId, updateFormat: { includeTransactions: { @@ -284,7 +286,7 @@ async function parseErrorBody(response: Response): Promise { } } -async function ledgerRequest( +async function request( method: 'GET' | 'POST', baseUrl: string, path: string, @@ -313,6 +315,7 @@ async function ledgerRequest( } finally { clearTimeout(timer) } + if (!response.ok) { throw new CantonApiError( `${method} ${path} failed`, @@ -327,17 +330,40 @@ async function ledgerRequest( return response.json() as Promise } -async function ledgerGet( +/** + * Send a GET request + * + * @param baseUrl - The base URL of the Canton API. + * @param path - The endpoint path to send the request to. + * @param headers - HTTP headers to include in the request. + * @param timeoutMs - Timeout for the request in milliseconds. + * @param queryParams - Optional query parameters to append to the URL. + * @returns A promise resolving to the parsed response of type T. + * @throws {CantonApiError} If the request fails or the response is not OK. + */ +export async function get( baseUrl: string, path: string, headers: Record, timeoutMs: number, queryParams?: Record, ): Promise { - return ledgerRequest('GET', baseUrl, path, headers, timeoutMs, { queryParams }) + return request('GET', baseUrl, path, headers, timeoutMs, { queryParams }) } -async function ledgerPost( +/** + * Send a POST request + * + * @param baseUrl - The base URL of the Canton API. + * @param path - The endpoint path to send the request to. + * @param headers - HTTP headers to include in the request. + * @param timeoutMs - Timeout for the request in milliseconds. + * @param body - The request payload to send as JSON. + * @param queryParams - Optional query parameters to append to the URL. + * @returns A promise resolving to the parsed response of type T. + * @throws {CantonApiError} If the request fails or the response is not OK. + */ +export async function post( baseUrl: string, path: string, headers: Record, @@ -345,5 +371,5 @@ async function ledgerPost( body: unknown, queryParams?: Record, ): Promise { - return ledgerRequest('POST', baseUrl, path, headers, timeoutMs, { body, queryParams }) + return request('POST', baseUrl, path, headers, timeoutMs, { body, queryParams }) } diff --git a/ccip-sdk/src/canton/events.ts b/ccip-sdk/src/canton/events.ts new file mode 100644 index 00000000..2a579a01 --- /dev/null +++ b/ccip-sdk/src/canton/events.ts @@ -0,0 +1,398 @@ +/** + * Canton gRPC JSON API event parsing utilities. + * + * The Canton Ledger JSON API (gRPC-gateway) wraps each ledger event as: + * `{ Event: { Created: { template_id, create_arguments, ... } } }` + * + * Field values use a `{ Sum: { Text|Numeric|Int64|Party|ContractId|... } }` + * envelope. The helpers in this module decode that format into plain + * JavaScript objects that the rest of the SDK can work with. + */ + +import { CCIPError, CCIPErrorCode } from '../errors/index.ts' +import { type ExecutionReceipt, ExecutionState } from '../types.ts' + +// --------------------------------------------------------------------------- +// Public types +// --------------------------------------------------------------------------- + +/** + * Structured result extracted from a `CCIPMessageSent` Created event in a + * Canton transaction response. + */ +export interface CantonSendResultFields { + messageId: string + encodedMessage: string + sequenceNumber: bigint + nonce?: bigint + onRampAddress?: string +} + +// --------------------------------------------------------------------------- +// Top-level parsers +// --------------------------------------------------------------------------- + +/** + * Walk a Canton transaction response and extract the `CCIPMessageSent` fields. + * + * The Canton gRPC JSON API returns Created events with the structure: + * ```json + * { "Event": { "Created": { + * "template_id": { "entity_name": "CCIPMessageSent" }, + * "create_arguments": { "fields": [ + * { "label": "event", "value": { "Sum": { "Record": { "fields": [...] } } } } + * ]} + * }}} + * ``` + * Field values use a `{ Sum: { Text|Numeric|... } }` envelope. + * + * @throws {@link CCIPError} if no `CCIPMessageSent` event is found. + */ +export function parseCantonSendResult( + transaction: unknown, + updateId: string, +): CantonSendResultFields { + const events = extractEventsFromTransaction(transaction) + + for (const event of events) { + if (!event || typeof event !== 'object') continue + const rec = event as Record + + if (getTemplateEntityName(rec) !== 'CCIPMessageSent') continue + + // Handle both naming conventions for the create arguments object + const createArgs = (rec.create_arguments ?? rec.createArgument) as + | Record + | undefined + + // Try to locate the nested `event` record (CCIPMessageSentEvent) + const sentEvent = extractCCIPMessageSentEvent(createArgs) + + if (sentEvent) { + return { + messageId: typeof sentEvent.messageId === 'string' ? sentEvent.messageId : updateId, + encodedMessage: + typeof sentEvent.encodedMessage === 'string' ? sentEvent.encodedMessage : '', + sequenceNumber: toBigIntSafe(sentEvent.sequenceNumber), + nonce: sentEvent.nonce != null ? toBigIntSafe(sentEvent.nonce) : undefined, + onRampAddress: + typeof sentEvent.onRampAddress === 'string' ? sentEvent.onRampAddress : undefined, + } + } + + // Flat fallback — fields directly on createArgument + if (createArgs) { + const flat = flattenCantonRecord(createArgs) + return { + messageId: typeof flat.messageId === 'string' ? flat.messageId : updateId, + encodedMessage: typeof flat.encodedMessage === 'string' ? flat.encodedMessage : '', + sequenceNumber: toBigIntSafe(flat.sequenceNumber), + nonce: flat.nonce != null ? toBigIntSafe(flat.nonce) : undefined, + onRampAddress: typeof flat.onRampAddress === 'string' ? flat.onRampAddress : undefined, + } + } + } + + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton send: no CCIPMessageSent event found in transaction ${updateId}`, + ) +} + +/** + * Walk a Canton transaction response and extract an {@link ExecutionReceipt}. + * + * Looks for an `ExecutionStateChanged` Created event and extracts the + * relevant fields. If no matching event is found, returns a minimal + * receipt with {@link ExecutionState.Success} (the command succeeded if we + * reached this point). + */ +export function parseCantonExecutionReceipt( + transaction: unknown, + updateId: string, +): ExecutionReceipt { + const events = extractEventsFromTransaction(transaction) + + for (const event of events) { + if (!event || typeof event !== 'object') continue + const rec = event as Record + + if (!getTemplateEntityName(rec).includes('ExecutionStateChanged')) continue + + // Handle both naming conventions for the create arguments object, then flatten + const rawArgs = (rec.create_arguments ?? rec.createArgument ?? rec) as Record + const payload = flattenCantonRecord(rawArgs) + + const msgId = payload['messageId'] + const seqNum = payload['sequenceNumber'] + const srcChain = payload['sourceChainSelector'] + const retData = payload['returnData'] + return { + messageId: typeof msgId === 'string' ? msgId : updateId, + sequenceNumber: toBigIntSafe(seqNum), + state: mapExecutionState(payload['state']), + sourceChainSelector: srcChain != null ? toBigIntSafe(srcChain) : undefined, + returnData: typeof retData === 'string' ? retData : undefined, + } + } + + // Fallback — the command completed successfully but we couldn't locate the + // specific ExecutionStateChanged event (e.g. different event format). + return { + messageId: updateId, + sequenceNumber: 0n, + state: ExecutionState.Success, + } +} + +/** + * Resolve the record-time from a Canton transaction record. + * + * The gRPC API returns `{ record_time: { seconds: N, nanos: N } }` while + * the legacy JSON API returns `{ recordTime: "ISO-string" }`. + */ +export function resolveTimestamp(txRecord: Record): number { + // gRPC: { record_time: { seconds: N } } + const rt = txRecord.record_time + if (rt && typeof rt === 'object') { + const rtRec = rt as Record + if (typeof rtRec.seconds === 'number') return rtRec.seconds + if (typeof rtRec.seconds === 'string') return parseInt(rtRec.seconds, 10) + } + // Legacy: { recordTime: "ISO-string" } + const rts = typeof txRecord.recordTime === 'string' ? txRecord.recordTime : '' + return rts ? Math.floor(new Date(rts).getTime() / 1000) : Math.floor(Date.now() / 1000) +} + +// --------------------------------------------------------------------------- +// Event extraction +// --------------------------------------------------------------------------- + +/** + * Recursively extract normalised event objects from a Canton transaction tree. + * + * The gRPC JSON API wraps each event as `{ Event: { Created: { ... } } }`. + * This function unwraps those wrappers so callers always receive the inner + * Created / Exercised record directly (which carries `template_id`, + * `create_arguments`, etc.). + */ +export function extractEventsFromTransaction(obj: unknown): unknown[] { + const results: unknown[] = [] + if (!obj || typeof obj !== 'object') return results + + const record = obj as Record + + // gRPC-style wrapper: { Event: { Created: {...} } } + if (record.Event && typeof record.Event === 'object') { + const eventWrapper = record.Event as Record + for (const eventType of ['Created', 'Exercised', 'Archived']) { + if (eventWrapper[eventType] && typeof eventWrapper[eventType] === 'object') { + results.push(eventWrapper[eventType]) + } + } + return results + } + + // Flat event-type wrapper: { CreatedEvent: {...} } / { ExercisedEvent: {...} } / { ArchivedEvent: {...} } + for (const key of ['CreatedEvent', 'ExercisedEvent', 'ArchivedEvent']) { + if (record[key] && typeof record[key] === 'object') { + results.push(record[key]) + return results + } + } + + // Arrays of events — each element might itself be an Event wrapper + for (const key of ['createdEvents', 'exercisedEvents', 'events']) { + if (Array.isArray(record[key])) { + for (const ev of record[key] as unknown[]) { + results.push(...extractEventsFromTransaction(ev)) + } + } + } + + // eventsById map + if (record.eventsById && typeof record.eventsById === 'object') { + for (const ev of Object.values(record.eventsById as Record)) { + results.push(...extractEventsFromTransaction(ev)) + } + } + + // Recurse into known wrapper keys that contain the event list + for (const key of ['transaction', 'JsTransaction']) { + if (record[key] && typeof record[key] === 'object' && !Array.isArray(record[key])) { + results.push(...extractEventsFromTransaction(record[key])) + } + } + + return results +} + +// --------------------------------------------------------------------------- +// Low-level field helpers +// --------------------------------------------------------------------------- + +/** + * Dig into a `create_arguments` / `createArgument` object to find the nested + * `CCIPMessageSentEvent` record and return it as a flat `label → value` map. + * + * The gRPC-style event format stores the nested `event` field as: + * ```json + * { "label": "event", "value": { "Sum": { "Record": { "fields": [...] } } } } + * ``` + */ +export function extractCCIPMessageSentEvent( + arg: Record | undefined, +): Record | undefined { + if (!arg) return undefined + + // gRPC structured fields array: arg.fields = [{ label, value }] + if (Array.isArray(arg.fields)) { + for (const field of arg.fields as Array>) { + if (field.label !== 'event') continue + const resolved = resolveEventFieldValue(field.value) + if (resolved) return resolved + } + } + + // Verbose/legacy JSON API mode: arg.event is already a named object + if (arg.event && typeof arg.event === 'object') { + return flattenCantonRecord(arg.event as Record) + } + + return undefined +} + +/** + * Resolve a Canton field `value` for the `event` label. + * + * Handles: + * - `{ Sum: { Record: { fields: [...] } } }` — gRPC style + * - `{ fields: [...] }` — already a Record, just flatten + * - plain object — return as-is + */ +export function resolveEventFieldValue(value: unknown): Record | undefined { + if (!value || typeof value !== 'object') return undefined + const v = value as Record + + // gRPC: { Sum: { Record: { fields: [...] } } } + if (v.Sum && typeof v.Sum === 'object') { + const sum = v.Sum as Record + if (sum.Record && typeof sum.Record === 'object') { + return flattenCantonRecord(sum.Record as Record) + } + } + + // Already a record with a fields array + if (Array.isArray(v.fields)) { + return flattenCantonRecord(v) + } + + // Flat object (verbose API) + return v +} + +/** + * Convert a Canton record `{ fields: [{ label, value }] }` into a plain + * `{ [label]: extractedValue }` map. When no `fields` array is present the + * record is returned unchanged. + */ +export function flattenCantonRecord(record: Record): Record { + if (!Array.isArray(record.fields)) return record + const result: Record = {} + for (const f of record.fields as Array>) { + if (typeof f.label === 'string') { + result[f.label] = extractFieldValue(f.value) + } + } + return result +} + +/** + * Extract the entity name from a Canton event, supporting both the gRPC format + * (`template_id.entity_name`) and the legacy flat format (`templateId` string + * with colon-separated parts). + */ +export function getTemplateEntityName(event: Record): string { + // gRPC format: { template_id: { entity_name: "..." } } + if (event.template_id && typeof event.template_id === 'object') { + const tid = event.template_id as Record + if (typeof tid.entity_name === 'string') return tid.entity_name + } + // Legacy flat format: "packageId:Module:Entity" or "Module:Entity" + if (typeof event.templateId === 'string') { + const parts = event.templateId.split(':') + return parts[parts.length - 1] ?? '' + } + return '' +} + +/** + * Extract a primitive value from a Canton Daml field value, handling both the + * gRPC `{ Sum: { Text|Numeric|Int64|Party|ContractId } }` envelope and the + * legacy verbose JSON API `{ text|int64|numeric|... }` form. + * + * Numeric values returned by the gRPC API have a trailing `"."` (e.g. `"1."`) + * which is stripped to yield a clean integer string. + */ +export function extractFieldValue(value: unknown): unknown { + if (!value || typeof value !== 'object') return value + const v = value as Record + + // gRPC Sum envelope: { Sum: { Text: "..." } } + if (v.Sum && typeof v.Sum === 'object') { + const sum = v.Sum as Record + if ('Text' in sum) return sum.Text + if ('Numeric' in sum) { + const n = String(sum.Numeric) + // Strip trailing "." produced by Daml numeric encoding (e.g. "1." → "1") + return n.endsWith('.') ? n.slice(0, -1) : n + } + if ('Int64' in sum) return sum.Int64 + if ('Party' in sum) return sum.Party + if ('ContractId' in sum) return sum.ContractId + if ('Bool' in sum) return sum.Bool + // For complex types (Record, List, GenMap) return the sum value as-is + return sum + } + + // Legacy verbose JSON API mode + if ('text' in v) return v.text + if ('int64' in v) return v.int64 + if ('numeric' in v) return v.numeric + if ('contractId' in v) return v.contractId + if ('party' in v) return v.party + return value +} + +/** + * Safely convert an unknown value to bigint, defaulting to `0n`. + */ +export function toBigIntSafe(v: unknown): bigint { + if (typeof v === 'bigint') return v + if (typeof v === 'number') return BigInt(v) + if (typeof v === 'string' && v.length > 0) { + try { + // Strip trailing "." produced by Daml numeric encoding (e.g. "1." → "1") + const s = v.endsWith('.') ? v.slice(0, -1) : v + return BigInt(s) + } catch { + return 0n + } + } + return 0n +} + +/** + * Map a Canton execution state value to the SDK {@link ExecutionState}. + */ +export function mapExecutionState(state: unknown): ExecutionState { + if (state === undefined || state === null) return ExecutionState.Success + + const s = typeof state === 'string' ? state.toLowerCase() : `${state as string | number}` + + if (s === 'success' || s === '2') return ExecutionState.Success + if (s === 'failed' || s === '3') return ExecutionState.Failed + if (s === 'inprogress' || s === 'in_progress' || s === '1') return ExecutionState.InProgress + + return ExecutionState.Success +} diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts index 16487bfe..b3a7e876 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -1,5 +1,5 @@ import type { DisclosedContract } from './types.ts' -import { CCIPError, CCIPErrorCode, CCIPNotImplementedError } from '../../errors/index.ts' +import { CCIPError, CCIPErrorCode } from '../../errors/index.ts' import { type CantonClient, type EventFormat, @@ -8,20 +8,20 @@ import { } from '../client/index.ts' /** - * Extract the `instanceId` string from a contract's `createArgument` object. + * Extract a named string field from a contract's `createArgument` object. * Handles both verbose mode (direct field) and structured fields arrays. */ -function extractInstanceId(createArgument: unknown): string | null { +function extractStringField(createArgument: unknown, fieldName: string): string | null { if (!createArgument || typeof createArgument !== 'object') return null const arg = createArgument as Record - if ('instanceId' in arg && typeof arg['instanceId'] === 'string') { - return arg['instanceId'] + if (fieldName in arg && typeof arg[fieldName] === 'string') { + return arg[fieldName] } if ('fields' in arg && Array.isArray(arg['fields'])) { for (const field of arg['fields'] as Array>) { - if (field['label'] === 'instanceId') { + if (field['label'] === fieldName) { const val = field['value'] if (typeof val === 'string') return val if (val && typeof val === 'object' && 'text' in val) { @@ -33,6 +33,9 @@ function extractInstanceId(createArgument: unknown): string | null { return null } +function extractInstanceId(createArgument: unknown): string | null { + return extractStringField(createArgument, 'instanceId') +} /** * Metadata for each CCIP contract type needed for ACS filtering: * - `templateId`: package-name reference used directly in the `TemplateFilter` so the @@ -49,6 +52,10 @@ const CCIP_TEMPLATES = { templateId: '#ccip-receiver:CCIP.CCIPReceiver:CCIPReceiver', moduleEntity: 'CCIP.CCIPReceiver:CCIPReceiver', }, + ccipSender: { + templateId: '#ccip-sender:CCIP.CCIPSender:CCIPSender', + moduleEntity: 'CCIP.CCIPSender:CCIPSender', + }, } as const type CcipContractType = keyof typeof CCIP_TEMPLATES @@ -87,6 +94,8 @@ interface RichContractMatch { synchronizerId: string instanceId: string | null signatory: string | null + /** partyOwner field from createArgument (present on PerPartyRouter) */ + partyOwner: string | null } /** @@ -127,6 +136,7 @@ async function fetchRichSnapshot( synchronizerId: active.synchronizerId, instanceId: extractInstanceId(created.createArgument), signatory: signatories.length === 1 ? (signatories[0] ?? null) : null, + partyOwner: extractStringField(created.createArgument, 'partyOwner'), } const list = byModuleEntity.get(moduleEntity) ?? [] @@ -205,6 +215,32 @@ function pickBySignatory( ) } +function pickByPartyOwner( + snapshot: Map, + contractType: CcipContractType, + party: string, +): DisclosedContract { + const moduleEntity = CCIP_MODULE_ENTITIES[contractType] + const candidates = snapshot.get(moduleEntity) ?? [] + + for (const c of candidates) { + if (c.partyOwner === party) { + return { + templateId: c.templateId, + contractId: c.contractId, + createdEventBlob: c.createdEventBlob, + synchronizerId: c.synchronizerId, + } + } + } + + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `Canton ACS: no active "${moduleEntity}" contract found with partyOwner "${party}". ` + + `Verify the party is correct and the contract is active.`, + ) +} + /** * Configuration for the ACS-based disclosure provider. * Requires direct access to the Canton Ledger API and the full set of contract @@ -226,7 +262,12 @@ export type AcsExecutionDisclosures = { /** * Same party disclosed contracts required to submit a `ccipSend` command on Canton. */ -export type AcsSendDisclosures = never // not implemented yet +export type AcsSendDisclosures = { + /** The sender's PerPartyRouter contract. */ + perPartyRouter: DisclosedContract + /** The sender's CCIPSender contract. */ + ccipSender: DisclosedContract +} /** * Disclosure provider that fetches `createdEventBlob`s directly from the Canton @@ -271,19 +312,29 @@ export class AcsDisclosureProvider { async fetchExecutionDisclosures(receiverCid?: string): Promise { const snapshot = await fetchRichSnapshot(this.client, this.config.party) - const perPartyRouter = pickBySignatory(snapshot, 'perPartyRouter', this.config.party) + const existingRouter = pickByPartyOwner(snapshot, 'perPartyRouter', this.config.party) const ccipReceiver = receiverCid ? pickByContractId(snapshot, receiverCid) : pickBySignatory(snapshot, 'ccipReceiver', this.config.party) - return { perPartyRouter, ccipReceiver } + return { perPartyRouter: existingRouter, ccipReceiver } } /** * Fetch all contracts that must be disclosed for a `ccipSend` command. + * + * Returns the sender's `PerPartyRouter` and `CCIPSender` contracts from the + * Active Contract Set, matched by signatory (the sender party). */ async fetchSendDisclosures(): Promise { - await Promise.resolve() // placeholder for potential future implementation - throw new CCIPNotImplementedError('AcsDisclosureProvider.fetchSendDisclosures') + const snapshot = await fetchRichSnapshot(this.client, this.config.party) + + const existingRouter = pickByPartyOwner(snapshot, 'perPartyRouter', this.config.party) + const existingSender = pickBySignatory(snapshot, 'ccipSender', this.config.party) + + return { + perPartyRouter: existingRouter, + ccipSender: existingSender, + } } } diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 5a108bed..02092e50 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -1,4 +1,4 @@ -import type { BytesLike } from 'ethers' +import { type BytesLike, id as keccak256Utf8 } from 'ethers' import type { PickDeep } from 'type-fest' import { @@ -38,19 +38,37 @@ import { type WithLogger, CCIPVersion, ChainFamily, - ExecutionState, } from '../types.ts' import { networkInfo } from '../utils.ts' import { type CantonClient, type JsCommands, createCantonClient } from './client/index.ts' +import { parseCantonExecutionReceipt, parseCantonSendResult, resolveTimestamp } from './events.ts' import { AcsDisclosureProvider } from './explicit-disclosures/acs.ts' +import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' import type { DisclosedContract } from './explicit-disclosures/types.ts' import { CCV_INDEXER_URL } from '../evm/const.ts' -import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' -import { type UnsignedCantonTx, isCantonWallet } from './types.ts' +import { type TokenMetadataClient, createTokenMetadataClient } from './token-metadata/client.ts' +import { + type TransferInstructionClient, + createTransferInstructionClient, +} from './transfer-instruction/client.ts' +import { + type CantonExtraArgsV1, + type UnsignedCantonTx, + isCantonWallet, + parseInstrumentId, +} from './types.ts' export type { CantonClient, CantonClientConfig } from './client/index.ts' -export type { CantonWallet, UnsignedCantonTx } from './types.ts' -export { isCantonWallet } from './types.ts' +export type { + CantonCCVSendInput, + CantonExtraArgsV1, + CantonInstrumentId, + CantonTokenExtraArgs, + CantonTokenInput, + CantonWallet, + UnsignedCantonTx, +} from './types.ts' +export { isCantonWallet, parseInstrumentId } from './types.ts' /** * Canton chain implementation supporting Canton Ledger networks. @@ -68,13 +86,19 @@ export class CantonChain extends Chain { readonly provider: CantonClient readonly acsDisclosureProvider: AcsDisclosureProvider readonly edsDisclosureProvider: EdsDisclosureProvider + readonly transferInstructionClient: TransferInstructionClient + readonly tokenMetadataClient: TokenMetadataClient readonly indexerUrl: string + readonly ccipParty: string /** * Creates a new CantonChain instance. * @param client - Canton Ledger API client. * @param acsDisclosureProvider - ACS-based disclosure provider. * @param edsDisclosureProvider - EDS-based disclosure provider. + * @param transferInstructionClient - Transfer Instruction API client. + * @param tokenMetadataClient - Token Metadata API client. + * @param ccipParty - The party ID to use for CCIP operations * @param indexerUrl - Base URL of the CCV indexer service. * @param network - Network information for this chain. * @param ctx - Context containing logger. @@ -83,6 +107,9 @@ export class CantonChain extends Chain { client: CantonClient, acsDisclosureProvider: AcsDisclosureProvider, edsDisclosureProvider: EdsDisclosureProvider, + transferInstructionClient: TransferInstructionClient, + tokenMetadataClient: TokenMetadataClient, + ccipParty: string, indexerUrl: string, network: NetworkInfo, ctx?: ChainContext, @@ -92,6 +119,9 @@ export class CantonChain extends Chain { this.network = network this.acsDisclosureProvider = acsDisclosureProvider this.edsDisclosureProvider = edsDisclosureProvider + this.transferInstructionClient = transferInstructionClient + this.tokenMetadataClient = tokenMetadataClient + this.ccipParty = ccipParty this.indexerUrl = indexerUrl } @@ -128,6 +158,9 @@ export class CantonChain extends Chain { client: CantonClient, acsDisclosureProvider: AcsDisclosureProvider, edsDisclosureProvider: EdsDisclosureProvider, + transferInstructionClient: TransferInstructionClient, + tokenMetadataClient: TokenMetadataClient, + ccipParty: string, indexerUrl = CCV_INDEXER_URL, ctx?: ChainContext, ): Promise { @@ -143,6 +176,9 @@ export class CantonChain extends Chain { client, acsDisclosureProvider, edsDisclosureProvider, + transferInstructionClient, + tokenMetadataClient, + ccipParty, indexerUrl, networkInfo(chainId) as NetworkInfo, ctx, @@ -175,6 +211,9 @@ export class CantonChain extends Chain { 'CantonChain.fromUrl: ctx.cantonConfig is required', ) } + console.log( + `[CantonChain.fromUrl] Connecting to Canton Ledger API at ${url}... with JWT: ${ctx.cantonConfig.jwt.substring(0, 10)}...`, + ) const client = createCantonClient({ baseUrl: url, jwt: ctx.cantonConfig.jwt }) try { const alive = await client.isAlive() @@ -190,10 +229,21 @@ export class CantonChain extends Chain { party: ctx.cantonConfig.party, }) const edsDisclosureProvider = new EdsDisclosureProvider({ edsBaseUrl: ctx.cantonConfig.edsUrl }) + const transferInstructionClient = createTransferInstructionClient({ + baseUrl: ctx.cantonConfig.transferInstructionUrl, + jwt: ctx.cantonConfig.jwt, + }) + const tokenMetadataClient = createTokenMetadataClient({ + baseUrl: ctx.cantonConfig.transferInstructionUrl, + jwt: ctx.cantonConfig.jwt, + }) return CantonChain.fromClient( client, acsDisclosureProvider, edsDisclosureProvider, + transferInstructionClient, + tokenMetadataClient, + ctx.cantonConfig.ccipParty, ctx.cantonConfig.indexerUrl ?? '', ctx, ) @@ -348,22 +398,288 @@ export class CantonChain extends Chain { throw new CCIPNotImplementedError('CantonChain.getFee') } - /** - * {@inheritDoc Chain.generateUnsignedSendMessage} - * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) - */ - override generateUnsignedSendMessage( - _opts: Parameters[0], - ): Promise { - return Promise.reject(new CCIPNotImplementedError('CantonChain.generateUnsignedSendMessage')) + override async generateUnsignedSendMessage( + opts: Parameters[0], + ): Promise { + const { sender, destChainSelector, message } = opts + + // --- validate inputs --- + if (!sender) { + throw new CCIPError( + CCIPErrorCode.WALLET_INVALID, + 'CantonChain.generateUnsignedSendMessage: sender (party ID) is required', + ) + } + + if (!message.feeToken) { + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + 'CantonChain.generateUnsignedSendMessage: message.feeToken is required ' + + '(use "admin::tokenId" format, e.g. "registryAdmin::Amulet")', + ) + } + + const cantonArgs = message.extraArgs as CantonExtraArgsV1 | undefined + if (!cantonArgs?.feeTokenHoldingCids?.length) { + throw new CCIPError( + CCIPErrorCode.METHOD_UNSUPPORTED, + 'CantonChain.generateUnsignedSendMessage: message.extraArgs.feeTokenHoldingCids is required. ' + + 'Pass at least one fee-token holding contract ID in extraArgs.', + ) + } + + // --- parse fields --- + const instrumentId = parseInstrumentId(message.feeToken) + const receiverHex = stripHexPrefix( + typeof message.receiver === 'string' ? message.receiver : String(message.receiver), + ) + const payloadHex = message.data + ? stripHexPrefix(typeof message.data === 'string' ? message.data : String(message.data)) + : '' + const gasLimit = cantonArgs.gasLimit ?? 200_000 + const feeTokenHoldingCids = cantonArgs.feeTokenHoldingCids + const ccvRawAddresses = cantonArgs.ccvRawAddresses ?? [] + // Derive hex InstanceAddresses at runtime: keccak256 of each raw "instanceId@party" string + const ccvAddresses = ccvRawAddresses.map((raw) => keccak256Utf8(raw)) + + this.logger.debug('CantonChain.generateUnsignedSendMessage: fetching ACS disclosures') + + // Step 1 — Fetch same-party disclosures (PerPartyRouter + CCIPSender) + const acsDisclosures = await this.acsDisclosureProvider.fetchSendDisclosures() + + // Step 2 — Fetch cross-party disclosures from EDS + this.logger.debug( + `CantonChain.generateUnsignedSendMessage: fetching EDS disclosures for ${ccvAddresses.length} CCVs`, + ) + const edsResult = await this.edsDisclosureProvider.fetchSendDisclosures(ccvAddresses) + + // Step 3 — Build CCV send inputs + const ccvSendInputs = ccvRawAddresses.map((rawAddr, i) => { + const addr = ccvAddresses[i]! + const ccvDisclosure = edsResult.ccvs[addr] + if (!ccvDisclosure?.disclosedContract) { + throw new CCIPError( + CCIPErrorCode.CANTON_API_ERROR, + `EDS did not return a disclosure for CCV at ${addr}`, + ) + } + return { + ccvCid: ccvDisclosure.disclosedContract.contractId, + ccvRawAddress: { unpack: rawAddr }, + verifierArgs: '', + } + }) + + // Step 4 — Extract CCV disclosed contracts + const ccvDisclosedContracts: DisclosedContract[] = ccvAddresses + .map((addr) => edsResult.ccvs[addr]?.disclosedContract) + .filter((dc): dc is DisclosedContract => dc !== undefined) + + // Step 5 — Fetch transfer factory from Transfer Instruction API + // Mirrors the Go test flow: get registry admin, then call getTransferFactory + // with choiceArguments describing the intended transfer. + this.logger.debug( + 'CantonChain.generateUnsignedSendMessage: fetching registry admin from Token Metadata API', + ) + const registryInfo = await this.tokenMetadataClient.getRegistryInfo() + const registryAdmin = registryInfo.adminId + + this.logger.debug( + `CantonChain.generateUnsignedSendMessage: registry admin is ${registryAdmin}, fetching transfer factory...`, + ) + + this.logger.debug( + 'CantonChain.generateUnsignedSendMessage: fetching transfer factory from Transfer Instruction API', + ) + const transferFactoryResponse = await this.transferInstructionClient.getTransferFactory({ + choiceArguments: { + expectedAdmin: registryAdmin, + transfer: { + sender, + receiver: this.ccipParty, + amount: '100.00', + instrumentId: { admin: instrumentId.admin, id: instrumentId.id }, + lock: null, + requestedAt: new Date(Date.now() - 3_600_000).toISOString(), + executeBefore: new Date(Date.now() + 86_400_000).toISOString(), + inputHoldingCids: [], + meta: { values: {} }, + }, + extraArgs: { + context: { values: {} }, + meta: { values: {} }, + }, + }, + }) + + // Step 6 — Build fee token input from the transfer factory response + const transferFactoryContextValues: Record = {} + const ctxData = transferFactoryResponse.choiceContext.choiceContextData + if (typeof ctxData === 'object' && 'values' in ctxData) { + const values = ctxData.values + if (values && typeof values === 'object') { + for (const [key, val] of Object.entries(values as Record)) { + // Preserve the full variant structure (e.g. { tag: "AV_ContractId", value: "00..." }) + // so the Canton JSON API receives the correct tagged union, not a bare string. + transferFactoryContextValues[key] = val + } + } + } + + const feeTokenInput = { + transferFactory: transferFactoryResponse.factoryId, + extraArgs: { + context: { values: transferFactoryContextValues }, + meta: { values: {} }, + }, + tokenPoolHoldings: [] as string[], + } + + // Step 7 — Assemble the Send choice argument. + // The EDS `choiceContextData` contains the contract CIDs for OnRamp, + // GlobalConfig, TAR, FeeQuoter, RMNRemote etc. + const edsContextData = + edsResult.choiceContext.choiceContextData != null && + typeof edsResult.choiceContext.choiceContextData === 'object' + ? (edsResult.choiceContext.choiceContextData as Record) + : {} + + const choiceArgument: Record = { + context: edsContextData, + routerCid: acsDisclosures.perPartyRouter.contractId, + destChainSelector: destChainSelector.toString(), + receiver: receiverHex, + payload: payloadHex, + ccipReceiveGasLimit: Number(gasLimit), + senderRequiredCCVs: [], + feeToken: instrumentId, + feeTokenInput, + feeTokenHoldingCids, + tokenTransfer: null, + ccvSendInputs, + } + + // Step 8 — Merge all disclosed contracts + const transferFactoryDisclosures: DisclosedContract[] = + transferFactoryResponse.choiceContext.disclosedContracts ?? [] + + const allDisclosed: DisclosedContract[] = [ + acsDisclosures.perPartyRouter, + acsDisclosures.ccipSender, + ...edsResult.choiceContext.disclosedContracts, + ...ccvDisclosedContracts, + ...transferFactoryDisclosures, + ] + + // Step 9 — Build the ExerciseCommand + const exerciseCommand = { + ExerciseCommand: { + templateId: acsDisclosures.ccipSender.templateId, + contractId: acsDisclosures.ccipSender.contractId, + choice: 'Send', + choiceArgument, + }, + } + + // Step 10 — Assemble JsCommands + const jsCommands: JsCommands = { + commands: [exerciseCommand], + commandId: `ccip-send-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + actAs: [sender], + disclosedContracts: allDisclosed.map((dc) => ({ + templateId: dc.templateId, + contractId: dc.contractId, + createdEventBlob: dc.createdEventBlob, + synchronizerId: dc.synchronizerId, + })), + } + + this.logger.debug( + `CantonChain.generateUnsignedSendMessage: built command with ${allDisclosed.length} disclosed contracts`, + ) + + return { + family: ChainFamily.Canton, + commands: jsCommands, + } } /** * {@inheritDoc Chain.sendMessage} - * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - sendMessage(_opts: Parameters[0]): Promise { - throw new CCIPNotImplementedError('CantonChain.sendMessage') + async sendMessage(opts: Parameters[0]): Promise { + const { wallet } = opts + if (!isCantonWallet(wallet)) { + throw new CCIPWalletInvalidError(wallet) + } + + // Build the unsigned command + const unsigned = await this.generateUnsignedSendMessage({ + ...opts, + sender: wallet.party, + }) + + this.logger.debug('CantonChain.sendMessage: submitting command to Ledger API') + + // Submit and wait for the full transaction (so we get events back) + const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) + const txRecord = (response.transaction ?? response) as Record + const updateId: string = + (typeof txRecord.update_id === 'string' ? txRecord.update_id : null) ?? + (typeof txRecord.updateId === 'string' ? txRecord.updateId : '') + + this.logger.debug(`CantonChain.sendMessage: submitted, updateId=${updateId}`) + + // Parse CCIPMessageSent from the transaction events + const sendResult = parseCantonSendResult(response.transaction ?? response, updateId) + + const timestamp = resolveTimestamp(txRecord) + + // Build the Lane + const lane: Lane = { + sourceChainSelector: this.network.chainSelector, + destChainSelector: opts.destChainSelector, + onRamp: sendResult.onRampAddress ?? '', + version: CCIPVersion.V2_0, + } + + // Build a synthetic ChainLog from the Canton transaction + const log: ChainLog = { + topics: [], + index: 0, + address: '', + blockNumber: 0, + transactionHash: updateId, + data: response.transaction as Record, + } + + // Build the transaction descriptor + const tx: Omit = { + hash: updateId, + blockNumber: 0, + timestamp, + from: wallet.party, + } + + // Build the CCIPMessage from the CCIPMessageSent event data + const ccipMessage = { + messageId: sendResult.messageId, + sourceChainSelector: this.network.chainSelector, + destChainSelector: opts.destChainSelector, + sequenceNumber: sendResult.sequenceNumber, + nonce: sendResult.nonce ?? 0n, + sender: wallet.party, + receiver: + typeof opts.message.receiver === 'string' + ? opts.message.receiver + : String(opts.message.receiver), + data: sendResult.encodedMessage, + tokenAmounts: [] as readonly { token: string; amount: bigint }[], + feeToken: opts.message.feeToken ?? '', + feeTokenAmount: 0n, + } as unknown as CCIPMessage + + return { lane, message: ccipMessage, log, tx } } /** @@ -541,17 +857,14 @@ export class CantonChain extends Chain { // Submit and wait for the full transaction (so we get events back) const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) - - const txRecord = response.transaction as Record - const updateId: string = typeof txRecord.updateId === 'string' ? txRecord.updateId : '' - const recordTime: string = typeof txRecord.recordTime === 'string' ? txRecord.recordTime : '' + const txRecord = (response.transaction ?? response) as Record + const updateId: string = + (typeof txRecord.update_id === 'string' ? txRecord.update_id : null) ?? + (typeof txRecord.updateId === 'string' ? txRecord.updateId : '') // Parse execution receipt from the transaction events - const receipt = parseCantonExecutionReceipt(response.transaction, updateId) - - const timestamp = recordTime - ? Math.floor(new Date(recordTime).getTime() / 1000) - : Math.floor(Date.now() / 1000) + const receipt = parseCantonExecutionReceipt(response.transaction ?? response, updateId) + const timestamp = resolveTimestamp(txRecord) // Build a synthetic ChainLog — Canton doesn't have EVM-style logs, but the // SDK contract expects a ChainLog in the CCIPExecution. @@ -775,102 +1088,3 @@ export class CantonChain extends Chain { function stripHexPrefix(hex: string): string { return hex.startsWith('0x') ? hex.slice(2) : hex } - -/** - * Walk a Canton transaction response and extract an {@link ExecutionReceipt}. - * - * Canton transaction events are nested inside the `transaction` object - * returned by `submitAndWaitForTransaction`. The structure varies but - * generally contains arrays of `createdEvents` and `exercisedEvents`. - * We look for an event whose `templateId` contains - * `ExecutionStateChanged` and extract the relevant fields. - * - * If no matching event is found we return a minimal receipt with - * {@link ExecutionState.Success} (the command succeeded if we got this far). - */ -function parseCantonExecutionReceipt(transaction: unknown, updateId: string): ExecutionReceipt { - // Attempt to extract events from the transaction - const events = extractEventsFromTransaction(transaction) - - for (const event of events) { - const templateId = - typeof event === 'object' && event !== null && 'templateId' in event - ? String((event as Record).templateId) - : '' - - if (templateId.includes('ExecutionStateChanged')) { - const payload = - 'createArgument' in (event as Record) - ? ((event as Record).createArgument as Record) - : (event as Record) - - const msgId = payload['messageId'] - const seqNum = payload['sequenceNumber'] - const srcChain = payload['sourceChainSelector'] - const retData = payload['returnData'] - return { - messageId: typeof msgId === 'string' ? msgId : updateId, - sequenceNumber: BigInt( - typeof seqNum === 'string' || typeof seqNum === 'number' ? seqNum : 0, - ), - state: mapExecutionState(payload['state']), - sourceChainSelector: srcChain != null ? BigInt(srcChain as string | number) : undefined, - returnData: typeof retData === 'string' ? retData : undefined, - } - } - } - - // Fallback — the command completed successfully but we couldn't locate the - // specific ExecutionStateChanged event (e.g. different event format). - return { - messageId: updateId, - sequenceNumber: 0n, - state: ExecutionState.Success, - } -} - -/** - * Recursively extract event-like objects from a Canton transaction tree. - */ -function extractEventsFromTransaction(obj: unknown): unknown[] { - const results: unknown[] = [] - if (!obj || typeof obj !== 'object') return results - - const record = obj as Record - - // Direct event arrays - for (const key of ['createdEvents', 'exercisedEvents', 'events']) { - if (Array.isArray(record[key])) { - results.push(...(record[key] as unknown[])) - } - } - - // Nested transaction/eventsById - if (record['eventsById'] && typeof record['eventsById'] === 'object') { - results.push(...Object.values(record['eventsById'] as Record)) - } - - // Recurse into known wrapper keys - for (const key of ['transaction', 'rootEventIds', 'JsTransaction']) { - if (record[key] && typeof record[key] === 'object' && !Array.isArray(record[key])) { - results.push(...extractEventsFromTransaction(record[key])) - } - } - - return results -} - -/** - * Map a Canton execution state value to the SDK {@link ExecutionState}. - */ -function mapExecutionState(state: unknown): ExecutionState { - if (state === undefined || state === null) return ExecutionState.Success - - const s = typeof state === 'string' ? state.toLowerCase() : `${state as string | number}` - - if (s === 'success' || s === '2') return ExecutionState.Success - if (s === 'failed' || s === '3') return ExecutionState.Failed - if (s === 'inprogress' || s === 'in_progress' || s === '1') return ExecutionState.InProgress - - return ExecutionState.Success -} diff --git a/ccip-sdk/src/canton/token-metadata/client.ts b/ccip-sdk/src/canton/token-metadata/client.ts new file mode 100644 index 00000000..35f05db1 --- /dev/null +++ b/ccip-sdk/src/canton/token-metadata/client.ts @@ -0,0 +1,174 @@ +import { get } from '../client/client.ts' + +/** + * Map from token standard API name to the minor version of the API supported. + * + * Example: `{ "splice-api-token-metadata-v1": 1 }` + */ +export type SupportedApis = Record + +/** + * Information about the token registry. + */ +export interface GetRegistryInfoResponse { + /** The Daml party representing the registry app. */ + adminId: string + /** The token standard APIs supported by the registry. */ + supportedApis: SupportedApis +} + +/** + * Metadata for a single instrument managed by the registry. + */ +export interface Instrument { + /** Unique identifier assigned by the admin to the instrument. */ + id: string + /** Display name recommended by the instrument admin (not necessarily unique). */ + name: string + /** Symbol recommended by the instrument admin (not necessarily unique). */ + symbol: string + /** Decimal-encoded current total supply of the instrument. */ + totalSupply?: string + /** Timestamp when the total supply was last computed. */ + totalSupplyAsOf?: string + /** + * Number of decimal places used by the instrument (0–10). + * + * Daml interfaces represent holding amounts as `Decimal` values with 10 + * decimal places. This number SHOULD be used for display purposes. + * + * Defaults to 10. + */ + decimals: number + /** Token standard APIs supported for this specific instrument. */ + supportedApis: SupportedApis +} + +/** + * Paginated list of instruments. + */ +export interface ListInstrumentsResponse { + /** Instruments on the current page. */ + instruments: Instrument[] + /** Token for fetching the next page, if available. */ + nextPageToken?: string +} + +/** + * Standard error envelope returned by the token-metadata API. + */ +export interface TokenMetadataErrorResponse { + error: string +} + +// --------------------------------------------------------------------------- +// Pagination options +// --------------------------------------------------------------------------- + +/** + * Options for listing instruments. + */ +export interface ListInstrumentsOptions { + /** Number of instruments per page (default: 25). */ + pageSize?: number + /** Page token received from a previous `listInstruments` response. */ + pageToken?: string +} + +// --------------------------------------------------------------------------- +// Client configuration +// --------------------------------------------------------------------------- + +/** + * Configuration for the Token Metadata API client. + */ +export interface TokenMetadataClientConfig { + /** Base URL of the token registry (e.g. http://localhost:9000) */ + baseUrl: string + /** Optional JWT for authentication */ + jwt?: string + /** Request timeout in milliseconds (default: 30 000) */ + timeout?: number +} + +// --------------------------------------------------------------------------- +// Client factory +// --------------------------------------------------------------------------- + +/** + * Create a typed Token Metadata API client. + * + * The client mirrors the endpoints defined in `token-metadata-v1.yaml`. + */ +export function createTokenMetadataClient(config: TokenMetadataClientConfig) { + const baseUrl = config.baseUrl.replace(/\/$/, '') + console.log('Creating Token Metadata client with base URL:', baseUrl) + const headers = buildHeaders(config.jwt) + const timeoutMs = config.timeout ?? 30_000 + + const appendScanProxyPath = (path: string) => `/api/validator/v0/scan-proxy${path}` + return { + /** + * Get information about the registry, including supported APIs. + * + * `GET /registry/metadata/v1/info` + */ + async getRegistryInfo(): Promise { + return get( + baseUrl, + appendScanProxyPath('/registry/metadata/v1/info'), + headers, + timeoutMs, + ) + }, + + /** + * List all instruments managed by this instrument admin. + * + * `GET /registry/metadata/v1/instruments` + */ + async listInstruments(options?: ListInstrumentsOptions): Promise { + const queryParams: Record = {} + if (options?.pageSize !== undefined) { + queryParams['pageSize'] = String(options.pageSize) + } + if (options?.pageToken !== undefined) { + queryParams['pageToken'] = options.pageToken + } + return get( + baseUrl, + appendScanProxyPath('/registry/metadata/v1/instruments'), + headers, + timeoutMs, + Object.keys(queryParams).length > 0 ? queryParams : undefined, + ) + }, + + /** + * Retrieve an instrument's metadata by its ID. + * + * `GET /registry/metadata/v1/instruments/{instrumentId}` + */ + async getInstrument(instrumentId: string): Promise { + return get( + baseUrl, + appendScanProxyPath( + `/registry/metadata/v1/instruments/${encodeURIComponent(instrumentId)}`, + ), + headers, + timeoutMs, + ) + }, + } +} + +/** + * Type alias for the Token Metadata client instance. + */ +export type TokenMetadataClient = ReturnType + +function buildHeaders(jwt?: string): Record { + const headers: Record = { 'Content-Type': 'application/json' } + if (jwt) headers['Authorization'] = `Bearer ${jwt}` + return headers +} diff --git a/ccip-sdk/src/canton/transfer-instruction/client.ts b/ccip-sdk/src/canton/transfer-instruction/client.ts new file mode 100644 index 00000000..a43a6c69 --- /dev/null +++ b/ccip-sdk/src/canton/transfer-instruction/client.ts @@ -0,0 +1,186 @@ +import { post } from '../client/client.ts' +import type { DisclosedContract } from '../explicit-disclosures/index.ts' + +/** + * Context required to exercise a choice on a contract via an interface. + */ +export interface ChoiceContext { + /** Additional data to use when exercising the choice. */ + choiceContextData: Record + /** Contracts that must be disclosed to the participant node. */ + disclosedContracts: DisclosedContract[] +} + +/** + * The transfer factory contract together with its choice context. + * + * Clients SHOULD avoid reusing the same response for exercising multiple + * choices, as the choice context MAY be specific to a single exercise. + */ +export interface TransferFactoryWithChoiceContext { + /** Contract ID of the factory contract. */ + factoryId: string + /** + * The kind of transfer workflow: + * - `offer` – offer a transfer; only completes if the receiver accepts + * - `direct` – transfer directly (receiver pre-approved) + * - `self` – sender and receiver are the same party + */ + transferKind: 'self' | 'direct' | 'offer' + /** Choice context for exercising the factory choice. */ + choiceContext: ChoiceContext +} + +/** + * Request body for `getTransferFactory`. + */ +export interface GetFactoryRequest { + /** + * Arguments intended to be passed to the factory choice, encoded as a + * Daml JSON API object (with `extraArgs.context` and `extraArgs.meta` set + * to the empty object). + */ + choiceArguments: Record + /** When `true` the response omits debug fields. Defaults to `false`. */ + excludeDebugFields?: boolean +} + +/** + * Request body for the accept / reject / withdraw choice-context endpoints. + */ +export interface GetChoiceContextRequest { + /** + * Metadata passed to the choice and incorporated into the choice context. + * Provided for extensibility. + */ + meta?: Record +} + +/** + * Standard error envelope returned by the transfer-instruction API. + */ +export interface TransferInstructionErrorResponse { + error: string +} + +// --------------------------------------------------------------------------- +// Client configuration +// --------------------------------------------------------------------------- + +/** + * Configuration for the Transfer Instruction API client. + */ +export interface TransferInstructionClientConfig { + /** Base URL of the token registry (e.g. http://localhost:9000) */ + baseUrl: string + /** Optional JWT for authentication */ + jwt?: string + /** Request timeout in milliseconds (default: 30 000) */ + timeout?: number +} + +// --------------------------------------------------------------------------- +// Client factory +// --------------------------------------------------------------------------- + +/** + * Create a typed Transfer Instruction API client. + * + * The client mirrors the endpoints defined in `transfer-instruction-v1.yaml`. + */ +export function createTransferInstructionClient(config: TransferInstructionClientConfig) { + const baseUrl = config.baseUrl.replace(/\/$/, '') + const headers = buildHeaders(config.jwt) + const timeoutMs = config.timeout ?? 30_000 + + const appendScanProxyPath = (path: string) => `/api/validator/v0/scan-proxy${path}` + return { + /** + * Get the factory and choice context for executing a direct transfer. + * + * `POST /registry/transfer-instruction/v1/transfer-factory` + */ + async getTransferFactory( + request: GetFactoryRequest, + ): Promise { + return post( + baseUrl, + appendScanProxyPath('/registry/transfer-instruction/v1/transfer-factory'), + headers, + timeoutMs, + request, + ) + }, + + /** + * Get the choice context to **accept** a transfer instruction. + * + * `POST /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/accept` + */ + async getAcceptContext( + transferInstructionId: string, + request?: GetChoiceContextRequest, + ): Promise { + return post( + baseUrl, + appendScanProxyPath( + `/registry/transfer-instruction/v1/${encodeURIComponent(transferInstructionId)}/choice-contexts/accept`, + ), + headers, + timeoutMs, + request ?? {}, + ) + }, + + /** + * Get the choice context to **reject** a transfer instruction. + * + * `POST /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/reject` + */ + async getRejectContext( + transferInstructionId: string, + request?: GetChoiceContextRequest, + ): Promise { + return post( + baseUrl, + appendScanProxyPath( + `/registry/transfer-instruction/v1/${encodeURIComponent(transferInstructionId)}/choice-contexts/reject`, + ), + headers, + timeoutMs, + request ?? {}, + ) + }, + + /** + * Get the choice context to **withdraw** a transfer instruction. + * + * `POST /registry/transfer-instruction/v1/{transferInstructionId}/choice-contexts/withdraw` + */ + async getWithdrawContext( + transferInstructionId: string, + request?: GetChoiceContextRequest, + ): Promise { + return post( + baseUrl, + appendScanProxyPath( + `/registry/transfer-instruction/v1/${encodeURIComponent(transferInstructionId)}/choice-contexts/withdraw`, + ), + headers, + timeoutMs, + request ?? {}, + ) + }, + } +} + +/** + * Type alias for the Transfer Instruction client instance. + */ +export type TransferInstructionClient = ReturnType + +function buildHeaders(jwt?: string): Record { + const headers: Record = { 'Content-Type': 'application/json' } + if (jwt) headers['Authorization'] = `Bearer ${jwt}` + return headers +} diff --git a/ccip-sdk/src/canton/types.ts b/ccip-sdk/src/canton/types.ts index 8042b0e4..f0b9e157 100644 --- a/ccip-sdk/src/canton/types.ts +++ b/ccip-sdk/src/canton/types.ts @@ -33,3 +33,80 @@ export interface UnsignedCantonTx { /** The Canton command payload ready for submission. */ commands: JsCommands } + +/** + * Identifies a token on Canton (maps to `splice_api_token_holding_v1.InstrumentId`). + * + * Encoded as `"admin::tokenId"` in the SDK's `message.feeToken` string. + */ +export interface CantonInstrumentId { + admin: string + id: string +} + +/** + * Input for a single CCV that should verify the outbound send + * (maps to Go `ccipsender.CCVSendInput`). + */ +export interface CantonCCVSendInput { + ccvCid: string + ccvRawAddress: string + verifierArgs: string +} + +/** + * Token input carrying the Transfer Factory reference and metadata + * (maps to Go `interfaces.TokenInput`). + */ +export interface CantonTokenInput { + transferFactory: string + extraArgs: CantonTokenExtraArgs + tokenPoolHoldings: string[] +} + +/** + * Extra arguments attached to a Canton token input + * (maps to Go `splice_api_token_metadata_v1.ExtraArgs`). + */ +export interface CantonTokenExtraArgs { + context: { values: Record } + meta: { values: Record } +} + +/** + * Canton-specific send parameters passed via `message.extraArgs` when calling + * {@link CantonChain.generateUnsignedSendMessage} or {@link CantonChain.sendMessage}. + * + * These values cannot be derived automatically and must be supplied by the caller. + */ +export interface CantonExtraArgsV1 { + /** Contract IDs of pre-funded fee-token holdings owned by the sender party. */ + feeTokenHoldingCids: string[] + /** + * CCV raw instance address strings in `"instanceId@party"` format. + * The corresponding hex InstanceAddresses (keccak256 of each raw address) + * are derived automatically at runtime and used to query the EDS for disclosures. + * These are also passed verbatim as `ccvRawAddress.unpack` in the Send choice argument. + */ + ccvRawAddresses?: string[] + /** Gas limit for ccipReceive on the destination chain */ + gasLimit?: bigint | number +} + +/** + * Parse a fee-token string of the form `"admin::tokenId"` into a + * {@link CantonInstrumentId}. + * + * @throws {Error} if the string does not contain the `::` separator. + */ +export function parseInstrumentId(feeToken: string): CantonInstrumentId { + const sep = feeToken.split('::') + if (sep.length !== 3) { + throw new Error( + `Invalid Canton instrument ID "${feeToken}": expected "ad::min::tokenId" format`, + ) + } + const admin = [sep[0], sep[1]].join('::') + const id = sep[2]! + return { admin, id } +} diff --git a/ccip-sdk/src/chain.ts b/ccip-sdk/src/chain.ts index b32d21e9..9a037684 100644 --- a/ccip-sdk/src/chain.ts +++ b/ccip-sdk/src/chain.ts @@ -175,12 +175,18 @@ export type CantonConfig = { /** Party identifier for the Canton Ledger API. */ party: string + /** CCIP party identifier */ + ccipParty: string + /** JSON Web Token for authentication with the Canton Ledger API. */ jwt: string /** Base URL for the EDS (Explicit Disclosure Service) API. */ edsUrl: string + /** Base URL for the Transfer Instruction API. */ + transferInstructionUrl: string + /** Optional base URL for a transaction indexer to fetch CCV verifications; if not provided, default URL will be used. */ indexerUrl?: string } diff --git a/ccip-sdk/src/extra-args.ts b/ccip-sdk/src/extra-args.ts index e1536fc8..0d53668c 100644 --- a/ccip-sdk/src/extra-args.ts +++ b/ccip-sdk/src/extra-args.ts @@ -1,5 +1,6 @@ import { type BytesLike, id } from 'ethers' +import type { CantonExtraArgsV1 } from './canton/types.ts' import { CCIPChainFamilyUnsupportedError, CCIPExtraArgsParseError } from './errors/index.ts' import { supportedChains } from './supported-chains.ts' import { ChainFamily } from './types.ts' @@ -150,6 +151,7 @@ export type ExtraArgs = | GenericExtraArgsV3 | SVMExtraArgsV1 | SuiExtraArgsV1 + | CantonExtraArgsV1 /** * Encodes extra arguments for CCIP messages. From ddd85ff637222782452c0144f2835dc9e181fe66 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:19:22 +0100 Subject: [PATCH 11/17] fix acs provider --- .../canton/explicit-disclosures/acs.test.ts | 77 +++++++++++++++++-- .../src/canton/explicit-disclosures/acs.ts | 2 +- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts index 8087811f..60304cf0 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.test.ts @@ -20,8 +20,11 @@ function makeAcsEntry( contractId: string, createdEventBlob: string, signatory: string, - instanceId?: string, + opts?: { instanceId?: string; partyOwner?: string }, ): JsGetActiveContractsResponse { + const createArgument: Record = {} + if (opts?.instanceId) createArgument['instanceId'] = opts.instanceId + if (opts?.partyOwner) createArgument['partyOwner'] = opts.partyOwner return { contractEntry: { JsActiveContract: { @@ -31,7 +34,7 @@ function makeAcsEntry( contractId, createdEventBlob, signatories: [signatory], - createArgument: instanceId ? { instanceId } : {}, + createArgument, // remaining CreatedEvent fields — not used by fetchRichSnapshot observers: [], witnesses: [], @@ -54,12 +57,22 @@ const RECEIVER_CONTRACT_ID = 'receiver-cid-002' const RECEIVER_BLOB = 'receiver-blob' const RECEIVER_TEMPLATE_ID = 'pkg-receiver:CCIP.CCIPReceiver:CCIPReceiver' -/** ACS snapshot returned by the stub for the default two-contract scenario */ +const SENDER_CONTRACT_ID = 'sender-cid-003' +const SENDER_BLOB = 'sender-blob' +const SENDER_TEMPLATE_ID = 'pkg-sender:CCIP.CCIPSender:CCIPSender' + +/** ACS snapshot for the default execute scenario (router + receiver) */ const DEFAULT_ACS: JsGetActiveContractsResponse[] = [ - makeAcsEntry(ROUTER_TEMPLATE_ID, ROUTER_CONTRACT_ID, ROUTER_BLOB, PARTY), + makeAcsEntry(ROUTER_TEMPLATE_ID, ROUTER_CONTRACT_ID, ROUTER_BLOB, PARTY, { partyOwner: PARTY }), makeAcsEntry(RECEIVER_TEMPLATE_ID, RECEIVER_CONTRACT_ID, RECEIVER_BLOB, PARTY), ] +/** ACS snapshot for the default send scenario (router + sender) */ +const SEND_ACS: JsGetActiveContractsResponse[] = [ + makeAcsEntry(ROUTER_TEMPLATE_ID, ROUTER_CONTRACT_ID, ROUTER_BLOB, PARTY, { partyOwner: PARTY }), + makeAcsEntry(SENDER_TEMPLATE_ID, SENDER_CONTRACT_ID, SENDER_BLOB, PARTY), +] + // --------------------------------------------------------------------------- // Stub factory // --------------------------------------------------------------------------- @@ -102,7 +115,7 @@ describe('canton/acs', () => { const OTHER_BLOB = 'other-receiver-blob' const entries = [ ...DEFAULT_ACS, - makeAcsEntry(RECEIVER_TEMPLATE_ID, OTHER_CID, OTHER_BLOB, 'other-party::ff'), + makeAcsEntry(RECEIVER_TEMPLATE_ID, OTHER_CID, OTHER_BLOB, 'other-party::ff', {}), ] const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) @@ -119,7 +132,9 @@ describe('canton/acs', () => { }) it('fetchExecutionDisclosures throws when no matching PerPartyRouter is found', async () => { - const entries = [makeAcsEntry(RECEIVER_TEMPLATE_ID, RECEIVER_CONTRACT_ID, RECEIVER_BLOB, PARTY)] + const entries = [ + makeAcsEntry(RECEIVER_TEMPLATE_ID, RECEIVER_CONTRACT_ID, RECEIVER_BLOB, PARTY, {}), + ] const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) @@ -139,4 +154,54 @@ describe('canton/acs', () => { 'should throw mentioning the missing contract ID', ) }) + + // ------------------------------------------------------------------------- + // fetchSendDisclosures + // ------------------------------------------------------------------------- + + it('fetchSendDisclosures returns PerPartyRouter and CCIPSender', async () => { + const provider = new AcsDisclosureProvider(makeStubClient(SEND_ACS), { party: PARTY }) + + const disclosures = await provider.fetchSendDisclosures() + + assert.equal(disclosures.perPartyRouter.contractId, ROUTER_CONTRACT_ID) + assert.equal(disclosures.perPartyRouter.templateId, ROUTER_TEMPLATE_ID) + assert.equal(disclosures.perPartyRouter.createdEventBlob, ROUTER_BLOB) + assert.equal(disclosures.perPartyRouter.synchronizerId, SYNC_ID) + + assert.equal(disclosures.ccipSender.contractId, SENDER_CONTRACT_ID) + assert.equal(disclosures.ccipSender.templateId, SENDER_TEMPLATE_ID) + assert.equal(disclosures.ccipSender.createdEventBlob, SENDER_BLOB) + assert.equal(disclosures.ccipSender.synchronizerId, SYNC_ID) + }) + + it('fetchSendDisclosures throws when no matching CCIPSender is found', async () => { + // Only router in the ACS — no sender contract + const entries = [ + makeAcsEntry(ROUTER_TEMPLATE_ID, ROUTER_CONTRACT_ID, ROUTER_BLOB, PARTY, { + partyOwner: PARTY, + }), + ] + + const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) + + await assert.rejects( + () => provider.fetchSendDisclosures(), + /CCIPSender/, + 'should throw mentioning CCIPSender', + ) + }) + + it('fetchSendDisclosures throws when no matching PerPartyRouter is found', async () => { + // Only sender in the ACS — no router contract + const entries = [makeAcsEntry(SENDER_TEMPLATE_ID, SENDER_CONTRACT_ID, SENDER_BLOB, PARTY, {})] + + const provider = new AcsDisclosureProvider(makeStubClient(entries), { party: PARTY }) + + await assert.rejects( + () => provider.fetchSendDisclosures(), + /PerPartyRouter/, + 'should throw mentioning PerPartyRouter', + ) + }) }) diff --git a/ccip-sdk/src/canton/explicit-disclosures/acs.ts b/ccip-sdk/src/canton/explicit-disclosures/acs.ts index b3a7e876..7d297e5a 100644 --- a/ccip-sdk/src/canton/explicit-disclosures/acs.ts +++ b/ccip-sdk/src/canton/explicit-disclosures/acs.ts @@ -220,7 +220,7 @@ function pickByPartyOwner( contractType: CcipContractType, party: string, ): DisclosedContract { - const moduleEntity = CCIP_MODULE_ENTITIES[contractType] + const { moduleEntity } = CCIP_TEMPLATES[contractType] const candidates = snapshot.get(moduleEntity) ?? [] for (const c of candidates) { From fb7b8e1c46fbf21a72f488fcffe701bc5d774e0b Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:30:46 +0100 Subject: [PATCH 12/17] add canton exec cmd --- ccip-cli/src/index.ts | 16 ++++++++++++++++ ccip-cli/src/providers/canton.ts | 27 +++++++++++++++++++++++++++ ccip-cli/src/providers/index.ts | 28 +++++++++++++++++++++++++--- ccip-sdk/src/index.ts | 13 ++++++++++++- 4 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 ccip-cli/src/providers/canton.ts diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index d4a20b27..0d35d502 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -52,6 +52,22 @@ const globalOpts = { return arg // it's a URL string }, }, + 'canton-jwt': { + type: 'string', + describe: 'JWT bearer token for Canton Ledger API authentication', + }, + 'canton-party': { + type: 'string', + describe: 'Daml party ID for Canton command submissions (e.g. participant1::12208...)', + }, + 'canton-eds-url': { + type: 'string', + describe: 'Base URL for the Canton Explicit Disclosure Service (EDS)', + }, + 'indexer-url': { + type: 'string', + describe: '(Optional) Base URL for the CCV indexer service', + }, } as const /** Type for global CLI options. */ diff --git a/ccip-cli/src/providers/canton.ts b/ccip-cli/src/providers/canton.ts new file mode 100644 index 00000000..a08ce627 --- /dev/null +++ b/ccip-cli/src/providers/canton.ts @@ -0,0 +1,27 @@ +import { type CantonWallet, CCIPArgumentInvalidError } from '@chainlink/ccip-sdk/src/index.ts' + +/** + * Loads a Canton wallet from the provided options. + * + * A Canton "wallet" is simply a Daml party ID. The party can be supplied via: + * - `--wallet ` (if the value contains `::`, it's treated as a party ID) + * - `--canton-party ` (explicit Canton party flag) + * + * @param opts - CLI options containing wallet and/or canton-party values. + * @returns A {@link CantonWallet} with the resolved party ID. + * @throws {@link CCIPArgumentInvalidError} if no valid party ID can be resolved. + */ +export function loadCantonWallet(opts: { wallet?: unknown; cantonParty?: string }): CantonWallet { + // Prefer --wallet if it looks like a Daml party ID (contains `::`) + if (typeof opts.wallet === 'string' && opts.wallet.includes('::')) { + return { party: opts.wallet } + } + // Fall back to --canton-party + if (opts.cantonParty) { + return { party: opts.cantonParty } + } + throw new CCIPArgumentInvalidError( + 'wallet', + 'Canton requires a Daml party ID via --wallet or --canton-party ', + ) +} diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index a3458af5..b6388992 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -17,6 +17,7 @@ import { } from '@chainlink/ccip-sdk/src/index.ts' import { loadAptosWallet } from './aptos.ts' +import { loadCantonWallet } from './canton.ts' import { loadEvmWallet } from './evm.ts' import { loadSolanaWallet } from './solana.ts' import { loadSuiWallet } from './sui.ts' @@ -55,11 +56,17 @@ async function collectEndpoints( export function fetchChainsFromRpcs( ctx: Ctx, - argv: Pick, + argv: Pick< + GlobalOpts, + 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + >, ): ChainGetter export function fetchChainsFromRpcs( ctx: Ctx, - argv: Pick, + argv: Pick< + GlobalOpts, + 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + >, txHash: string, ): [ChainGetter, Promise<[Chain, ChainTransaction]>] @@ -74,9 +81,20 @@ export function fetchChainsFromRpcs( */ export function fetchChainsFromRpcs( ctx: Ctx, - argv: Pick, + argv: Pick< + GlobalOpts, + 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + >, txHash?: string, ) { + const cantonConfig = argv.cantonJwt + ? { + jwt: argv.cantonJwt, + party: argv.cantonParty ?? '', + edsUrl: argv.cantonEdsUrl ?? '', + indexerUrl: argv.indexerUrl, + } + : undefined const chains: Record> = {} const chainsCbs: Record< string, @@ -104,6 +122,7 @@ export function fetchChainsFromRpcs( for (const url of endpoints) { const chain$ = C.fromUrl(url, { ...ctx, + ...(cantonConfig ? { cantonConfig } : {}), apiClient: argv.api === false ? null : typeof argv.api === 'string' ? argv.api : undefined, }) @@ -222,6 +241,9 @@ export async function loadChainWallet(chain: Chain, argv: { wallet?: unknown; rp chain.network.networkType === NetworkType.Testnet, ) return [wallet.getAddress(), wallet] as const + case ChainFamily.Canton: + wallet = loadCantonWallet(argv) + return [wallet.party, wallet] as const default: // TypeScript exhaustiveness check - this should never be reached throw new CCIPChainFamilyUnsupportedError((chain.network as { family: string }).family) diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index a6578d15..f9c00c0c 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -103,6 +103,7 @@ export * from './errors/index.ts' // chains import { AptosChain } from './aptos/index.ts' export type { UnsignedAptosTx } from './aptos/index.ts' +import { CantonChain } from './canton/index.ts' import { EVMChain } from './evm/index.ts' export type { UnsignedEVMTx } from './evm/index.ts' import { SolanaChain } from './solana/index.ts' @@ -111,7 +112,17 @@ import { SuiChain } from './sui/index.ts' export type { UnsignedSuiTx } from './sui/index.ts' import { TONChain } from './ton/index.ts' export type { UnsignedTONTx } from './ton/index.ts' +export type { CantonWallet, UnsignedCantonTx } from './canton/index.ts' import { ChainFamily, NetworkType } from './types.ts' -export { AptosChain, ChainFamily, EVMChain, NetworkType, SolanaChain, SuiChain, TONChain } +export { + AptosChain, + CantonChain, + ChainFamily, + EVMChain, + NetworkType, + SolanaChain, + SuiChain, + TONChain, +} // use `supportedChains` to override/register derived classes, if needed export { supportedChains } from './supported-chains.ts' From 5c4278d8ec7d564580fd6f89cc4d397aed082fa3 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:53:56 +0100 Subject: [PATCH 13/17] add canton get tx method --- ccip-sdk/src/canton/client/client.ts | 39 ++++++++++++++ ccip-sdk/src/canton/client/index.ts | 1 + ccip-sdk/src/canton/index.ts | 77 +++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index a259e2cd..be5e5aa0 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -11,6 +11,8 @@ export type SubmitAndWaitResponse = components['schemas']['SubmitAndWaitResponse /** Full transaction response including all events */ export type JsSubmitAndWaitForTransactionResponse = components['schemas']['JsSubmitAndWaitForTransactionResponse'] +/** A single transaction from the ledger */ +export type JsTransaction = components['schemas']['JsTransaction'] /** Request to get active contracts */ export type GetActiveContractsRequest = components['schemas']['GetActiveContractsRequest'] /** Response containing active contracts */ @@ -187,6 +189,43 @@ export function createCantonClient(config: CantonClientConfig) { } }, + /** + * Fetch a transaction by its update ID without requiring a known party. + * Uses `filtersForAnyParty` with a wildcard so all visible events are returned. + * @param updateId - The update ID (Canton transaction hash) + * @returns The full `JsTransaction` including all events + */ + async getTransactionById(updateId: string): Promise { + const response = await ledgerPost<{ transaction: JsTransaction }>( + baseUrl, + '/v2/updates/transaction-by-id', + headers, + timeoutMs, + { + updateId, + transactionFormat: { + eventFormat: { + filtersByParty: {}, + filtersForAnyParty: { + cumulative: [ + { + identifierFilter: { + WildcardFilter: { + value: { includeCreatedEventBlob: false }, + }, + }, + }, + ], + }, + verbose: true, + }, + transactionShape: 'TRANSACTION_SHAPE_LEDGER_EFFECTS', + }, + }, + ) + return response.transaction + }, + /** * Get update by ID * @param updateId - The update ID returned from submit-and-wait diff --git a/ccip-sdk/src/canton/client/index.ts b/ccip-sdk/src/canton/client/index.ts index f40be8f1..a7cfcdf8 100644 --- a/ccip-sdk/src/canton/client/index.ts +++ b/ccip-sdk/src/canton/client/index.ts @@ -16,6 +16,7 @@ export { type JsCommands, type JsGetActiveContractsResponse, type JsSubmitAndWaitForTransactionResponse, + type JsTransaction, type SubmitAndWaitResponse, type TemplateFilter, type TransactionFilter, diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 02092e50..a8248174 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -40,7 +40,12 @@ import { ChainFamily, } from '../types.ts' import { networkInfo } from '../utils.ts' -import { type CantonClient, type JsCommands, createCantonClient } from './client/index.ts' +import { + type CantonClient, + type JsCommands, + type JsTransaction, + createCantonClient, +} from './client/index.ts' import { parseCantonExecutionReceipt, parseCantonSendResult, resolveTimestamp } from './events.ts' import { AcsDisclosureProvider } from './explicit-disclosures/acs.ts' import { EdsDisclosureProvider } from './explicit-disclosures/eds.ts' @@ -142,7 +147,7 @@ export class CantonChain extends Chain { ['mainnet', 'canton:MainNet'], ['main', 'canton:MainNet'], ['canton-mainnet', 'canton:MainNet'], - ['global', 'canton:DevNet'], + ['global', 'canton:LocalNet'], ]) /** @@ -260,18 +265,76 @@ export class CantonChain extends Chain { } /** - * {@inheritDoc Chain.getTransaction} - * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) + * Fetches a Canton transaction (update) by its update ID. + * + * The ledger is queried via `/v2/updates/transaction-by-id` with a wildcard + * party filter so that all visible events are returned without requiring a + * known party ID. + * + * Canton concepts are mapped to {@link ChainTransaction} fields as follows: + * - `hash` — the Canton `updateId` + * - `blockNumber` — the ledger `offset` + * - `timestamp` — `effectiveAt` parsed to Unix seconds + * - `from` — first `actingParties` entry of the first exercised event + * - `logs` — one {@link ChainLog} per transaction event + * + * @param hash - The Canton update ID (transaction hash) to look up. + * @returns A {@link ChainTransaction} with events mapped to logs. */ - getTransaction(_hash: string): Promise { - throw new CCIPNotImplementedError('CantonChain.getTransaction') + async getTransaction(hash: string): Promise { + const tx: JsTransaction = await this.provider.getTransactionById(hash) + + const timestamp = tx.effectiveAt + ? Math.floor(new Date(tx.effectiveAt).getTime() / 1000) + : Math.floor(Date.now() / 1000) + + // Extract the submitter from the first exercised event's actingParties. + let from = '' + for (const event of tx.events) { + const ev = event as Record + const exercised = ev['ExercisedEvent'] as Record | undefined + if ( + exercised?.actingParties && + Array.isArray(exercised.actingParties) && + exercised.actingParties.length > 0 + ) { + from = String(exercised.actingParties[0]) + break + } + } + + // Build one ChainLog per event. Events can be + // { CreatedEvent: ... }, { ExercisedEvent: ... }, or { ArchivedEvent: ... }. + const logs: ChainLog[] = tx.events.map((event, index) => { + const ev = event as Record + const inner = (ev['CreatedEvent'] ?? + ev['ExercisedEvent'] ?? + ev['ArchivedEvent'] ?? + ev) as Record + const templateId = typeof inner['templateId'] === 'string' ? inner['templateId'] : '' + return { + address: templateId, + transactionHash: hash, + index, + blockNumber: tx.offset, + topics: templateId ? [templateId] : [], + data: inner, + } + }) + + return { + hash, + blockNumber: tx.offset, + timestamp, + from, + logs, + } } /** * {@inheritDoc Chain.getLogs} * @throws {@link CCIPNotImplementedError} always (not yet implemented for Canton) */ - // eslint-disable-next-line require-yield getLogs(_opts: LogFilter): AsyncIterableIterator { throw new CCIPNotImplementedError('CantonChain.getLogs') } From 850eb05ccb49975885b15b05a58db5355931dc12 Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:10:56 +0100 Subject: [PATCH 14/17] add build artifacts --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index 0d35d502..bbf9062d 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -11,7 +11,7 @@ import { Format } from './commands/index.ts' util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.1.1-f915d74' +const VERSION = '1.1.1-7c1e223' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 5eb7a6eb..10543bc2 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -60,7 +60,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.1.1-f915d74' +export const SDK_VERSION = '1.1.1-7c1e223' // generate:end /** SDK telemetry header name */ From 15bba1ee36d69406031f994d0c618e44b326f08a Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:09:35 +0100 Subject: [PATCH 15/17] fix rebase --- ccip-cli/src/index.ts | 10 +++++++- ccip-cli/src/providers/index.ts | 35 ++++++++++++++++++++++++---- ccip-sdk/package.json | 1 - ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/canton/client/client.ts | 2 +- ccip-sdk/src/index.ts | 1 + 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index bbf9062d..f65d90cb 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -11,7 +11,7 @@ import { Format } from './commands/index.ts' util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.1.1-7c1e223' +const VERSION = '1.3.1-850eb05' // generate:end const globalOpts = { @@ -68,6 +68,14 @@ const globalOpts = { type: 'string', describe: '(Optional) Base URL for the CCV indexer service', }, + 'canton-ccip-party': { + type: 'string', + describe: 'Daml party ID for Canton CCIP interactions (e.g. participant1::12208...)', + }, + 'canton-transfer-instruction-url': { + type: 'string', + describe: 'Base URL for fetching transfer instructions from Canton (if different from EDS URL)', + }, } as const /** Type for global CLI options. */ diff --git a/ccip-cli/src/providers/index.ts b/ccip-cli/src/providers/index.ts index b6388992..b07590e1 100644 --- a/ccip-cli/src/providers/index.ts +++ b/ccip-cli/src/providers/index.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { + type CantonConfig, type Chain, type ChainGetter, type ChainTransaction, @@ -58,14 +59,30 @@ export function fetchChainsFromRpcs( ctx: Ctx, argv: Pick< GlobalOpts, - 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + | 'rpcs' + | 'rpcsFile' + | 'api' + | 'cantonJwt' + | 'cantonParty' + | 'cantonEdsUrl' + | 'indexerUrl' + | 'cantonCcipParty' + | 'cantonTransferInstructionUrl' >, ): ChainGetter export function fetchChainsFromRpcs( ctx: Ctx, argv: Pick< GlobalOpts, - 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + | 'rpcs' + | 'rpcsFile' + | 'api' + | 'cantonJwt' + | 'cantonParty' + | 'cantonEdsUrl' + | 'indexerUrl' + | 'cantonCcipParty' + | 'cantonTransferInstructionUrl' >, txHash: string, ): [ChainGetter, Promise<[Chain, ChainTransaction]>] @@ -83,16 +100,26 @@ export function fetchChainsFromRpcs( ctx: Ctx, argv: Pick< GlobalOpts, - 'rpcs' | 'rpcsFile' | 'api' | 'cantonJwt' | 'cantonParty' | 'cantonEdsUrl' | 'indexerUrl' + | 'rpcs' + | 'rpcsFile' + | 'api' + | 'cantonJwt' + | 'cantonParty' + | 'cantonEdsUrl' + | 'indexerUrl' + | 'cantonCcipParty' + | 'cantonTransferInstructionUrl' >, txHash?: string, ) { - const cantonConfig = argv.cantonJwt + const cantonConfig: CantonConfig | undefined = argv.cantonJwt ? { jwt: argv.cantonJwt, party: argv.cantonParty ?? '', edsUrl: argv.cantonEdsUrl ?? '', indexerUrl: argv.indexerUrl, + ccipParty: argv.cantonCcipParty ?? '', + transferInstructionUrl: argv.cantonTransferInstructionUrl ?? '', } : undefined const chains: Record> = {} diff --git a/ccip-sdk/package.json b/ccip-sdk/package.json index 8425fcd4..3017f241 100644 --- a/ccip-sdk/package.json +++ b/ccip-sdk/package.json @@ -94,7 +94,6 @@ "got": "^11.8.6", "micro-memoize": "^5.1.1", "type-fest": "^5.5.0", - "openapi-fetch": "^0.17.0", "yaml": "2.8.2" }, "overrides": { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 10543bc2..0f31e170 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -60,7 +60,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.1.1-7c1e223' +export const SDK_VERSION = '1.3.1-850eb05' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index be5e5aa0..c574ff2f 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -196,7 +196,7 @@ export function createCantonClient(config: CantonClientConfig) { * @returns The full `JsTransaction` including all events */ async getTransactionById(updateId: string): Promise { - const response = await ledgerPost<{ transaction: JsTransaction }>( + const response = await post<{ transaction: JsTransaction }>( baseUrl, '/v2/updates/transaction-by-id', headers, diff --git a/ccip-sdk/src/index.ts b/ccip-sdk/src/index.ts index f9c00c0c..f52f552f 100644 --- a/ccip-sdk/src/index.ts +++ b/ccip-sdk/src/index.ts @@ -25,6 +25,7 @@ export { export type { ApiRetryConfig, + CantonConfig, Chain, ChainContext, ChainGetter, From 22c1ec2acdb928bf0b1a01b57586905a386ad0ff Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:19:42 +0100 Subject: [PATCH 16/17] fix lock and extra args --- ccip-cli/src/index.ts | 2 +- ccip-sdk/src/api/index.ts | 2 +- ccip-sdk/src/canton/index.ts | 2 +- ccip-sdk/src/canton/types.ts | 2 +- ccip-sdk/src/solana/index.ts | 2 +- package-lock.json | 105 +++++++++++++---------------------- 6 files changed, 45 insertions(+), 70 deletions(-) diff --git a/ccip-cli/src/index.ts b/ccip-cli/src/index.ts index f65d90cb..306fa5c7 100755 --- a/ccip-cli/src/index.ts +++ b/ccip-cli/src/index.ts @@ -11,7 +11,7 @@ import { Format } from './commands/index.ts' util.inspect.defaultOptions.depth = 6 // print down to tokenAmounts in requests // generate:nofail // `const VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -const VERSION = '1.3.1-850eb05' +const VERSION = '1.3.1-15bba1e' // generate:end const globalOpts = { diff --git a/ccip-sdk/src/api/index.ts b/ccip-sdk/src/api/index.ts index 0f31e170..6cf36797 100644 --- a/ccip-sdk/src/api/index.ts +++ b/ccip-sdk/src/api/index.ts @@ -60,7 +60,7 @@ export const DEFAULT_TIMEOUT_MS = 30000 /** SDK version string for telemetry header */ // generate:nofail // `export const SDK_VERSION = '${require('./package.json').version}-${require('child_process').execSync('git rev-parse --short HEAD').toString().trim()}'` -export const SDK_VERSION = '1.3.1-850eb05' +export const SDK_VERSION = '1.3.1-15bba1e' // generate:end /** SDK telemetry header name */ diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index a8248174..272feaf9 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -499,7 +499,7 @@ export class CantonChain extends Chain { const payloadHex = message.data ? stripHexPrefix(typeof message.data === 'string' ? message.data : String(message.data)) : '' - const gasLimit = cantonArgs.gasLimit ?? 200_000 + const gasLimit = cantonArgs.gasLimit ?? 200_000n const feeTokenHoldingCids = cantonArgs.feeTokenHoldingCids const ccvRawAddresses = cantonArgs.ccvRawAddresses ?? [] // Derive hex InstanceAddresses at runtime: keccak256 of each raw "instanceId@party" string diff --git a/ccip-sdk/src/canton/types.ts b/ccip-sdk/src/canton/types.ts index f0b9e157..c83f91aa 100644 --- a/ccip-sdk/src/canton/types.ts +++ b/ccip-sdk/src/canton/types.ts @@ -90,7 +90,7 @@ export interface CantonExtraArgsV1 { */ ccvRawAddresses?: string[] /** Gas limit for ccipReceive on the destination chain */ - gasLimit?: bigint | number + gasLimit?: bigint } /** diff --git a/ccip-sdk/src/solana/index.ts b/ccip-sdk/src/solana/index.ts index e82c3aac..f78244c0 100644 --- a/ccip-sdk/src/solana/index.ts +++ b/ccip-sdk/src/solana/index.ts @@ -878,7 +878,7 @@ export class SolanaChain extends Chain { */ static encodeExtraArgs(args: ExtraArgs): string { if ('computeUnits' in args) throw new CCIPSolanaExtraArgsEncodingError() - const gasLimitUint128Le = toLeArray(args.gasLimit, 16) + const gasLimitUint128Le = toLeArray(args.gasLimit ?? 0n, 16) return concat([ EVMExtraArgsV2Tag, gasLimitUint128Le, diff --git a/package-lock.json b/package-lock.json index bd4a56ad..8966c779 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,7 @@ }, "ccip-api-ref": { "name": "@chainlink/ccip-api-ref", - "version": "1.1.1", + "version": "1.3.1", "dependencies": { "@chainlink/ccip-sdk": "*", "@chainlink/design-system": "^0.2.8", @@ -63,25 +63,24 @@ } }, "ccip-api-ref/node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, "ccip-api-ref/node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, "ccip-api-ref/node_modules/scheduler": { @@ -178,7 +177,7 @@ "ethers": "6.16.0", "got": "^11.8.6", "micro-memoize": "^5.1.1", - "type-fest": "^5.4.4", + "type-fest": "^5.5.0", "yaml": "2.8.2" }, "devDependencies": { @@ -386,7 +385,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.49.2.tgz", "integrity": "sha512-y1IOpG6OSmTpGg/CT0YBb/EAhR2nsC18QWp9Jy8HO9iGySpcwaTvs5kHa17daP3BMTwWyaX9/1tDTDQshZzXdg==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.49.2", "@algolia/requester-browser-xhr": "5.49.2", @@ -599,7 +597,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2545,7 +2542,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2568,7 +2564,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2678,7 +2673,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3100,7 +3094,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4247,7 +4240,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.9.2.tgz", "integrity": "sha512-C5wZsGuKTY8jEYsqdxhhFOe1ZDjH0uIYJ9T/jebHwkyxqnr4wW0jTkB72OMqNjsoQRcb0JN3PcSeTwFlVgzCZg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.9.2", "@docusaurus/logger": "3.9.2", @@ -4544,7 +4536,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.9.2.tgz", "integrity": "sha512-6c4DAbR6n6nPbnZhY2V3tzpnKnGL+6aOsLvFL26VRqhlczli9eWG0VDUNoCQEPnGwDMhPS42UhSAnz5pThm5Ag==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.9.2", "@docusaurus/module-type-aliases": "3.9.2", @@ -4690,7 +4681,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.9.2.tgz", "integrity": "sha512-lBSBiRruFurFKXr5Hbsl2thmGweAPmddhF3jb99U4EMDA5L+e5Y1rAkOS07Nvrup7HUMBDrCV45meaxZnt28nQ==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/types": "3.9.2", @@ -4736,7 +4726,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.9.2.tgz", "integrity": "sha512-l7yk3X5VnNmATbwijJkexdhulNsQaNDwoagiwujXoxFbWLcxHQqNQ+c/IAlzrfMMOfa/8xSBZ7KEKDesE/2J7A==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.9.2", "@docusaurus/utils": "3.9.2", @@ -7571,7 +7560,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -9248,7 +9236,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.4.tgz", "integrity": "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -9465,7 +9452,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -10176,7 +10162,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -10204,7 +10189,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -10602,7 +10586,6 @@ "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", @@ -11678,7 +11661,6 @@ "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/wevm" }, @@ -11722,7 +11704,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -11817,7 +11798,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -11882,7 +11862,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.49.2.tgz", "integrity": "sha512-1K0wtDaRONwfhL4h8bbJ9qTjmY6rhGgRvvagXkMBsAOMNr+3Q2SffHECh9DIuNVrMA1JwA0zCwhyepgBZVakng==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.15.2", "@algolia/client-abtesting": "5.49.2", @@ -12790,7 +12769,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -13227,7 +13205,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.2.tgz", "integrity": "sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.1.2", "@chevrotain/gast": "11.1.2", @@ -14094,7 +14071,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -14414,7 +14390,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -14836,7 +14811,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -17844,7 +17818,6 @@ "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", "license": "MIT", - "peer": true, "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -17904,7 +17877,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -23569,7 +23541,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -24516,7 +24487,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -25522,7 +25492,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -26191,7 +26160,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -26700,7 +26668,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -26713,7 +26680,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -26756,7 +26722,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -26817,7 +26782,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -26895,7 +26859,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -26919,7 +26882,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -27083,8 +27045,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -27895,7 +27856,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -27988,12 +27948,6 @@ "node": ">=11.0.0" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, "node_modules/schema-dts": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", @@ -28024,7 +27978,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -29360,7 +29313,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -29728,6 +29680,35 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -29890,8 +29871,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsx": { "version": "4.21.0", @@ -30117,7 +30097,6 @@ "integrity": "sha512-2iunh2ALyfyh204OF7h2u0kuQ84xB3jFZtFyUr01nThJkLvR8oGGSSDlyt2gyO4kXhvUxDcVbO0y43+qX+wFbw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 18" }, @@ -30146,7 +30125,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -30607,7 +30585,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "napi-postinstall": "^0.3.0" }, @@ -31236,7 +31213,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -31979,7 +31955,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, From 6047f6d0525a630704decc55c96cd5bf9644526b Mon Sep 17 00:00:00 2001 From: RodrigoAD <15104916+RodrigoAD@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:38:46 +0100 Subject: [PATCH 17/17] fix lint --- ccip-sdk/src/canton/client/client.ts | 4 ++-- ccip-sdk/src/canton/index.ts | 13 +++++++------ ccip-sdk/src/canton/types.ts | 8 +++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/ccip-sdk/src/canton/client/client.ts b/ccip-sdk/src/canton/client/client.ts index c574ff2f..a7972659 100644 --- a/ccip-sdk/src/canton/client/client.ts +++ b/ccip-sdk/src/canton/client/client.ts @@ -378,7 +378,7 @@ async function request( * @param timeoutMs - Timeout for the request in milliseconds. * @param queryParams - Optional query parameters to append to the URL. * @returns A promise resolving to the parsed response of type T. - * @throws {CantonApiError} If the request fails or the response is not OK. + * @throws {@link CantonApiError} If the request fails or the response is not OK. */ export async function get( baseUrl: string, @@ -400,7 +400,7 @@ export async function get( * @param body - The request payload to send as JSON. * @param queryParams - Optional query parameters to append to the URL. * @returns A promise resolving to the parsed response of type T. - * @throws {CantonApiError} If the request fails or the response is not OK. + * @throws {@link CantonApiError} If the request fails or the response is not OK. */ export async function post( baseUrl: string, diff --git a/ccip-sdk/src/canton/index.ts b/ccip-sdk/src/canton/index.ts index 272feaf9..08daa39f 100644 --- a/ccip-sdk/src/canton/index.ts +++ b/ccip-sdk/src/canton/index.ts @@ -461,6 +461,7 @@ export class CantonChain extends Chain { throw new CCIPNotImplementedError('CantonChain.getFee') } + /** {@inheritDoc Chain.generateUnsignedSendMessage} */ override async generateUnsignedSendMessage( opts: Parameters[0], ): Promise { @@ -483,7 +484,7 @@ export class CantonChain extends Chain { } const cantonArgs = message.extraArgs as CantonExtraArgsV1 | undefined - if (!cantonArgs?.feeTokenHoldingCids?.length) { + if (!cantonArgs?.feeTokenHoldingCids.length) { throw new CCIPError( CCIPErrorCode.METHOD_UNSUPPORTED, 'CantonChain.generateUnsignedSendMessage: message.extraArgs.feeTokenHoldingCids is required. ' + @@ -624,7 +625,7 @@ export class CantonChain extends Chain { // Step 8 — Merge all disclosed contracts const transferFactoryDisclosures: DisclosedContract[] = - transferFactoryResponse.choiceContext.disclosedContracts ?? [] + transferFactoryResponse.choiceContext.disclosedContracts const allDisclosed: DisclosedContract[] = [ acsDisclosures.perPartyRouter, @@ -686,7 +687,7 @@ export class CantonChain extends Chain { // Submit and wait for the full transaction (so we get events back) const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) - const txRecord = (response.transaction ?? response) as Record + const txRecord = response.transaction as Record const updateId: string = (typeof txRecord.update_id === 'string' ? txRecord.update_id : null) ?? (typeof txRecord.updateId === 'string' ? txRecord.updateId : '') @@ -694,7 +695,7 @@ export class CantonChain extends Chain { this.logger.debug(`CantonChain.sendMessage: submitted, updateId=${updateId}`) // Parse CCIPMessageSent from the transaction events - const sendResult = parseCantonSendResult(response.transaction ?? response, updateId) + const sendResult = parseCantonSendResult(response.transaction, updateId) const timestamp = resolveTimestamp(txRecord) @@ -920,13 +921,13 @@ export class CantonChain extends Chain { // Submit and wait for the full transaction (so we get events back) const response = await this.provider.submitAndWaitForTransaction(unsigned.commands) - const txRecord = (response.transaction ?? response) as Record + const txRecord = response.transaction as Record const updateId: string = (typeof txRecord.update_id === 'string' ? txRecord.update_id : null) ?? (typeof txRecord.updateId === 'string' ? txRecord.updateId : '') // Parse execution receipt from the transaction events - const receipt = parseCantonExecutionReceipt(response.transaction ?? response, updateId) + const receipt = parseCantonExecutionReceipt(response.transaction, updateId) const timestamp = resolveTimestamp(txRecord) // Build a synthetic ChainLog — Canton doesn't have EVM-style logs, but the diff --git a/ccip-sdk/src/canton/types.ts b/ccip-sdk/src/canton/types.ts index c83f91aa..3aa9a182 100644 --- a/ccip-sdk/src/canton/types.ts +++ b/ccip-sdk/src/canton/types.ts @@ -1,5 +1,6 @@ import type { ChainFamily } from '../types.ts' import type { JsCommands } from './client/index.ts' +import { CCIPArgumentInvalidError } from '../errors/specialized.ts' /** * A Canton "wallet" identifies the acting party and optionally overrides the @@ -97,13 +98,14 @@ export interface CantonExtraArgsV1 { * Parse a fee-token string of the form `"admin::tokenId"` into a * {@link CantonInstrumentId}. * - * @throws {Error} if the string does not contain the `::` separator. + * @throws {@link CCIPArgumentInvalidError} if the string does not contain the `::` separator. */ export function parseInstrumentId(feeToken: string): CantonInstrumentId { const sep = feeToken.split('::') if (sep.length !== 3) { - throw new Error( - `Invalid Canton instrument ID "${feeToken}": expected "ad::min::tokenId" format`, + throw new CCIPArgumentInvalidError( + 'feeToken', + `invalid Canton instrument ID "${feeToken}": expected "admin::tokenId" format`, ) } const admin = [sep[0], sep[1]].join('::')