diff --git a/docs/wallet-integration-guide/examples/__snapshots__/snippets.test.ts.snap b/docs/wallet-integration-guide/examples/__snapshots__/snippets.test.ts.snap index 15cbd48b0..787cf18f6 100644 --- a/docs/wallet-integration-guide/examples/__snapshots__/snippets.test.ts.snap +++ b/docs/wallet-integration-guide/examples/__snapshots__/snippets.test.ts.snap @@ -46,6 +46,8 @@ exports[`testing doc snippets > monitor-transaction-holdings.ts 1`] = `undefined exports[`testing doc snippets > party.ts 1`] = `undefined`; +exports[`testing doc snippets > plugin.ts 1`] = `undefined`; + exports[`testing doc snippets > prepare-incoming-command.ts 1`] = `undefined`; exports[`testing doc snippets > prepare-transfer-transaction.ts 1`] = `undefined`; diff --git a/docs/wallet-integration-guide/examples/snippets/plugin.ts b/docs/wallet-integration-guide/examples/snippets/plugin.ts new file mode 100644 index 000000000..500a0cd0a --- /dev/null +++ b/docs/wallet-integration-guide/examples/snippets/plugin.ts @@ -0,0 +1,33 @@ +import { SDK, SDKContext, SDKPlugin } from '@canton-network/wallet-sdk' + +export default async function () { + const sdk = ( + await SDK.create({ + auth: { + method: 'self_signed', + issuer: 'unsafe-auth', + credentials: { + clientId: 'ledger-api-user', + clientSecret: 'unsafe', + audience: 'https://canton.network.global', + scope: '', + }, + }, + ledgerClientUrl: 'http://localhost:2975', + }) + ).registerPlugins({ + myPlugin: class extends SDKPlugin { + // wallet-sdk plugin should always accept SDKContext + constructor(protected readonly ctx: SDKContext) { + super('myPlugin', ctx) + } + + myMethod() { + // do some logic + return + } + }, + }) + + sdk.myPlugin.myMethod() +} diff --git a/docs/wallet-integration-guide/src/wallet-sdk-configuration/index.rst b/docs/wallet-integration-guide/src/wallet-sdk-configuration/index.rst index 14a421c61..e24543f6e 100644 --- a/docs/wallet-integration-guide/src/wallet-sdk-configuration/index.rst +++ b/docs/wallet-integration-guide/src/wallet-sdk-configuration/index.rst @@ -88,7 +88,7 @@ The wallet-sdk can either take in a Provider (which will have auth bundled into In our examples, we have provided a default TokenProviderConfig for connecting to localnet, which uses a self-signed token. .. code-block:: javascript - + { method: 'self_signed', issuer: 'unsafe-auth', @@ -129,7 +129,7 @@ SDK using a different TokenProviderConfig. The following programmatic methods of configUrl: string credentials: ClientCredentials } - + export interface ClientCredentials { clientId: string clientSecret: string @@ -138,3 +138,27 @@ SDK using a different TokenProviderConfig. The following programmatic methods of } +Registering Plugins +------------------- + +The Wallet SDK supports extending its functionality through a plugin system. Plugins allow you to add custom methods and functionality +to the SDK instance while maintaining access to the SDK context and logger. + +Creating and Registering a Plugin +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To create a plugin, extend the ``SDKPlugin`` class and implement your custom functionality. Plugins are registered using the +``registerPlugins`` method, which accepts a record of plugin constructors keyed by their desired property names. + +.. literalinclude:: ../../examples/snippets/plugin.ts + :language: typescript + :dedent: + +Key Points +^^^^^^^^^^ + +- **Plugin Constructor**: Plugin classes must accept ``SDKContext`` as a constructor parameter and pass it to the ``super()`` call along with the plugin name. +- **Type Safety**: The ``registerPlugins`` method provides full type safety, ensuring that registered plugins are accessible with proper autocompletion and type checking. +- **Access to SDK Context**: Plugins have access to the SDK's context, logger, and other internal utilities through the ``ctx`` property. +- **Multiple Plugins**: You can register multiple plugins at once by passing them in a single object to ``registerPlugins``. + diff --git a/sdk/wallet-sdk/src/wallet/init/index.ts b/sdk/wallet-sdk/src/wallet/init/index.ts index e43196569..1d8ca6c87 100644 --- a/sdk/wallet-sdk/src/wallet/init/index.ts +++ b/sdk/wallet-sdk/src/wallet/init/index.ts @@ -3,3 +3,4 @@ export * from './initializedSDK.js' export * from './types/index.js' +export { SDKPlugin } from './plugin.js' diff --git a/sdk/wallet-sdk/src/wallet/init/initializedSDK.ts b/sdk/wallet-sdk/src/wallet/init/initializedSDK.ts index 57d7dc701..b2e881813 100644 --- a/sdk/wallet-sdk/src/wallet/init/initializedSDK.ts +++ b/sdk/wallet-sdk/src/wallet/init/initializedSDK.ts @@ -19,6 +19,7 @@ import { ExtendedFullSDKInterface, ExtendedSDKOptions, OfflineSDKInterface, + RegisteredPlugins, SDKInterface, TokenConfig, } from './types/index.js' @@ -32,6 +33,7 @@ import { TokenStandardService } from '@canton-network/core-token-standard-servic import { SDKLogger } from '../logger/logger.js' import { AmuletNamespace } from '../namespace/amulet/namespace.js' import { EventsNamespace } from '../namespace/events/index.js' +import { SDKPlugin } from './plugin.js' const createNamespace: { [K in keyof ExtendedSDKOptions]: ( @@ -155,6 +157,24 @@ export class InitializedSDK implements BasicSDKInterface { ) { return await ExtendedInitializedSDK.create(this.ctx, config) } + + public registerPlugins< + P extends Record SDKPlugin>, + >(plugins: P): InitializedSDK & RegisteredPlugins

{ + const newSDK = new InitializedSDK(this.ctx) + + for (const name in plugins) { + const plugin = new plugins[name](this.ctx) + Object.defineProperty(newSDK, plugin.name, { + value: plugin, + writable: false, + enumerable: true, + configurable: false, + }) + } + + return newSDK as InitializedSDK & RegisteredPlugins

+ } } export class OfflineInitializedSDK implements OfflineSDKInterface { @@ -229,7 +249,7 @@ export class ExtendedInitializedSDK< ...config, } as Pick - return await ExtendedInitializedSDK.create(this.ctx, mergedConfig) + return await super.extend(mergedConfig) } } diff --git a/sdk/wallet-sdk/src/wallet/init/plugin.ts b/sdk/wallet-sdk/src/wallet/init/plugin.ts new file mode 100644 index 000000000..c3d508438 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/init/plugin.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SDKLogger } from '../logger/index.js' +import { + EXTENDED_SDK_OPTION_KEYS, + ExtendedSDKOptions, + SDKContext, +} from '../sdk.js' + +export abstract class SDKPlugin { + protected readonly logger: ReturnType + + constructor( + public readonly name: string, + protected readonly ctx: SDKContext + ) { + if (EXTENDED_SDK_OPTION_KEYS.includes(name as keyof ExtendedSDKOptions)) + throw Error( + `Name ${name} is reserved and cannot be used to register the plugin. Reserved names: ${EXTENDED_SDK_OPTION_KEYS.join(', ')}.` + ) + + this.logger = ctx.logger.child({ + plugin: name, + }) + } +} diff --git a/sdk/wallet-sdk/src/wallet/init/types/sdk.ts b/sdk/wallet-sdk/src/wallet/init/types/sdk.ts index f3b24be7b..51a8d3940 100644 --- a/sdk/wallet-sdk/src/wallet/init/types/sdk.ts +++ b/sdk/wallet-sdk/src/wallet/init/types/sdk.ts @@ -9,7 +9,7 @@ import { PartyNamespace } from '../../namespace/party/index.js' import { UserNamespace } from '../../namespace/user/index.js' import { SDKUtilsNamespace } from '../../namespace/utils/index.js' import { AmuletNamespace } from '../../namespace/amulet/namespace.js' -import { AssetNamespace, TokenNamespace } from '../../sdk.js' +import { AssetNamespace, SDKContext, TokenNamespace } from '../../sdk.js' import { EventsNamespace } from '../../namespace/events/namespace.js' import { AmuletConfig, @@ -19,6 +19,7 @@ import { } from './config.js' import { Provider } from '@canton-network/core-splice-provider' import { LedgerTypes } from '@canton-network/core-ledger-client-types' +import { SDKPlugin } from '../plugin.js' // SDK OPTIONS @@ -87,6 +88,11 @@ export type BasicSDKInterface< extend: ( config: Pick ) => Promise> + registerPlugins: < + P extends Record SDKPlugin>, + >( + plugins: P + ) => BasicSDKInterface & RegisteredPlugins

}> export type ExtendedFullSDKInterface = Readonly<{ @@ -121,3 +127,14 @@ export type OfflineSDKInterface = Readonly<{ keys: KeysNamespace utils: SDKUtilsNamespace }> + +// PLUGINS + +export type RegisteredPlugins< + P extends Record SDKPlugin> = Record< + string, + new (ctx: SDKContext) => SDKPlugin + >, +> = { + [K in keyof P]: InstanceType +} diff --git a/sdk/wallet-sdk/src/wallet/sdk.ts b/sdk/wallet-sdk/src/wallet/sdk.ts index 8147ffd77..e1dd8001c 100644 --- a/sdk/wallet-sdk/src/wallet/sdk.ts +++ b/sdk/wallet-sdk/src/wallet/sdk.ts @@ -35,6 +35,7 @@ export type * from './namespace/amulet/index.js' export { type TokenProviderConfig } from '@canton-network/core-wallet-auth' export { LedgerProvider } from '@canton-network/core-provider-ledger' export { type Event } from './namespace/events/index.js' +export type * from './namespace/transactions/types.js' export { signTransactionHash, getPublicKeyFromPrivate, @@ -55,7 +56,7 @@ export type OfflineSDKContext = { error: SDKErrorHandler } -export type * from './init/index.js' +export * from './init/index.js' export { PrepareOptions, ExecuteOptions } from './namespace/ledger/index.js' export * from './namespace/transactions/prepared.js' export * from './namespace/transactions/signed.js' @@ -99,7 +100,8 @@ export class SDK { if ( //this is only the cause if authentication is completely disabled on the ledger. err?.cause && - (err.cause as string).includes( + typeof err.cause === 'string' && + err.cause.includes( 'The submitted request is missing a user-id' ) ) {