Skip to content

Choice-context routes pass interface ids but the ledger client only builds template filters #39

Closed

Description

@lmcorbalan

Description

The transfer and allocation choice-context routes are configured with interface ids, but the ledger client only ever builds a template filter for an active-contract query. On a live participant the filter therefore matches nothing, and every choice-context route returns 404.

Compounding the confusion, the handlers read fields that exist only on the concrete template create arguments and are absent from the standard interface views. So pointing the configuration at genuine interface ids would not fix it either: the filter would match, but the payload fields the handler needs would not be there.

The environment variable names actively push an operator toward the broken configuration, because they are named for interfaces while the code requires concrete template ids.

Steps to reproduce

  1. Start a Canton participant with the JSON Ledger API enabled and upload the DAR.
  2. Seed an operator, a registry, and one instrument, then create a pending transfer instruction.
  3. Configure the service with the interface ids the variable names ask for.
  4. Request the choice context for accepting that instruction.

Expected vs actual behavior

Expected: the route resolves the instruction, discloses the escrow and the instrument config, and returns a choice context.

Actual: the active-contract query matches nothing and the route returns 404. Every choice-context route is affected.

Additional context

Part of #23. Blocks the end-to-end transfer test and the CLI conformance check.

This is invisible to the current test suite because the in-memory ledger stub returns whatever it is handed, regardless of the filter.

A review confirmed the good news that limits the blast radius: the fields the handlers read really are concrete-template create arguments, not interface-view fields. So once the routes query concrete template ids, those fields are present and no projection rewrite is needed. What must be resolved together is the filter type, which contract is queried, and the environment variable naming and documentation.

Activity

  1. added
    bugSomething isn't working
    priority: criticalBlocking work, system down, or security issue
    on Jul 22, 2026
  2. moved this from Backlog to Ready in Canton - dAppBooster (#390)on Jul 22, 2026
  3. lmcorbalan commented on Aug 4, 2026

    @lmcorbalan
    CollaboratorAuthor

    Live-node findings: the premise here is half wrong, and the defect is smaller than filed

    Exercised against Canton 3.5.6 via dpm sandbox, seeded with scripts/seed.mjs. Correcting the record before anyone implements this.

    What this issue got right

    The env var names really do mislead. TRANSFER_INSTRUCTION_INTERFACE_ID and ALLOCATION_INTERFACE_ID must hold concrete template ids, and the handlers read concrete-template create-argument fields (lockedCid, transfer) that are absent from the standard interface views. That part stands.

    What this issue got wrong

    the ledger client only ever builds a template filter ... On a live participant the filter therefore matches nothing

    The TemplateFilter shape is correct. A TemplateFilter carrying a concrete template id matches on a live participant and returns the concrete createArgument payload, which is exactly what the handlers need. No filter-type change and no projection rewrite are required.

    The real blocker is the identifier form, which nobody had reason to suspect: the participant resolves templates in an active-contract filter by package NAME, written #<package-name>:<module>:<entity>. A package-id-qualified identifier is rejected outright:

    INVALID_FIELD: Invalid field packageId: Received an identifier with package ID
    8986f3a6a6eaf57964467ff3e0a3a3aaebd6fa92b2a5473db2f1f2e7c55df135,
    but expected a package name.
    

    This is broader than the two interface-named vars. It applies to all seven template-id env vars, including INSTRUMENT_CONFIG_TEMPLATE_ID, TOKEN_REGISTRY_TEMPLATE_ID, PREAPPROVAL_TEMPLATE_ID, and LOCKED_TOKEN_TEMPLATE_ID. An operator who pastes package-id-qualified ids gets a 400 from the participant on every read, not a silent empty match.

    Working values, as printed by scripts/seed.mjs:

    INSTRUMENT_CONFIG_TEMPLATE_ID=#canton-token-forge:Canton.TokenForge.Registry:InstrumentConfig
    INSTRUMENT_CONFIG_PROPOSAL_TEMPLATE_ID=#canton-token-forge:Canton.TokenForge.Registry:InstrumentConfigProposal
    TOKEN_REGISTRY_TEMPLATE_ID=#canton-token-forge:Canton.TokenForge.Registry:TokenRegistry
    PREAPPROVAL_TEMPLATE_ID=#canton-token-forge:Canton.TokenForge.Registry:TokenTransferPreapproval
    LOCKED_TOKEN_TEMPLATE_ID=#canton-token-forge:Canton.TokenForge.Locked:LockedToken
    TRANSFER_INSTRUCTION_INTERFACE_ID=#canton-token-forge:Canton.TokenForge.Instruction:TokenTransferInstruction
    ALLOCATION_INTERFACE_ID=#canton-token-forge:Canton.TokenForge.Allocation:TokenAllocation
    

    With these, the metadata read path works end to end on a live node once the offset defect (#40) is also fixed. Verified.

    Revised scope

    • Rename TRANSFER_INSTRUCTION_INTERFACE_ID to TRANSFER_INSTRUCTION_TEMPLATE_ID and ALLOCATION_INTERFACE_ID to ALLOCATION_TEMPLATE_ID, since they are template ids in every code path that consumes them
    • Document the #<package-name>:<module>:<entity> form in registry/.env.example, replacing the current "fully qualified: <package-id>:<module>:<entity>" instruction, which produces a 400 on a live node
    • Remove the .env.example CAVEAT block once the names are honest
    • Decide whether to validate the form at boot in config.ts (reject an id that does not start with #) so a bad value fails at startup with a clear message instead of on the first request
    • Leave the filter construction in ledger.ts alone apart from the shared work with Active-contract queries run at offset 0 and return an empty set on a live node #40

    Suggested retitle

    The current title asserts the filter type is the problem. Something like "Template-id env vars must use the package-name form, and the two interface-named vars are template ids" would match what is actually broken. Not renaming it unilaterally since the issue is already linked from several places.

    The full set of conventions this participant enforces is recorded in tracked RUNBOOK.md. Related: #40 (offset), #45 (spike, now answered), and a newly filed defect covering the command-submission envelope on the write path.

  4. moved this from Ready to In progress in Canton - dAppBooster (#390)on Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: criticalBlocking work, system down, or security issue

    Type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions