Canton Network docsSDKs › Wallet SDK › Using the SDK › v0 to v1 migration
Ledger Namespace
Updated 12-11-2026
The ledger namespace is used for preparing, signing, and executing transactions and other Ledger API operations.
Each step in lifecycle is clearer, workflowIds are generated automatically and there is better typesafety at each step.
The below example demonstrates how offline signing works.
Active Contract Set (ACS) queries
No need to manually enter get the ledger end for the offset and there is direct extraction of the activeContracts. However, we still have an
Availability
The ledger namespace is always available as part of the basic SDK interface. It’s initialized automatically when you create an SDK instance and doesn’t require additional configuration viaextend().
import { localNetStaticConfig, SDK } from '@canton-network/wallet-sdk'
export default async function () {
const sdk = await SDK.create({
auth: global.TOKEN_PROVIDER_CONFIG_DEFAULT,
ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL,
synchronizerId: global.LOCALNET_GLOBAL_SYNCHRONIZER,
amulet: global.AMULET_NAMESPACE_CONFIG,
})
const partyId = EXISTING_PARTY_1
const privateKey = EXISTING_PARTY_1_KEYS.privateKey
const [commands, disclosedContracts] = await sdk.amulet.tap(partyId, '200')
// ledger namespace is immediately available
await sdk.ledger
.prepare({ partyId, commands, disclosedContracts })
.sign(privateKey)
.execute({ partyId })
}
Key changes from v0 to v1
v0 used theuserLedger or adminLedger controller with implicit party context set via sdk.setPartyId().
v1 uses the ledger namespace where you:
- Pass
partyIdexplicitly to each operation - Have an explicit lifecycle with
prepare/sign/executechain instead of a single method - Access operations through logical groupings (
external,internal,dar, andacs)
await sdk.userLedger.prepareSignExecuteAndWaitFor(
commands,
privateKey,
workFlowId,
disclosedContracts
)
await sdk.ledger
.prepare({
partyId: partyId,
commands: [...],
})
.sign(privateKey)
.execute({
partyId: partyId,
})
Full example: online and offline signing
import {
localNetStaticConfig,
SDK,
signTransactionHash,
} from '@canton-network/wallet-sdk'
import {
TOKEN_NAMESPACE_CONFIG,
TOKEN_PROVIDER_CONFIG_DEFAULT,
AMULET_NAMESPACE_CONFIG,
localNetGlobalSynchronizer,
} from './utils/index.js'
import { configureLogging } from './utils/configure-logging.js'
const logger = await configureLogging()
const sdk = await SDK.create({
auth: TOKEN_PROVIDER_CONFIG_DEFAULT,
ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL,
synchronizerId: localNetGlobalSynchronizer,
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: crypto.randomUUID(),
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')
// Forcefully exit to prevent floating ledger retries from crashing the event loop
process.exit(0)
See all 168 lines
const contracts = await sdk.userLedger.activeContracts({
offset,
templateIds: [...],
parties: [],
filterByParty: true
})
const contractId = LedgerController.getActiveContractCid(contracts?.[0]?.contractEntry!)
const contracts = await sdk.ledger.acs.read({
parties: [...],
templateIds: [...],
filterByParty: true
})
const contractId = contracts[0].contractId
acs.readRaw method for unfiltered results.
DAR management
const isPackageUploaded = await sdk.userLedger.isPackageUploaded(PACKAGED_ID)
if(!isPackageUploaded){
await sdk.adminLedger.uploadDar(darBytes)
}
//automatically checks if uploaded and skips if present
await sdk.ledger.dar.upload(darBytes, packageId)
Migration reference
| v0 method | v1 method |
|---|---|
sdk.userLedger.prepareSignExecuteAndWaitFor | sdk.ledger.prepare({partyId, commands, disclosedContracts}).sign(privateKey).execute(partyId) |
sdk.userLedger.activeContracts | sdk.ledger.acs.read |
sdk.adminLedger.uploadDar | sdk.ledger.dar.upload |
sdk.userLedger.isPackageUploaded | sdk.ledger.dar.check |
See also
- v0 to v1 migration overview
- Migration cheat sheet
- Configuration — SDK configuration
- Preparing and signing a transaction — Transaction lifecycle