Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 45 additions & 8 deletions sdk/wallet-sdk/src/wallet/__test__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,25 @@
// SPDX-License-Identifier: Apache-2.0

import { MockedObject, vi } from 'vitest'
import { SDKContext } from '../sdk.js'
import {
AmuletConfig,
AssetConfig,
BasicSDKOptions,
EventsConfig,
SDKContext,
TokenConfig,
TokenProviderConfig,
} from '../sdk.js'
import { SDKLogger } from '../logger/logger.js'
import { SDKErrorHandler } from '../error/handler.js'

const ledgerProvider = {
const exampleLink = 'http://example.com'

export const ledgerProvider = {
request: vi.fn().mockResolvedValue(undefined),
}

const mockLogger = {
export const mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
Expand All @@ -26,16 +36,43 @@ const mockErrorHandler = new SDKErrorHandler(mockLogger)
const throwSpy = vi.spyOn(mockErrorHandler, 'throw')
throwSpy.mockImplementation(vi.fn() as never)

const ctx: SDKContext = {
export const ctx: SDKContext = {
ledgerProvider,
userId: 'userId',
logger: mockLogger,
error: mockErrorHandler,
defaultSynchronizerId: '',
}

export const mock = {
ledgerProvider,
mockLogger,
ctx,
export const tokenProviderConfig: TokenProviderConfig = {
method: 'static',
token: 'token',
}

export const basicSDKOptions: BasicSDKOptions<never> = {
auth: tokenProviderConfig,
ledgerClientUrl: exampleLink,
}

export const amuletConfig: AmuletConfig = {
validatorUrl: exampleLink,
scanApiUrl: exampleLink,
auth: tokenProviderConfig,
registryUrl: exampleLink,
}

export const tokenConfig: TokenConfig = {
validatorUrl: exampleLink,
auth: tokenProviderConfig,
registries: [exampleLink, exampleLink],
}

export const assetConfig: AssetConfig = {
auth: tokenProviderConfig,
registries: [exampleLink, exampleLink],
}

export const eventsConfig: EventsConfig = {
websocketURL: exampleLink,
auth: tokenProviderConfig,
}
172 changes: 172 additions & 0 deletions sdk/wallet-sdk/src/wallet/init/__test__/init.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as mock from '../../__test__/mocks'
import {
InitializedSDK,
OfflineInitializedSDK,
ExtendedInitializedSDK,
} from '../..'
import { KeysNamespace } from '../../namespace/keys'
import { SDKUtilsNamespace } from '../../namespace/utils'
import { LedgerNamespace } from '../../namespace/ledger'
import { PartyNamespace } from '../../namespace/party'
import { UserNamespace } from '../../namespace/user'
import { AmuletNamespace } from '../../namespace/amulet'
import { AssetNamespace } from '../../namespace/asset'
import { EventsNamespace } from '../../namespace/events'
import { TokenNamespace } from '../../namespace/token'

const {
ValidatorInternalClient,
get,
TokenStandardService,
AmuletService,
ScanProxyClient,
ScanClient,
} = vi.hoisted(() => {
const get = vi.fn().mockImplementation(() =>
Promise.resolve({
party_id: 'partyId',
})
)

const registriesToAssets = vi.fn().mockResolvedValue([])

return {
ValidatorInternalClient: vi.fn(
class {
get = get
}
),
get,
TokenStandardService: vi.fn(
class {
registriesToAssets = registriesToAssets
}
),
AmuletService: vi.fn(class {}),
ScanProxyClient: vi.fn(class {}),
ScanClient: vi.fn(class {}),
}
})

vi.mock('@canton-network/core-splice-client', async (importOriginal) => {
const actual =
await importOriginal<
typeof import('@canton-network/core-splice-client')
>()
return {
...actual,
ValidatorInternalClient,
ScanProxyClient,
ScanClient,
}
})

vi.mock('@canton-network/core-token-standard-service', () => ({
TokenStandardService,
}))

vi.mock('@canton-network/core-amulet-service', () => ({
AmuletService,
}))

describe('init SDK', () => {
describe('offline', () => {
let sdk: OfflineInitializedSDK
beforeEach(() => {
vi.clearAllMocks()

sdk = new OfflineInitializedSDK(mock.ctx)
})

it('should expose offline interface', () => {
expect(sdk.keys).toBeInstanceOf(KeysNamespace)
expect(sdk.utils).toBeInstanceOf(SDKUtilsNamespace)
})
})

describe('basic', () => {
let sdk: InitializedSDK
beforeEach(() => {
vi.clearAllMocks()

sdk = new InitializedSDK(mock.ctx)
})

it('should expose basic interface', () => {
sdk = new InitializedSDK(mock.ctx)

// OfflineSDKInterface
expect(sdk.keys).toBeInstanceOf(KeysNamespace)
expect(sdk.utils).toBeInstanceOf(SDKUtilsNamespace)

// BasicSDKInterface
expect(sdk.ledger).toBeInstanceOf(LedgerNamespace)
expect(sdk.party).toBeInstanceOf(PartyNamespace)
expect(sdk.user).toBeInstanceOf(UserNamespace)
expect(sdk.registerPlugins).toBeDefined()
})
})

describe('extended', () => {
beforeEach(async () => {
vi.clearAllMocks()
})

it('should expose extended interface', async () => {
const sdk = await ExtendedInitializedSDK.create(mock.ctx, {
amulet: mock.amuletConfig,
asset: mock.assetConfig,
events: mock.eventsConfig,
token: mock.tokenConfig,
})

// OfflineSDKInterface
expect(sdk.keys).toBeInstanceOf(KeysNamespace)
expect(sdk.utils).toBeInstanceOf(SDKUtilsNamespace)

// BasicSDKInterface
expect(sdk.ledger).toBeInstanceOf(LedgerNamespace)
expect(sdk.party).toBeInstanceOf(PartyNamespace)
expect(sdk.user).toBeInstanceOf(UserNamespace)
expect(sdk.registerPlugins).toBeDefined()

// ExtendedInitializedSDK
expect(sdk.amulet).toBeInstanceOf(AmuletNamespace)
expect(sdk.asset).toBeInstanceOf(AssetNamespace)
expect(sdk.events).toBeInstanceOf(EventsNamespace)
expect(sdk.token).toBeInstanceOf(TokenNamespace)
})

it('should create amulet namespace based on services', async () => {
await ExtendedInitializedSDK.create(mock.ctx, {
amulet: mock.amuletConfig,
})

expect(ScanClient).toHaveBeenCalledOnce()
expect(ScanProxyClient).toHaveBeenCalledOnce()
expect(TokenStandardService).toHaveBeenCalledOnce()
expect(AmuletService).toHaveBeenCalledOnce()
})

it('should create token namespace based on services', async () => {
await ExtendedInitializedSDK.create(mock.ctx, {
token: mock.tokenConfig,
})

expect(get).toHaveBeenCalledOnce()
expect(TokenStandardService).toHaveBeenCalledOnce()
})

it('should create asset namespace based on services', async () => {
await ExtendedInitializedSDK.create(mock.ctx, {
asset: mock.assetConfig,
})

expect(TokenStandardService).toHaveBeenCalledOnce()
})
})
})
90 changes: 90 additions & 0 deletions sdk/wallet-sdk/src/wallet/init/__test__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EXTENDED_SDK_OPTION_KEYS, SDKPlugin } from '../'
import * as mock from '../../__test__/mocks'
import { SDK, SDKContext } from '../..'

const testPluginFactory = (key: string) => {
return vi.fn(
class extends SDKPlugin {
constructor(ctx: SDKContext) {
super(key, ctx)
}
}
)
}

const pluginName = 'pluginName'

class TestPlugin extends SDKPlugin {
constructor(ctx: SDKContext) {
super(pluginName, ctx)
}

public testMethod() {
return true
}
}

describe('plugin', () => {
beforeEach(() => {
vi.clearAllMocks()
})

EXTENDED_SDK_OPTION_KEYS.forEach((key) => {
it(`should throw error if ${key} is used as a name`, () => {
expect(() => new (testPluginFactory(key))(mock.ctx)).toThrow()
})
})

it('should call a plugin constructor when registering', async () => {
// Mock the authenticated user response
mock.ledgerProvider.request
.mockResolvedValueOnce({
user: { id: 'test-user-id' },
})
// Mock the connected synchronizers response
.mockResolvedValueOnce({
connectedSynchronizers: [{ id: 'sync-1' }],
})

const sdk = await SDK.create({
ledgerProvider: mock.ledgerProvider as never,
})

const PluginClass = testPluginFactory('plugin')

const SDKWithPlugin = sdk.registerPlugins({
plugin: PluginClass,
})

expect(SDKWithPlugin.plugin).toBeInstanceOf(PluginClass)
expect(PluginClass).toHaveBeenCalledOnce()
})

it('should successfully register a plugin under provided name', async () => {
// Mock the authenticated user response
mock.ledgerProvider.request
.mockResolvedValueOnce({
user: { id: 'test-user-id' },
})
// Mock the connected synchronizers response
.mockResolvedValueOnce({
connectedSynchronizers: [{ id: 'sync-1' }],
})

const sdk = await SDK.create({
ledgerProvider: mock.ledgerProvider as never,
})
const SDKWithPlugin = sdk.registerPlugins({
[pluginName]: TestPlugin,
})

const registeredPlugin = SDKWithPlugin[pluginName]

expect(registeredPlugin).toBeDefined()
expect(registeredPlugin.testMethod()).toBe(true)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to do sdk[pluginName].testMethod() (on the original sdk object) directly? would be nice to test that

})
})
1 change: 1 addition & 0 deletions sdk/wallet-sdk/src/wallet/init/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

export * from './types/context.js'
export * from './initializedSDK.js'
export * from './types/index.js'
export { SDKPlugin } from './plugin.js'
28 changes: 20 additions & 8 deletions sdk/wallet-sdk/src/wallet/init/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,38 @@
// SPDX-License-Identifier: Apache-2.0

import { SDKLogger } from '../logger/index.js'
import {
EXTENDED_SDK_OPTION_KEYS,
ExtendedSDKOptions,
SDKContext,
} from '../sdk.js'
import { EXTENDED_SDK_OPTION_KEYS, ExtendedSDKOptions } from './types/sdk.js'
import type { SDKContext } from './types/context.js'

export abstract class SDKPlugin {
/**
*
* @deprecated use this.ctx.logger instead
*/
protected readonly logger: ReturnType<SDKLogger['child']>
protected readonly ctx: SDKContext

constructor(
public readonly name: string,
protected readonly ctx: SDKContext
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(', ')}.`
`Name "${name}" is reserved and cannot be used to register the plugin. Reserved names: ${EXTENDED_SDK_OPTION_KEYS.join(', ')}.`
)

this.logger = ctx.logger.child({
const logger = _ctx.logger.child({
plugin: name,
})

/**
* @deprecated
*/
this.logger = logger

this.ctx = {
..._ctx,
logger,
}
}
}
Loading