Decoupling Application DAR Dependencies from Splice Amulet Upgrades

Hi everyone,

We have built a non-custodial Gift escrow application on Canton Network.

Our Gift DAR currently has data dependencies on both the stable Token Standard
interfaces and the concrete Splice Amulet package:

  • splice-api-token-metadata-v1
  • splice-api-token-holding-v1
  • splice-api-token-transfer-instruction-v1
  • splice-amulet

The Gift contract currently uses concrete Splice templates and choices such as:

  • AmuletRules_Transfer
  • LockedAmulet
  • LockedAmulet_UnlockV2
  • Amulet

At gift creation, the creator’s Amulet is converted into a LockedAmulet. The
creator remains the owner, while our operator is only a lock holder. When an
unknown user claims the gift, the Gift contract unlocks the LockedAmulet and
delivers it to the claimer through TransferFactory_Transfer.

This gives us the properties we need:

  1. The creator cannot double-spend the gifted funds.
  2. Our operator never owns or can redirect the funds.
  3. The creator does not need to be online when the gift is claimed.
  4. An initially unknown claimer can claim through a disclosed Gift contract.
  5. Unclaimed funds can only return to the creator after expiry.

The maintenance problem is that our DAR is compiled against a concrete
splice-amulet package. When Splice upgrades the Amulet package, the active
AmuletRules and newly created Amulet/LockedAmulet contracts use the new package
version. We therefore need to update the data dependency, rebuild and redeploy
our Gift DAR and update the corresponding package IDs in our backend.

We would like to reduce this coupling and keep the core Gift DAR stable across
Splice upgrades.

Questions:

  1. Is there a recommended way for an application DAR to depend only on the
    stable CIP-0056 Token Standard interfaces while still creating, locking and
    later unlocking a holding?

  2. HoldingV1/HoldingV2 exposes a generic holding view, but is there a stable
    interface for creating a time-locked holding and releasing that lock without
    importing the concrete Splice LockedAmulet and AmuletRules templates?

  3. Can a ContractId of a concrete LockedAmulet safely be stored and exercised
    through a stable interface, including its unlock operation?

  4. Would Daml package upgrades solve this dependency problem, or would the Gift
    package still need to be rebuilt because its choices refer to concrete
    Amulet types in their signatures and implementation?

  5. Is the recommended pattern to split the application into:

    • a stable Gift core package, and
    • a small version-specific Splice/Amulet adapter package
      that is replaced after each Splice upgrade?
  6. If an adapter package is recommended, how should authority be delegated so
    that the adapter can unlock and transfer the funds but cannot redirect or
    withdraw them?

  7. Can old application contracts continue interacting safely with the upgraded
    Amulet ecosystem, or should applications expect to migrate all active Gift
    contracts whenever the network changes the active Amulet package?

Our main objective is not merely dynamic package-ID discovery in the backend.
We want to remove, or at least isolate, the compile-time dependency on concrete
Splice Amulet implementations while preserving the non-custodial guarantees
above.

Is there an official or recommended architecture for this use case?

Thank you.

Great questions.

I really appreciate seeing builders openly discussing architecture and long-term maintainability. Looking forward to the recommendations from the Canton team and seeing Askardex continue to improve :rocket:

Hey, cannot vouch for an “official” architecture but have you looked at the Allocation API? This is meant for exactly this kind of use case. Canton Coin/Amulet implements the Token Standard APIs, so the application should preferably depend on Holding/Allocation rather than directly on LockedAmulet. Regarding your questions:

  1. Is there a recommended way for an application DAR to depend only on the stable CIP-0056 Token Standard interfaces while still creating, locking and later unlocking a holding?
    Yes, if the workflow fits the Token Standard Allocation API. That API is the standard abstraction for reserving holdings for later settlement, so the application can avoid depending directly on LockedAmulet.

  2. HoldingV1/HoldingV2 exposes a generic holding view, but is there a stable interface for creating a time-locked holding and releasing that lock without importing LockedAmulet and AmuletRules?
    Yes, the relevant stable abstraction is again Allocation, not Holding itself. Holding describes the asset position; Allocation handles reserving it and later executing or canceling that reservation.

  3. Can a ContractId of a concrete LockedAmulet safely be stored and exercised through a stable interface, including its unlock operation?
    It can be viewed as a Holding, but you cannot call LockedAmulet_UnlockV2 through the Holding interface because that is a concrete LockedAmulet choice. If you want to stay on stable interfaces, use the Allocation lifecycle instead.

  4. Would Daml package upgrades solve this dependency problem, or would the Gift package still need to be rebuilt because its choices refer to concrete Amulet types in their signatures and implementation?
    Package upgrades help compatible versions interoperate, but they do not remove the compile-time dependency. If Gift directly references LockedAmulet, AmuletRules, or their choices, it remains coupled to those concrete types and may need rebuilding if those APIs change incompatibly.

  5. Is the recommended pattern to split the application into a stable Gift core package and a small version-specific Splice/Amulet adapter package that is replaced after each Splice upgrade?
    Only if the Allocation API cannot express the required Gift behavior. A version-specific Amulet adapter should be a fallback, not the default.

  6. If an adapter package is recommended, how should authority be delegated so that the adapter can unlock and transfer the funds but cannot redirect or withdraw them?
    The Gift contract should define the allowed outcome: who may receive the funds, how much, and under what conditions. The adapter should only have enough authority to execute that predetermined action. It should not be given a generic choice where it can supply an arbitrary recipient or amount.

  7. Can old application contracts continue interacting safely with the upgraded Amulet ecosystem, or should applications expect to migrate all active Gift contracts whenever the network changes the active Amulet package?
    Old contracts can continue to work across compatible Daml upgrades. A new active Amulet package does not automatically imply migration of every Gift contract. Migration is mainly a concern when there is an incompatible change in the concrete API your application depends on.

Thank you. The remaining issue is that an Allocation contains a fixed transfer leg, including the receiver, when it is created. Allocation_ExecuteTransfer does not appear to allow the executor to provide or substitute the receiver.

In our Gift workflow, the receiver is intentionally unknown when the creator locks the funds and is determined only when the first eligible user claims the Gift. Creating the Allocation only at claim time would still require a stable mechanism to keep the creator’s holdings locked beforehand while the creator remains offline.

Is there a Token Standard pattern for a late-bound receiver, where an application contract constrains how the beneficiary is selected? Or does this requirement fall outside the Allocation API, requiring a registry-specific lock implementation or adapter?

CIP-0112 / Token Standard V2 addresses this use case. It allows an Allocation to be created without specifying the transfer legs upfront, so the receiver can be determined later at settlement time.

The creator would commit the funds in an Allocation and assign the Gift application/protocol as the executor.

The flow would then be:

Creator commits funds → Gift is executor → Creator goes offline → User claims Gift → Gift determines receiver → Gift settles Allocation to that receiver