Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
81f734a
feat: multi-sync example (example 15)
Viktor-Kalashnykov-da May 11, 2026
dae0c05
Code Review: removed dependency on @canton-network/core-splice-client…
Viktor-Kalashnykov-da May 11, 2026
4862bad
Code Review: refactored names of parties in logs
Viktor-Kalashnykov-da May 12, 2026
b143830
Code Review: added TokenAdmin party and replaced reassignment of Toke…
Viktor-Kalashnykov-da May 12, 2026
9028bfa
Merge branch 'main' into wiktor/multisync-example
Viktor-Kalashnykov-da May 12, 2026
3967eb4
Merge branch 'main' into wiktor/multisync-example
Viktor-Kalashnykov-da May 13, 2026
320cad2
Code Review: removed usage of hardcoded values for adding for request…
Viktor-Kalashnykov-da May 13, 2026
5c92e06
Code Review: replaced the reassignment of Bob's Token to self-transfe…
Viktor-Kalashnykov-da May 13, 2026
53d1592
Code Review: removed Token interface disovery description from docume…
Viktor-Kalashnykov-da May 13, 2026
13e99aa
Code Review: removed reference to token dars Troubleshooting section
Viktor-Kalashnykov-da May 13, 2026
c76aa8b
Code Review: moved globalSynchronizerId() method from common.ts to n…
Viktor-Kalashnykov-da May 13, 2026
366e8f3
Multi- Sync: added Asset Registry usage in the example
Viktor-Kalashnykov-da May 15, 2026
6b74322
Improvement: moved completely interaction with Token and Token Rules …
Viktor-Kalashnykov-da May 15, 2026
ca6ff9a
Fix: fixed build.yml for CI
Viktor-Kalashnykov-da May 18, 2026
00bb733
Fix: fixed multi sync tests on CI
Viktor-Kalashnykov-da May 18, 2026
45a64d1
Fix: removed vetting of Participant 3 packages on Global Synchronizer
Viktor-Kalashnykov-da May 18, 2026
30e0556
Improvement: connected SV to app synchronizer as well
Viktor-Kalashnykov-da May 18, 2026
ab00c56
Fix: added waiting for Multi-Sync startup to finish for starting mult…
Viktor-Kalashnykov-da May 18, 2026
42bcbc5
Improvement: additional improvements for multi-sync cluster start
Viktor-Kalashnykov-da May 18, 2026
5e4a94a
Fix: Multi-Sync Cluster startup fix
Viktor-Kalashnykov-da May 18, 2026
c9240b2
Improvement: added active polling with max attempts for retrieval of …
Viktor-Kalashnykov-da May 18, 2026
e9020ee
More improvements for testing of Multi Sync example in CI
Viktor-Kalashnykov-da May 18, 2026
980bd27
Fix: fixed configuration for participants connection to global domain…
Viktor-Kalashnykov-da May 18, 2026
c0e9f80
Fix: fixed check for global synchonizer to become active inside parti…
Viktor-Kalashnykov-da May 18, 2026
b1264bb
Fix: fix for flaky test for Multi Sync suite
Viktor-Kalashnykov-da May 19, 2026
8e3b064
Removed retry logic for Multi-Sync tests
Viktor-Kalashnykov-da May 19, 2026
341c455
Fixed build-docs CI step
Viktor-Kalashnykov-da May 19, 2026
161b7be
Refactoring
Viktor-Kalashnykov-da May 19, 2026
7deb339
docs(example-15): simplify README, run all commands from repo root
jarekr-da May 20, 2026
074ac96
Code Review: revert usage of Registry for TokenRules and Token Contra…
Viktor-Kalashnykov-da May 20, 2026
0ba4a98
chore: reworking scenario - interna.reassign used
jarekr-da May 20, 2026
9571324
feat: working version - explicit reassign
jarekr-da May 20, 2026
a194868
chore(example-15): revert self-transfer token variant and remove gene…
jarekr-da May 20, 2026
14d8745
chore: fixed dar use
jarekr-da May 20, 2026
6b7b82e
Merge branch 'wiktor/multisync-example' into wiktor/multisync-example…
Viktor-Kalashnykov-da May 20, 2026
57bc5af
Fix: fixed compilation errors
Viktor-Kalashnykov-da May 20, 2026
b2ecbf5
Fix: post merge conflicts issue resolution
Viktor-Kalashnykov-da May 20, 2026
c4d1780
Post Merge Conflict resolution fixes
Viktor-Kalashnykov-da May 21, 2026
6ef5c82
Fixed actions for Multi-Sync tests on CI
Viktor-Kalashnykov-da May 21, 2026
49cd2b3
Fix: moved splice-test-token-v1-1.0.0.dar with DAML file (for documen…
Viktor-Kalashnykov-da May 21, 2026
7416bb6
Merge branch 'wiktor/multisync-example' into wiktor/multisync-example…
Viktor-Kalashnykov-da May 21, 2026
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
38 changes: 38 additions & 0 deletions .github/actions/setup_localnet/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: 'Setup Localnet'
description: 'Restore cached Localnet environment and optionally start services'
inputs:
network:
description: 'Network type: devnet or mainnet'
required: true
splice_version:
description: 'Splice version (required)'
required: true
start_services:
description: 'Whether to start Localnet services after setup'
required: false
default: 'true'
runs:
using: 'composite'
steps:
- name: Cache Localnet files
uses: actions/cache/restore@v5
with:
path: |
.localnet/docker-compose/localnet
.localnet/dars
/tmp/docker-images
key: ${{ runner.os }}-localnet-${{ inputs.splice_version }}
fail-on-cache-miss: true

- name: Load cached Docker images
shell: bash
run: |
if [ -f /tmp/docker-images/images.tar ]; then
docker load -i /tmp/docker-images/images.tar
echo "Docker images loaded from cache."
fi

- name: Start Localnet
if: ${{ inputs.start_services == 'true' }}
shell: bash
run: yarn start:localnet -- --network=${{ inputs.network }}
55 changes: 55 additions & 0 deletions .github/actions/setup_yarn/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: 'Setup Yarn'
description: 'Setup Yarn environment (full initial setup without pre-built artifacts)'
inputs:
daml_release_version:
description: 'Resolved DAML release version from workflow metadata job'
required: true
save_cache:
description: 'Whether to save the DPM cache on a cache miss'
required: false
default: 'false'
runs:
using: composite
steps:
- name: Enable Corepack
shell: bash
run: corepack enable

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 'v24.9.0'
cache: 'yarn'

- name: Restore DPM cache
id: dpm_cache
uses: actions/cache/restore@v5
with:
path: |
~/.dpm
key: dpm-${{ runner.os }}-${{ inputs.daml_release_version }}

- name: Add DPM to PATH
shell: bash
run: echo "$HOME/.dpm/bin" >> "$GITHUB_PATH"

- name: reduce DPM size
if: ${{ steps.dpm_cache.outputs.cache-hit != 'true' }}
shell: bash
#the OCI is only used for multiple version support and can safely be deleted to save ~ 1.3 gb.
run: rm -rf ~/.dpm/cache/oci-layout

- name: Install dependencies
shell: bash
run: yarn install --immutable

- name: generate all
shell: bash
run: yarn generate:all

- name: Save DPM cache
uses: ./.github/actions/save_cache_if_absent
with:
path: |
~/.dpm
key: ${{ steps.dpm_cache.outputs.cache-primary-key }}
81 changes: 77 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,14 +139,15 @@ jobs:
uses: snok/install-poetry@v1
with:
version: 2.1.3
virtualenvs-in-project: true

# load cached venv if cache exists
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache/restore@v5
with:
path: docs/wallet-integration-guide/.venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('docs/wallet-integration-guide/poetry.lock') }}
key: venv-v1-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('docs/wallet-integration-guide/poetry.lock') }}

# install dependencies if cache does not exist
- name: Install dependencies
Expand All @@ -159,6 +160,7 @@ jobs:
with:
path: docs/wallet-integration-guide/.venv
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('docs/wallet-integration-guide/poetry.lock') }}
initial_cache_hit: ${{ steps.cached-poetry-dependencies.outputs.cache-hit }}

- name: Build docs
working-directory: docs/wallet-integration-guide
Expand Down Expand Up @@ -459,7 +461,7 @@ jobs:
run: yarn nx snippets docs-wallet-integration-guide-examples

- uses: ./.github/actions/check_resources

# TODO (#1721): make multi-sync the default and remove the flag once multi-sync is fully supported and tested in the main scripts e2e tests, but for now we want to keep it as an option to avoid accidentally running multi-sync e2e tests without updating the main scripts e2e tests to cover multi-sync as well
- name: Stop Localnet (${{ matrix.network }})
if: always()
run: yarn stop:localnet -- --network=${{ matrix.network }}
Expand Down Expand Up @@ -511,7 +513,7 @@ jobs:
run: yarn script:test:examples

- uses: ./.github/actions/check_resources

# TODO (#1721): make multi-sync the default and remove the flag once multi-sync is fully supported and tested in the main scripts e2e tests, but for now we want to keep it as an option to avoid accidentally running multi-sync e2e tests without updating the main scripts e2e tests to cover multi-sync as well
- name: Stop Localnet (${{ matrix.network }})
if: always()
run: yarn stop:localnet -- --network=${{ matrix.network }}
Expand All @@ -533,10 +535,77 @@ jobs:
name: docker-logs-scripts-${{ matrix.network }}
path: logs/

# TODO (#1721): remove multi-sync scripts e2e tests once multi-sync is fully supported and tested in the main scripts e2e tests
wallet-sdk-scripts-e2e-multi-sync:
name: wallet-sdk-scripts-e2e-multi-sync (${{ matrix.network }})
runs-on: ubuntu-latest
needs: [version-config, hydrate-canton-caches]
strategy:
fail-fast: false
matrix:
network: [devnet, mainnet]

steps:
- name: Checkout
uses: actions/checkout@v6

- uses: ./.github/actions/setup_yarn
with:
daml_release_version: ${{ needs.version-config.outputs.daml_release_version }}

- uses: ./.github/actions/setup_localnet
with:
network: ${{ matrix.network }}
splice_version: ${{ matrix.network == 'devnet' && needs.version-config.outputs.devnet_splice_version || needs.version-config.outputs.mainnet_splice_version }}
start_services: 'false'

# TODO (#1721): make multi-sync the default and remove the flag once multi-sync is fully supported and tested in the main scripts e2e tests, but for now we want to keep it as an option to avoid accidentally running multi-sync e2e tests without updating the main scripts e2e tests to cover multi-sync as well
- name: Start Localnet with multi-sync (${{ matrix.network }})
run: yarn start:localnet -- --network=${{ matrix.network }} --multi-sync

- uses: ./.github/actions/check_resources

- name: Build project
run: yarn build:all

- name: Test multi-sync example script (${{ matrix.network }})
env:
MAX_IO_LISTENERS: '50'
run: yarn script:test:examples:multi-sync

- uses: ./.github/actions/check_resources
# TODO (#1721): make multi-sync the default and remove the flag once multi-sync is fully supported and tested in the main scripts e2e tests, but for now we want to keep it as an option to avoid accidentally running multi-sync e2e tests without updating the main scripts e2e tests to cover multi-sync as well
- name: Stop Localnet (${{ matrix.network }})
if: always()
continue-on-error: true
run: yarn stop:localnet -- --network=${{ matrix.network }} --multi-sync

- name: Save container logs
if: failure()
run: |
#!/usr/bin/env bash
set -euo pipefail
mkdir -p logs
for c in $(docker ps -a --format '{{.Names}}'); do
docker logs "$c" &> "logs/$c.log" || true
done

- name: Upload logs as artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: docker-logs-scripts-multi-sync-${{ matrix.network }}
path: logs/

test-wallet-sdk-e2e:
name: test-wallet-sdk-e2e
runs-on: ubuntu-latest
needs: [wallet-sdk-snippets-e2e, wallet-sdk-scripts-e2e, wallet-sdk-pkg]
needs: [
wallet-sdk-snippets-e2e,
wallet-sdk-scripts-e2e,
wallet-sdk-scripts-e2e-multi-sync, # TODO (#1721): remove multi-sync scripts e2e tests once multi-sync is fully supported and tested in the main scripts e2e tests, but for now we want to keep it as a gate to ensure multi-sync e2e tests are not accidentally skipped without updating the main scripts e2e tests to cover multi-sync as well
wallet-sdk-pkg,
]
if: always()
steps:
- name: Report wallet-sdk e2e execution
Expand All @@ -549,6 +618,10 @@ jobs:
echo "wallet-sdk scripts e2e did not succeed"
exit 1
fi
if [ "${{ needs.wallet-sdk-scripts-e2e-multi-sync.result }}" != "success" ]; then
echo "wallet-sdk scripts e2e (multi-sync) did not succeed"
exit 1
fi
if [ "${{ needs.wallet-sdk-pkg.result }}" != "success" ]; then
echo "wallet-sdk package validation did not succeed"
exit 1
Expand Down
37 changes: 30 additions & 7 deletions canton/multi-sync/app-synchronizer.sc
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,29 @@ bootstrap.synchronizer(
staticSynchronizerParameters = StaticSynchronizerParameters.defaultsWithoutKMS(ProtocolVersion.latest),
)

// Connect app-user and app-provider to the new synchronizer.
// Connect all participants to the new synchronizer.
// app-user — global + app-synchronizer
// app-provider — global + app-synchronizer
// sv — global only (TradingApp is only an observer of Token Allocations;
// it learns about them when they are reassigned to global before settlement)
// sv — global + app-synchronizer (TokenAdmin on sv submits TokenRules
// and Token contracts on the app-synchronizer)
//
// The global domain is connected first (before this bootstrap script runs),
// so connectedSynchronizers[0] remains global for all participants — the
// default synchronizer selection is unaffected.
`app-provider`.synchronizers.connect_local(`app-sequencer`, "app-synchronizer")
`app-user`.synchronizers.connect_local(`app-sequencer`, "app-synchronizer")
`sv`.synchronizers.connect_local(`app-sequencer`, "app-synchronizer")

// Wait for both participants to be active on app-synchronizer
// Wait for all participants to be active on app-synchronizer
utils.retry_until_true {
`app-provider`.synchronizers.active("app-synchronizer")
}
utils.retry_until_true {
`app-user`.synchronizers.active("app-synchronizer")
}
utils.retry_until_true {
`sv`.synchronizers.active("app-synchronizer")
}

// Vet packages on app-synchronizer for all three participants.
// The Splice app already uploaded DARs and vetted them on global-domain.
Expand All @@ -39,7 +43,7 @@ val appSyncId = `app-provider`.synchronizers.list_connected()
.getOrElse(throw new RuntimeException("app-synchronizer not found in connected synchronizers"))
.synchronizerId

for (participant <- Seq(`app-provider`, `app-user`)) {
for (participant <- Seq(`app-provider`, `app-user`, `sv`)) {
val vettedFromAuthorized = participant.topology.vetted_packages
.list(store = Some(TopologyStoreId.Authorized), filterParticipant = participant.id.filterString)
.flatMap(_.item.packages)
Expand All @@ -54,7 +58,7 @@ for (participant <- Seq(`app-provider`, `app-user`)) {
}
}

// Wait for vetting topology to propagate for app-provider and app-user
// Wait for vetting topology to propagate for all participants
utils.retry_until_true {
val providerVetted = `app-provider`.topology.vetted_packages
.list(store = Some(appSyncId), filterParticipant = `app-provider`.id.filterString)
Expand All @@ -65,5 +69,24 @@ utils.retry_until_true {
.list(store = Some(appSyncId), filterParticipant = `app-user`.id.filterString)
userVetted.nonEmpty && userVetted.head.item.packages.nonEmpty
}
utils.retry_until_true {
val svVetted = `sv`.topology.vetted_packages
.list(store = Some(appSyncId), filterParticipant = `sv`.id.filterString)
svVetted.nonEmpty && svVetted.head.item.packages.nonEmpty
}

logger.info("app-synchronizer bootstrap with package vetting completed successfully for app-provider and app-user")
logger.info("app-synchronizer bootstrap with package vetting completed successfully for app-provider, app-user, and sv")

// Final gate: confirm all participants are active on the global synchronizer
// (Canton alias "global", as configured in conf/splice/app.conf domains.global.alias).
// On slower CI environments (e.g. devnet) sv's global synchronizer ledger API connection
// can still be initialising when the app-synchronizer steps above finish.
// docker wait multi-sync-startup will not return until this check passes,
// preventing the "Unknown or not connected synchronizer global-domain::..." error
// that occurs when party allocation is attempted before sv is ready.
utils.retry_until_true {
`app-provider`.synchronizers.active("global") &&
`app-user`.synchronizers.active("global") &&
`sv`.synchronizers.active("global")
}
logger.info("All participants confirmed active on global synchronizer — localnet ready")
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* Port layout (PARTICIPANT_JSON_API_PORT_SUFFIX = 975):
* 2975 — app-user (P1): global + app-synchronizer
* 3975 — app-provider (P2): global + app-synchronizer
* 4975 — sv (P3): global only
* 4975 — sv (P3): global + app-synchronizer
*
*/

Expand All @@ -17,7 +17,15 @@ export const LOCALNET_BOB_LEDGER_URL = new URL('http://localhost:3975')
// trading-app-participant JSON API (4 + PARTICIPANT_JSON_API_PORT_SUFFIX 975)
export const LOCALNET_TRADING_APP_LEDGER_URL = new URL('http://localhost:4975')

// Party hint labels used when allocating parties
// TestToken Token Standard registry (hosted in-process by example 15)
export const LOCALNET_TEST_TOKEN_REGISTRY_PORT = parseInt(
process.env['REGISTRY_PORT'] ?? '5975',
10
)
export const LOCALNET_TEST_TOKEN_REGISTRY_URL = new URL(
`http://localhost:${LOCALNET_TEST_TOKEN_REGISTRY_PORT}`
)

export const PARTY_HINT_ALICE = 'Alice'
export const PARTY_HINT_BOB = 'Bob'
export const PARTY_HINT_TRADING_APP = 'TradingApp'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

/**
* TestToken implementation of AllocationInstructionHandlers.
*
* Resolves the AllocationFactory by looking up the live TokenRules contract on the
* *global* synchronizer from the ledger ACS. For trade settlement the token must
* be allocated on the global (trade) synchronizer, so we always return the
* TokenRules contract that lives there. The TokenRules contract is also included
* as a disclosed contract so the wallet SDK can pass it through to the Ledger API
* when exercising AllocationFactory_Allocate via the interface.
*/

import type {
FactoryWithChoiceContext,
AllocationInstructionHandlers,
GetFactoryRequest,
} from '../../types.js'
import type { TokenRulesContract } from '../../ledger.js'

export interface AllocationInstructionHandlerContext {
getTokenRules: (
synchronizerId?: string
) => Promise<TokenRulesContract | null>

globalSynchronizerId: string
}

export function createAllocationInstructionHandlers(
ctx: AllocationInstructionHandlerContext
): AllocationInstructionHandlers {
return {
getAllocationFactory: async (
_req: GetFactoryRequest
): Promise<FactoryWithChoiceContext | null> => {
const tokenRules = await ctx.getTokenRules(ctx.globalSynchronizerId)
if (!tokenRules) return null
return {
factoryId: tokenRules.contractId,
choiceContext: {
choiceContextData: {},
disclosedContracts: [
{
templateId: tokenRules.templateId,
contractId: tokenRules.contractId,
createdEventBlob: tokenRules.createdEventBlob,
synchronizerId: tokenRules.synchronizerId,
},
],
},
}
},
}
}
Loading
Loading