Porting a Daml 2.9.4 app to a Canton 3.x shared node: LF 2.2, Ledger API v2, and two auth gotchas
Sharing the sequence of problems I hit porting a small Daml application from a local 2.9.4 sandbox onto a shared Canton 3.x participant node, in case it saves someone the day it cost me. None of it is novel on its own, but I couldn’t find the whole chain in one place, and most of the error messages point somewhere unhelpful.
Context: a settlement/auction model in Daml with a Spring Boot service over the Ledger API via the Java bindings. Local build was SDK 2.9.4 / LF 1.14 / Ledger API v1. Target was a shared devnet participant on Canton 3.x with Keycloak-issued JWTs over TLS. I ported against 3.4.11 — the network has moved on to 3.5/3.6 since, so some details may already differ.
- DAR upload rejected: “Disallowed language version … expected 2.1, 2.2, 2.3 but got 1.14”
Canton 3.x only accepts LF 2.x packages; SDK 2.9.4 emits LF 1.14. Rebuilt the DAR with SDK 3.4.11 (LF 2.2).
- LF 2.x removed contract keys
My Instrument template declared a key/maintainer, which doesn’t compile on LF 2.x. Nothing looked the instrument up by key — the app queries the ACS — so removing it kept the package portable across both SDK lines.
- The v2 Java bindings are a different API (182 compile errors)
All the breakage was in two files:
- the PartyManagementServiceGrpc admin channel is gone from the rxjava bindings — I read the party roster from config instead, and on a shared node the parties are pre-allocated anyway
- client.getActiveContractSetClient() became client.getStateClient()
- ACS queries are offset-anchored: getActiveContracts(filter, parties, verbose) became getActiveContracts(filter, parties, verbose, ledgerEndOffset), fetching getStateClient().getLedgerEnd() per query
- CommandsSubmission.create(appId, cmdId, cmds) gained an optional synchronizerId
- withAccessToken(Optional) became withAccessToken(String)
- the getLedgerId() handshake is gone — a successful connect is the confirmation
One time-waster here: javap was resolving the 2.9.4 jar ahead of the 3.4 one, so I spent a while reading the old signatures. Worth pointing it at the exact jar.
- UNIMPLEMENTED: CommandService/SubmitAndWaitForTransactionTree
Canton 3.3+ removed the transaction-tree command endpoints. Switching to submitAndWaitForTransaction(…, TransactionFormat) with the LEDGER_EFFECTS shape returns the same full node set, and since CreatedEvent implements both Event and TreeEvent the decoding logic barely changed.
- PERMISSION_DENIED on every submit, while reads returned 200
The one that took longest, because the client-side error says nothing usefuwas denied, including after the node operator granted actAs.
What worked was going to the participant’s own logs (Grafana/Loki) and searid. The real message was:
Claims are only valid for userId ‘’, actual userId is ‘canton-dvp-desk’
I had set an arbitrary application name. In Ledger API v2 the submission’s applicationId is the user id, and it has to equal the token’s sub. @WallaceKelly explains the same underlying identity model in “Claims do not authorize to act as party” — worth reading if you’re comparing a UI session against an API call.
- Claims do not authorize to act as party ‘Issuer’
Immediately after fixing 5. The actAs claims match the full party id (Issuelay label. I added a resolve step mapping labels to full ids beforesubmitting.
After that: HTTP 201, and a full atomic two-leg settlement on the shared node, visible only to the parties and the auditor.
Two things I’d have wanted to know at the start:
- the client-side gRPC error is often not the real error; the participant’s logs have the actual reason, keyed by transaction id
- identity in v2 is stricter than it looks — applicationId, the token’s subave to line up
Happy to be corrected on any of this; it’s one app’s experience on one node, not a general guide.
I had set an arbitrary application name. In Ledger API v2 the submission’s applicationId is the user id, and it has to equal the token’s sub. @WallaceKelly explains the same underlying identity model here — worth reading if you’re comparing a UI session against an API call:
Hello, I am trying to mint via the API request but got this error msg. However, via the utility UI, I can mint using the same PartyId with no problem. This issue occurred after the upgrade my validator in Devnet to 0.5.1. {"@timestamp":"2025-11-19T14:33:48.790Z","message":"Request com.daml.ledger.api.v2.CommandService/SubmitAndWaitForTransaction by inprocess-grpc-5001: received a message \n Request tid:9800ba569988ecd802137faba00a7468","logger_name":"c.d.c.n.g.ApiRequestLogger:participant=par…