Wallet SDK Configuration¶
If you have already played around with the wallet SDK you might have come across snippets like:
import {
localNetStaticConfig,
SDK,
signTransactionHash,
} from '@canton-network/wallet-sdk'
import { pino } from 'pino'
import { v4 } from 'uuid'
import {
TOKEN_NAMESPACE_CONFIG,
TOKEN_PROVIDER_CONFIG_DEFAULT,
AMULET_NAMESPACE_CONFIG,
} from './utils/index.js'
const logger = pino({ name: 'v1-01-ping-localnet', level: 'info' })
const sdk = await SDK.create({
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL,
token: TOKEN_NAMESPACE_CONFIG,
amulet: AMULET_NAMESPACE_CONFIG,
})
const senderKeys = sdk.keys.generate()
const sender = await sdk.party.external
.create(senderKeys.publicKey, {
partyHint: 'v1-01-alice',
})
.sign(senderKeys.privateKey)
.execute()
const senderFingerprint = await sdk.keys.fingerprint(senderKeys.publicKey)
logger.info({ sender, senderFingerprint }, 'Sender party representation:')
if (sender.publicKeyFingerprint !== senderFingerprint)
throw Error('Inconsistent fingerprints')
const receiverKeys = sdk.keys.generate()
const receiverPartyCreation = sdk.party.external.create(
receiverKeys.publicKey,
{
partyHint: 'v1-01-bob',
}
)
const unsignedReceiver = await receiverPartyCreation.topology()
// external signing simulation
const receiverPartySignature = signTransactionHash(
unsignedReceiver.multiHash,
receiverKeys.privateKey
)
const signedReceiverParty = await receiverPartyCreation.execute(
receiverPartySignature
)
logger.info({ signedReceiverParty }, 'Receiver party representation:')
const pingCommand = [
{
CreateCommand: {
templateId:
'#canton-builtin-admin-workflow-ping:Canton.Internal.Ping:Ping',
createArguments: {
id: v4(),
initiator: sender.partyId,
responder: sender.partyId,
},
},
},
]
logger.info({ pingCommand }, 'Ping command to be submitted:')
await sdk.ledger
.prepare({
partyId: sender.partyId,
commands: pingCommand,
disclosedContracts: [],
})
.sign(senderKeys.privateKey)
.execute({ partyId: sender.partyId })
logger.info('Ping command submitted with online signing')
/*
offline signing example
*/
const preparedPingCommand = sdk.ledger.prepare({
partyId: sender.partyId,
commands: pingCommand,
disclosedContracts: [],
})
const { response: preparedPingCommandResponse } =
await preparedPingCommand.toJSON()
logger.info({ preparedPingCommand }, 'Prepared ping command:')
/*
Note: The following code uses the @canton-network/core-signing-lib as the 'custodian' of the private key to sign the prepared transaction hash,
but in a real scenario, the signing could be done using any compatible signing mechanism, such as a hardware wallet or an external signing service.
*/
const signature = signTransactionHash(
preparedPingCommandResponse.preparedTransactionHash,
senderKeys.privateKey
)
const signed = sdk.ledger.fromSignature(preparedPingCommandResponse, signature)
await sdk.ledger.execute(signed, { partyId: sender.partyId })
logger.info('Ping command submitted with offline signing')
const [amuletTapCommand, amuletTapDisclosedContracts] = await sdk.amulet.tap(
sender.partyId,
'10000'
)
const result = await sdk.ledger
.prepare({
partyId: sender.partyId,
commands: amuletTapCommand,
disclosedContracts: amuletTapDisclosedContracts,
})
.sign(senderKeys.privateKey)
.execute({ partyId: sender.partyId })
const senderUtxos = await sdk.token.utxos.list({ partyId: sender.partyId })
const tapTransaction = await sdk.token.transactionsById({
updateId: result.updateId,
partyId: sender.partyId,
})
const mintEvent = tapTransaction.events.find(
(tokenStandardEvent) =>
tokenStandardEvent.label.type === 'Mint' &&
tokenStandardEvent.unlockedHoldingsChange.creates.find(
(h) => h.amount === '10000.0000000000'
)
)
if (mintEvent) {
logger.info('Found token standard event with type Mint')
} else {
throw new Error(`Couldn't find tap transaction by updateId`)
}
const senderAmuletUtxos = senderUtxos.filter((utxo) => {
return (
utxo.interfaceViewValue.amount === '10000.0000000000' &&
utxo.interfaceViewValue.instrumentId.id === 'Amulet'
)
})
if (senderAmuletUtxos.length === 0) {
throw new Error('No UTXOs found for Sender')
}
logger.info('Tap command for Amulet for Sender submitted and UTXO received')
This is the default config that can be used in combination with a non-altered Localnet running instance.
However as soon as you need to migrate your script, code and deployment to a different environment these default configurations are no longer viable to use. In those cases creating custom factories for each controller is needed. Here is a template that you can use when setting up your own custom connectivity configuration:
import { SDK, localNetStaticConfig } 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: new URL('http://localhost:2975'),
token: {
validatorUrl: new URL('http://localhost:2000/api/validator'),
registries: [
new URL('http://localhost:2000/api/validator/v0/scan-proxy'),
],
auth: global.TOKEN_PROVIDER_CONFIG_DEFAULT,
},
amulet: {
validatorUrl: localNetStaticConfig.LOCALNET_APP_VALIDATOR_URL,
scanApiUrl: localNetStaticConfig.LOCALNET_SCAN_API_URL,
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
registryUrl: localNetStaticConfig.LOCALNET_REGISTRY_API_URL,
},
asset: {
registries: [localNetStaticConfig.LOCALNET_REGISTRY_API_URL],
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
},
})
const myParty = global.EXISTING_PARTY_1
await sdk.token.utxos.list({ partyId: myParty })
await sdk.amulet.traffic.status()
// OR, you can defer loading config by calling .extend()
const basicSDK = await SDK.create({
auth: {
method: 'self_signed',
issuer: 'unsafe-auth',
credentials: {
clientId: 'ledger-api-user',
clientSecret: 'unsafe',
audience: 'https://canton.network.global',
scope: '',
},
},
ledgerClientUrl: new URL('http://localhost:2975'),
})
// Extend with token namespace
const tokenExtendedSDK = await basicSDK.extend({
token: {
validatorUrl: new URL('http://localhost:2000/api/validator'),
registries: [
new URL('http://localhost:2000/api/validator/v0/scan-proxy'),
],
auth: global.TOKEN_PROVIDER_CONFIG_DEFAULT,
},
})
// Now token namespace is available
await tokenExtendedSDK.token.utxos.list({ partyId: myParty })
// Can extend further with more namespaces
const fullyExtendedSDK = await tokenExtendedSDK.extend({
amulet: {
validatorUrl: localNetStaticConfig.LOCALNET_APP_VALIDATOR_URL,
scanApiUrl: localNetStaticConfig.LOCALNET_SCAN_API_URL,
auth: global.TOKEN_PROVIDER_CONFIG_DEFAULT,
registryUrl: localNetStaticConfig.LOCALNET_REGISTRY_API_URL,
},
})
// Now both token and amulet are available
await fullyExtendedSDK.token.utxos.list({ partyId: myParty })
await fullyExtendedSDK.amulet.traffic.status()
}
How do I validate my configurations?¶
Knowing if you are using the correct url and port can be daunting, here is a few curl and gcurl commands you can use to validate against an expected output
my-json-ledger-api can be identified with curl http://${my-json-ledger-api}/v2/version it should produce a json that looks like
{
"version": "3.4.12-SNAPSHOT",
"features": {
"experimental": {
"staticTime": {
"supported": false
},
"commandInspectionService": {
"supported": true
}
},
"userManagement": {
"supported": true,
"maxRightsPerUser": 1000,
"maxUsersPageSize": 1000
},
"partyManagement": {
"maxPartiesPageSize": 10000
},
"offsetCheckpoint": {
"maxOffsetCheckpointEmissionDelay": {
"seconds": 75,
"nanos": 0,
"unknownFields": {
"fields": {}
}
}
},
"packageFeature": {
"maxVettedPackagesPageSize": 100
}
}
}
the fields may vary based on your configuration.
my-validator-app-api can be identified with curl ${api}/version it should produce an output like
{"version":"0.4.15","commit_ts":"2025-09-05T11:38:13Z"}
my-scan-proxy-api is an api inside the validator api and can be defined as ${my-validator-app-api}/v0/scan-proxy.
my-registry-api is the registry for the token you want to use, for Canton Coin you can use my-scan-proxy-api, however for any other token standard token it is required to source the api from a reputable source.
Configuring auth¶
The wallet-sdk can either take in a Provider (which will have auth bundled into it) or a LedgerClientUrl + TokenProviderConfig. In our examples, we have provided a default TokenProviderConfig for connecting to localnet, which uses a self-signed token.
{
method: 'self_signed',
issuer: 'unsafe-auth',
credentials: {
clientId: 'ledger-api-user',
clientSecret: 'unsafe',
audience: 'https://canton.network.global',
scope: '',
},
}
The value for some of the audiences in localnet would have to be adjusted to match “https://canton.network.global”. This is specifically the LEDGER_API_AUTH_AUDIENCE & VALIDATOR_AUTH_AUDIENCE.
When upgrading your setup from a localnet setup to a production or client facing environment then it might make more sense to add proper authentication to the ledger api and other services. The community contributions include okta and keycloak OIDC. These can easily be configured for the SDK using a different TokenProviderConfig. The following programmatic methods of token fetching are supported:
static: a fixed, in-memory token. Only used for compatibility, it will totally break for expired tokens.
self_signed: only for development purposes, used for Canton setups that accept HMAC256 self signed tokens.
client_credentials: used to programmatically acquire tokens via oauth2, a.k.a “machine-to-machine” tokens
export type TokenProviderConfig =
| {
method: 'static'
token: string
}
| {
method: 'self_signed'
issuer: string
credentials: ClientCredentials
}
| {
method: 'client_credentials'
configUrl: string
credentials: ClientCredentials
}
export interface ClientCredentials {
clientId: string
clientSecret: string
scope: string | undefined
audience: string | undefined
}
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.
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()
}
Key Points¶
Plugin Constructor: Plugin classes must accept
SDKContextas a constructor parameter and pass it to thesuper()call along with the plugin name.Type Safety: The
registerPluginsmethod 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
ctxproperty.Multiple Plugins: You can register multiple plugins at once by passing them in a single object to
registerPlugins.