Wallet SDK Release Notes

Below are the release notes for the Wallet SDK versions, detailing new features, improvements, and bug fixes in each version.

0.10.0

Released on October 2nd, 2025

  • Self-issue feature app rights

you can now grant yourself feature app rights (similar to the wallet UI) for both internal and external parties

// For external parties
const [command,disclosedContracts] = sdk.tokenStandard!.selfGrantFeatureAppRights()

await sdk.userLedger?.prepareSignExecuteAndWaitFor(
    command,
    keyPair.privateKey,
    v4(),
    disclosedContracts
)

// For internal parties
await sdk.tokenStandard!.grantFeatureAppRightsForInternalParty()
  • localNet variation for AppProvider & AppUser

you can now use both the appProvider and AppUser easily for show operations between two validators

const providerSDK = new WalletSDKImpl().configure({
    logger,
    authFactory: localNetAuthDefault,
    ledgerFactory: localNetLedgerAppProvider, //new variations here
    topologyFactory: localNetTopologyAppProvider, //new variations here
    tokenStandardFactory: localNetTokenStandardAppProvider, //new variations here
    validatorFactory: localValidatorDefault,
})

const userSDK = new WalletSDKImpl().configure({
    logger,
    authFactory: localNetAuthDefault,
    ledgerFactory: localNetLedgerAppUser, //new variations here
    topologyFactory: localNetTopologyAppUser, //new variations here
    tokenStandardFactory: localNetTokenStandardAppUser, //new variations here
    validatorFactory: localValidatorDefault,
})

LocalNet..Default still exists, they as previously defaults to the appUser validator

  • topology transaction recalculate hash

you can now offline validate a topology transaction by recomputing the hash

const recomputeHash = await TopologyController.computeTopologyTxHash(
    prepared!.partyTransactions
)

if (recomputeHash !== prepared!.combinedHash) {
    throw new Error(
        'Recomputed hash does not match prepared combined hash'
    )
}
  • new awaiting variation with prepareSignExecuteAndWaitFor & executeSubmissionAndWaitFor

release 0.7.0 introduced the `waitForCompletion`, we have now backed that into the executions

// PREVIOUS CODE EXAMPLE
//it is recommended to fetch ledger offset before preparing your command
const offsetLatest = (await sdk.userLedger?.ledgerEnd())?.offset ?? 0

const transferCommandId =
    // prepareSignAndExecuteTransaction & prepareSign now returns the commandId
    await sdk.userLedger?.prepareSignAndExecuteTransaction(
        [{ ExerciseCommand: transferCommand }],
        keyPairSender.privateKey,
        v4(),
        disclosedContracts2
    )

//new command that scans the ledger to ensure the command have completed
const completion = await sdk.userLedger?.waitForCompletion(
    offsetLatest, //where to start from
    5000, //optional timeout in ms
    transferCommandId! //the command to look for
)

// NEW VARIATION
const completion =
        await sdk.userLedger?.prepareSignExecuteAndWaitFor(
            transferCommand,
            keyPairSender.privateKey,
            v4(),
            disclosedContracts,
            10000 // 10 second timeout, if no value is provided here a default of 15 seconds is used
        )

// VARIATION FOR `ExecuteSubmission`
const completion =
        await onlineSDK.userLedger?.executeSubmissionAndWaitFor(
            transferCommand,
            signedHash,
            keyPairSender.publicKey,
            v4()
        )
  • executeSubmission now returns the submissionId similarly to prepareSignAndExecuteTransaction

  • fixed thrown exception for missing seed when using TopologyController.createTransactionHash

  • prepareSubmission now has same command input signature as prepareSignAndExecuteTransaction

0.9.0

Released on September 26th, 2025

  • Supporting both canton 3.3 and 3.4 at the same timeout

since canton 3.4 will soon come to splice being able to support both versions is imperative before

  • localNetStaticConfig added

since the wallet api and registry are static for localnet, a new config has been added to make early development easier

import {
    WalletSDKImpl,
    localNetAuthDefault,
    localNetLedgerDefault,
    localNetTopologyDefault,
    localNetTokenStandardDefault,
    localNetStaticConfig,
} from '@canton-network/wallet-sdk'

const sdk = new WalletSDKImpl().configure({
    logger,
    authFactory: localNetAuthDefault,
    ledgerFactory: localNetLedgerDefault,
    topologyFactory: localNetTopologyDefault,
    tokenStandardFactory: localNetTokenStandardDefault,
})

await sdk.connectTopology(localNetStaticConfig.LOCALNET_SCAN_PROXY_API_URL)

sdk.tokenStandard?.setTransferFactoryRegistryUrl(
    localNetStaticConfig.LOCALNET_REGISTRY_API_URL
)

0.8.0

Release on September 24th, 2025

  • Important!: The flow has been simplified for prepare and execute of commands, however this means code needs to be converted

// previous prepare and submit flow
const [tapCommand, disclosedContracts] = await sdk.tokenStandard!.createTap(
    sender!.partyId,
    '2000000',
    {
        instrumentId: 'Amulet',
        instrumentAdmin: instrumentAdminPartyId,
    }
)

await sdk.userLedger?.prepareSignAndExecuteTransaction(
    [{ ExerciseCommand: tapCommand }],
    keyPairSender.privateKey,
    v4(),
    disclosedContracts
)

in the new flow it is no longer needed to perform the array wrapping [{ ExerciseCommand: tapCommand }] and you can instead pass the tapCommand directly

// new prepare and submit flow
const [tapCommand, disclosedContracts] = await sdk.tokenStandard!.createTap(
    sender!.partyId,
    '2000000',
    {
        instrumentId: 'Amulet',
        instrumentAdmin: instrumentAdminPartyId,
    }
)

await sdk.userLedger?.prepareSignAndExecuteTransaction(
    tapCommand,
    keyPairSender.privateKey,
    v4(),
    disclosedContracts
)

this goes for all transaction!

  • Support Withdrawal flow for 2-step transfer

it is now possible for sender to withdraw a 2-step transfer that have previously been send

// Alice withdraws the transfer
const [withdrawTransferCommand, disclosedContracts] =
    await sdk.tokenStandard!.exerciseTransferInstructionChoice(
        transferCid!,
        'Withdraw'
    )

note: this does not work if the receiver have already perform Accept or Reject

  • Allow validating if receiver have set up transfer pre-approval before performing a transaction

//check if bob have set up transfer pre approval before sending
const transferPreApprovalStatus =
        await sdk.tokenStandard?.getTransferPreApprovalByParty(
            receiver!.partyId,
            'Amulet'
        )
    logger.info(transferPreApprovalStatus, '[BOB] transfer preapproval status')
  • Tested and verified against Splice 0.4.17

  • Fix endless loop bug when onboarding a party

0.7.0

Release on September 18th, 2025

  • Important!: scan api is not longer used for methods like `connectTopology` use scan proxy instead

  • Added support for multi-hosting a party upon creation against multiple validators

// setup config against multiple nodes to acquire signature
const multiHostedParticipantEndpointConfig = [
    {
        adminApiUrl: '127.0.0.1:2902',
        baseUrl: new URL('http://127.0.0.1:2975'),
        accessToken: adminToken.accessToken,
    },
    {
        adminApiUrl: '127.0.0.1:3902',
        baseUrl: new URL('http://127.0.0.1:3975'),
        accessToken: adminToken.accessToken,
    },
]

const participantIdPromises = multiHostedParticipantEndpointConfig.map(
    async (endpoint) => {
        return await sdk.topology?.getParticipantId(endpoint)
    }
)
const participantIds = await Promise.all(participantIdPromises)

const participantPermissionMap = new Map<string, Enums_ParticipantPermission>()

// decide on Permission for each participant
participantIds.map((pId) =>
    participantPermissionMap.set(pId!, Enums_ParticipantPermission.CONFIRMATION)
)

// setup multi-hosting for a party against
await sdk.topology?.prepareSignAndSubmitMultiHostExternalParty(
    multiHostedParticipantEndpointConfig,
    multiHostedParty.privateKey,
    synchronizerId,
    participantPermissionMap,
    'bob'
)
  • Verify signed transaction hash

we have also extended the executeSubmission and prepareSignAndExecuteTransaction to validate the hash before transmitting to the ledger

const hash = 'my-transaction-hash'
const publicKey = 'my-public-key'
const signature = 'my-signed-hash-with-private-key'
const isValid = sdk.userLedger?.verifyTxHash(hash, publicKey, signature)
  • wait for command completion

//it is recommended to fetch ledger offset before preparing your command
const offsetLatest = (await sdk.userLedger?.ledgerEnd())?.offset ?? 0

const transferCommandId =
    // prepareSignAndExecuteTransaction & prepareSign now returns the commandId
    await sdk.userLedger?.prepareSignAndExecuteTransaction(
        [{ ExerciseCommand: transferCommand }],
        keyPairSender.privateKey,
        v4(),
        disclosedContracts2
    )

//new command that scans the ledger to ensure the command have completed
const completion = await sdk.userLedger?.waitForCompletion(
    offsetLatest, //where to start from
    5000, //optional timeout in ms
    transferCommandId! //the command to look for
)
  • Added new endpoint to quickly fetch all pending 2-step incoming transfer to easily accept or reject

const pendingInstructions = await sdk.tokenStandard?.fetchPendingTransferInstructionView()

const [acceptTransferCommand, disclosedContracts3] =
    await sdk.tokenStandard!.exerciseTransferInstructionChoice(
        transferCid,
        'Accept'
    )
  • optional expiry date for create transfer

const [transferCommand, disclosedContracts2] =
    await sdk.tokenStandard!.createTransfer(
        sender!.partyId,
        receiver!.partyId,
        '100',
        {
            instrumentId: 'Amulet',
            instrumentAdmin: instrumentAdminPartyId,
        },
        utxos?.map((t) => t.contractId),
        'memo-ref',
        new Date(Date.now()+60*1000) // custom expiry of 1 hour
        // default is 24 hours
    )
  • fetch transaction by update id

// convenient new endpoint to get transaction based on update id
// this will come out in same format as listHoldingTransactions
sdk.tokenStandard?.getTransactionById('my-update-id')
  • The access token generated by the authController is now correctly passed to the scan proxy and registry

0.6.1

Released on September 16th, 2025

Fixed a minor edge case where a future mining round would be chosen if there was a client clock skew.

0.6.0

Released on September 16th, 2025

  • ledgerFactory, TopologyFactory & ValidatorFactory changed to use URL instead of strings (where applicable)

const myLedgerFactory = (userId: string, token: string) => {
    return new LedgerController(
        userId,
        new URL('http://my-json-ledger-api'), //HERE
        token
    )
}

const myTopologyFactory = (
    userId: string,
    userAdminToken: string,
    synchronizerId: string
) => {
    return new TopologyController(
        'my-grpc-admin-api',
        new URL('http://my-json-ledger-api'), //HERE
        userId,
        userAdminToken,
        synchronizerId
    )
}

const myValidatorFactory = (userId: string, token: string) => {
    return new ValidatorController(
        userId,
        new URL('http://my-validator-app-api'), //HERE
        token
    )
}
  • connectTopology now uses scanProxy instead of scan for proper decentralized setup

  • stronger typing now required strings of specific formats for parties across all controllers

  • fixed a bug where the combinedHash returned from topologyController.prepareExternalPartyTopology was in hex encoding instead of base64

const preparedParty = await sdk.topology?.prepareExternalPartyTopology(
    keyPair.publicKey
)

logger.info('Prepared external topology')

if (preparedParty) {
    logger.info('Signing the hash')
    const signedHash = signTransactionHash(
    //previously this would have to be converted from hex to base64
        preparedParty?.combinedHash,
        keyPair.privateKey
    )

    const allocatedParty = await sdk.topology?.submitExternalPartyTopology(
        signedHash,
        preparedParty
    )
  • fixed a bug that caused the expectedDso field to be required when performing TransferPreApprovalProposal (this is only required after Splice 0.1.11)

  • simplified setParty & setSynchronizer, now it can all be done with one call on sdk.setPartyId()

//the connects are still needed and should be run before sdk.setPartyId
await sdk.connect()
await sdk.connectAdmin()
await sdk.connectTopology(LOCALNET_SCAN_API_URL)

//Previously all these was required to get everything working
sdk.userLedger!.setPartyId(partyId)
sdk.userLedger!.setSynchronizerId(synchronizerId)
sdk.tokenStandard?.setPartyId(partyId)
sdk.tokenStandard?.setSynchronizerId(synchronizerId)
sdk.validator?.setPartyId(partyId)
sdk.validator?.setSynchronizerId(synchronizerId)

//New version
await sdk.setPartyId(partyId,synchronizerId)
//synchronizerId is optional, it will automatically select the first synchronizerId,
//that the party is connected to if, none is defined

0.5.0

Released on September 11th, 2025

  • Memo field added to create transfer

const [transferCommand, disclosedContracts2] =
    await sdk.tokenStandard!.createTransfer(
        sender!.partyId,
        receiver!.partyId,
        '100',
        {
            instrumentId: 'Amulet',
            instrumentAdmin: instrumentAdminPartyId,
        },
        'my-new-favorite-memo-field'
    )
  • pre-approval creation now supported through ledgerController instead of validatorController

previously

await sdk.validator?.externalPartyPreApprovalSetup(privateKey)

now instead using ledger api:

const transferPreApprovalProposal =
    sdk.userLedger?.createTransferPreapprovalCommand(
        validatorOperatorParty, //this needs to be sourced from the validator
        receiver?.partyId,
        instrumentAdminPartyId
    )

await sdk.userLedger?.prepareSignAndExecuteTransaction(
    [transferPreApprovalProposal],
    keyPairReceiver.privateKey,
    v4()
)

0.4.0

Released on September 10th, 2025

  • Range filter for listHoldingTransactions(afterOffset?: string,beforeOffset?: string)

  • Transfer pre-approval support:

const sdk = new WalletSDKImpl().configure({
    logger,
    authFactory: localNetAuthDefault,
    ledgerFactory: localNetLedgerDefault,
    topologyFactory: localNetTopologyDefault,
    tokenStandardFactory: localNetTokenStandardDefault,
    validatorFactory: localValidatorDefault, //Extend SDK with new validator factory
})

//set the party
sdk.validator?.setPartyId(receiver?.partyId!)

//provide private key to sign the pre-approval
await sdk.validator?.externalPartyPreApprovalSetup(keyPairReceiver.privateKey)
  • Support added for 2-step transfers (propose / accept)

const [acceptTransferCommand, disclosedContracts3] =
    await sdk.tokenStandard!.exerciseTransferInstructionChoice(
        transferCid, //cid of the transfer instruction
        'Accept' // or 'Reject'
    )
  • listHoldingsUtxo has been extended to only nonLocked UTXOs

//new optional parameter, default is true (to be backwards compatible
const usableUtxos = await sdk.tokenStandard?.listHoldingUtxos(false)

//this include locked UTXOs
const allUtxos = await sdk.tokenStandard?.listHoldingUtxos()