Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions .github/actions/setup_canton/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ inputs:
canton_version:
description: 'Canton version (required)'
required: true
multi-sync:
description: 'Start localnet with --profile multi-sync'
start_services:
description: 'Whether to start canton services after setup'
required: false
Expand Down Expand Up @@ -60,6 +62,28 @@ runs:
cat "$LOGFILE"
exit 1
'
# 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
if: inputs.instance == 'localnet'
shell: bash
run: |
MULTI_SYNC_FLAG=""
if [ "${{ inputs.multi-sync }}" = "true" ]; then
MULTI_SYNC_FLAG="--multi-sync"
fi
yarn start:localnet -- --network=${{ inputs.network }} $MULTI_SYNC_FLAG

- name: Save Docker images to cache
if: ${{ inputs.instance == 'localnet' && steps.localnet-cache.outputs.cache-hit != 'true' }}
shell: bash
run: |
mkdir -p /tmp/docker-images
images=$(docker images --format "{{.Repository}}:{{.Tag}}" | grep -v "<none>" || true)
if [ -n "$images" ]; then
echo "$images" | xargs -r docker save -o /tmp/docker-images/images.tar
else
echo "No Docker images found to save."
fi

- name: Save Canton cache
uses: ./.github/actions/save_cache_if_absent
Expand Down
87 changes: 81 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,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 @@ -431,7 +431,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 @@ -453,17 +453,92 @@ 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:
Comment thread
Viktor-Kalashnykov-da marked this conversation as resolved.
name: wallet-sdk-scripts-e2e-multi-sync (${{ matrix.network }})
runs-on: ubuntu-latest
needs: [build, e2e-affected]
if: needs.e2e-affected.outputs.affected_wallet_sdk == 'true'
strategy:
fail-fast: false
matrix:
network: [devnet, mainnet]

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

- uses: ./.github/actions/setup_yarn

- uses: ./.github/actions/setup_canton
with:
network: ${{ matrix.network }}
instance: localnet
multi-sync: 'true'

- 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()
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: [
e2e-affected,
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
run: |
if [ "${{ needs.wallet-sdk-snippets-e2e.result }}" != "success" ]; then
echo "wallet-sdk snippets e2e did not succeed"
exit 1
if [ "${{ needs.e2e-affected.outputs.affected_wallet_sdk }}" = "true" ]; then
if [ "${{ needs.wallet-sdk-snippets-e2e.result }}" != "success" ]; then
echo "wallet-sdk snippets e2e was scheduled but did not succeed"
exit 1
fi
if [ "${{ needs.wallet-sdk-scripts-e2e.result }}" != "success" ]; then
echo "wallet-sdk scripts e2e was scheduled but did not succeed"
exit 1
fi
if [ "${{ needs.wallet-sdk-scripts-e2e-multi-sync.result }}" != "success" ]; then
echo "wallet-sdk scripts e2e (multi-sync) was scheduled but did not succeed"
exit 1
fi
echo "all wallet-sdk-e2e jobs passed"
else
echo "wallet-sdk e2e skipped (no affected wallet-sdk dependencies)"
fi
if [ "${{ needs.wallet-sdk-scripts-e2e.result }}" != "success" ]; then
echo "wallet-sdk scripts e2e did not succeed"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/examples-under-stress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
run: yarn script:test:examples-stress

- 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 (${{ github.event.inputs.network || 'devnet' }})
if: always()
run: yarn stop:localnet -- --network=${{ github.event.inputs.network || 'devnet' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stress-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
run: yarn script:test:stress-scripts

- 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 (${{ github.event.inputs.network || 'devnet' }})
if: always()
run: yarn stop:localnet -- --network=${{ github.event.inputs.network || 'devnet' }}
Expand Down
76 changes: 38 additions & 38 deletions canton/multi-sync/app-synchronizer.sc
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,60 @@ bootstrap.synchronizer(
staticSynchronizerParameters = StaticSynchronizerParameters.defaultsWithoutKMS(ProtocolVersion.latest),
)

// Connect app-provider to the new synchronizer.
// TODO: app-user is intentionally NOT connected to app-synchronizer so that
// the SDK (which picks connectedSynchronizers[0]) always selects the global synchronizer.
// This is a temporary workaround until we have a better way to select synchronizers in the SDK.
// Connect app-user and app-provider 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)
//
// 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")

// Wait for app-provider to be active on app-synchronizer
// Wait for both 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")
}

// Replicate package vetting from the global synchronizer to app-synchronizer so that
// the new synchronizer is fully functional for app-provider.
//
// Splice connects app-provider to the global synchronizer under the alias "global".
// We read vetting from its per-synchronizer store rather than the authorized store
// because we want to replicate exactly what is active on the global synchronizer.
// We wait until the global-synchronizer view is non-empty to avoid a topology-
// propagation race (which caused `multi-sync-startup` to fail in CI).
val connectedSynchronizers = `app-provider`.synchronizers.list_connected()
val appSyncId = connectedSynchronizers
// Vet packages on app-synchronizer for all three participants.
// The Splice app already uploaded DARs and vetted them on global-domain.
// We replicate the vetting from the authorized store to app-synchronizer
// so that the synchronizer is fully functional.
val appSyncId = `app-provider`.synchronizers.list_connected()
.find(_.synchronizerAlias.unwrap == "app-synchronizer")
.getOrElse(throw new RuntimeException("app-synchronizer not found in connected synchronizers"))
.synchronizerId
val globalSyncId = connectedSynchronizers
.find(_.synchronizerAlias.unwrap == "global")
.getOrElse(throw new RuntimeException(
s"'global' synchronizer not found. Connected: ${connectedSynchronizers.map(_.synchronizerAlias.unwrap).mkString(", ")}"
))
.synchronizerId

utils.retry_until_true {
`app-provider`.topology.vetted_packages
.list(store = Some(TopologyStoreId.Synchronizer(globalSyncId)), filterParticipant = `app-provider`.id.filterString)
for (participant <- Seq(`app-provider`, `app-user`)) {
val vettedFromAuthorized = participant.topology.vetted_packages
.list(store = Some(TopologyStoreId.Authorized), filterParticipant = participant.id.filterString)
.flatMap(_.item.packages)
.nonEmpty
}

val vettedPackages = `app-provider`.topology.vetted_packages
.list(store = Some(TopologyStoreId.Synchronizer(globalSyncId)), filterParticipant = `app-provider`.id.filterString)
.flatMap(_.item.packages)

logger.info(s"Vetting ${vettedPackages.size} packages on app-synchronizer for app-provider")
`app-provider`.topology.vetted_packages.propose_delta(
participant = `app-provider`.id,
store = appSyncId,
adds = vettedPackages.toSeq,
)
if (vettedFromAuthorized.nonEmpty) {
logger.info(s"Vetting ${vettedFromAuthorized.size} packages on app-synchronizer for ${participant.name}")
participant.topology.vetted_packages.propose_delta(
participant = participant.id,
store = appSyncId,
adds = vettedFromAuthorized.toSeq,
)
}
}

// Wait for vetting to propagate on app-synchronizer
// Wait for vetting topology to propagate for app-provider and app-user
utils.retry_until_true {
val providerVetted = `app-provider`.topology.vetted_packages
.list(store = Some(appSyncId), filterParticipant = `app-provider`.id.filterString)
providerVetted.nonEmpty && providerVetted.head.item.packages.nonEmpty
}
utils.retry_until_true {
val userVetted = `app-user`.topology.vetted_packages
.list(store = Some(appSyncId), filterParticipant = `app-user`.id.filterString)
userVetted.nonEmpty && userVetted.head.item.packages.nonEmpty
}

logger.info("app-synchronizer bootstrap with package vetting completed successfully")
logger.info("app-synchronizer bootstrap with package vetting completed successfully for app-provider and app-user")
1 change: 1 addition & 0 deletions docs/wallet-integration-guide/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"run-12": "tsx ./scripts/12-subscribe-to-events.ts | pino-pretty",
"run-13": "tsx ./scripts/13-rewards-for-deposits/index.ts | pino-pretty",
"run-14": "tsx ./scripts/14-offline-signing.ts | pino-pretty",
"run-15": "tsx ./scripts/15-multi-sync/index.ts | pino-pretty",
"stress-run-01": "tsx ./scripts/stress/01-merge-utxos.ts | pino-pretty",
"stress-run-02": "tsx ./scripts/stress/02-merge-utxos-delegate.ts | pino-pretty"
},
Expand Down
Loading