You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Choice-context routes pass interface ids but the ledger client only builds template filters #39
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
Start a Canton participant with the JSON Ledger API enabled and upload the DAR.
Seed an operator, a registry, and one instrument, then create a pending transfer instruction.
Configure the service with the interface ids the variable names ask for.
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.
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.
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
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.
Activity
lmcorbalan commentedon Aug 4, 2026
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 withscripts/seed.mjs. Correcting the record before anyone implements this.What this issue got right
The env var names really do mislead.
TRANSFER_INSTRUCTION_INTERFACE_IDandALLOCATION_INTERFACE_IDmust 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
TemplateFiltershape is correct. ATemplateFiltercarrying a concrete template id matches on a live participant and returns the concretecreateArgumentpayload, 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: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, andLOCKED_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: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
TRANSFER_INSTRUCTION_INTERFACE_IDtoTRANSFER_INSTRUCTION_TEMPLATE_IDandALLOCATION_INTERFACE_IDtoALLOCATION_TEMPLATE_ID, since they are template ids in every code path that consumes them#<package-name>:<module>:<entity>form inregistry/.env.example, replacing the current "fully qualified:<package-id>:<module>:<entity>" instruction, which produces a 400 on a live node.env.exampleCAVEAT block once the names are honestconfig.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 requestledger.tsalone apart from the shared work with Active-contract queries run at offset 0 and return an empty set on a live node #40Suggested 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.