Tenzro
Bridges and interoperability

Canton.

Enterprise Canton adapter. DAML on Canton 3.5+ with the JSON Ledger API v2, CIP-26 User Management, and CIP-56 Canton Coin interop.
STATUS
Live on testnet
CRATE
tenzro-bridge
STABILITY
Stable
REFERENCE
Canton 3.5.1
01

Adapter

CantonAdapter in tenzro-bridge speaks the Canton 3.5+ JSON Ledger API v2. It bridges between Tenzro TNZOand Canton CIP-56 Canton Coin holdings, exposes the participant’s DAML contract set to callers, and runs the DAML submit-and-wait command path. All access flows through the Tenzro node’s scoped API surface.

02

Read surface

21 MCP tools at https://canton-mcp.tenzro.network/mcp and matching JSON-RPC methods at https://rpc.tenzro.network:

  • canton_list_domains — synchronizer domains the node is configured against
  • canton_list_contracts — active-contracts query with template_ids filter (the node attaches the live ledger-end offset and the resolved FQ party id automatically)
  • canton_list_partiesGET /v2/parties/known
  • canton_list_packagesGET /v2/packages — installed DAR package ids
  • canton_health — combined /livez + /readyz + /v2/version probe with version + CIP feature flags
  • canton_version — participant version + CIP feature flags (verified Canton 3.5.1)
  • canton_get_my_user — returns the Canton user record for the presented credential (CIP-26 User Management)
  • canton_coin_balance — CIP-56 Canton Coin balance from Splice.Amulet:Amulet contracts
  • canton_fee_schedule — latest Splice.AmuletRules:AmuletRules active contract
  • canton_connected_synchronizersGET /v2/state/connected-synchronizers
  • canton_get_transaction, canton_get_events, canton_get_balance
  • canton_get_my_analytics — per-API-key call counters (calls_total, errors_total, per-method counts, first_seen_at, last_called_at) for the presented credential
  • canton_list_api_key_analytics — operator admin-read across every tenant (admin-token-gated)
  • canton_list_idps — list configured Canton identity providers (admin-token-gated)
  • canton_watch_party — active-contracts query scoped to an explicit party. The presenting key must be authorized for that party: either it matches the key’s primaryParty, or the party is on the key’s can_read_as_parties / can_act_as_parties whitelist. Anything else returns -32004.
03

Write surface

  • canton_submit_command — DAML create / exercise via the submit-and-wait JSON Ledger API path. The presenting API key must carry a bound canton_user_id; the node resolves the user’s primaryParty and uses it as actAs. Optionally pass act_as: <party> to pin a specific party — the node verifies the party either matches the key’s primaryParty or is on the key’s can_act_as_parties agent-delegation whitelist, otherwise the call returns -32004. A key without canton_user_id is rejected (the operator’s default party is never inherited).
  • canton_allocate_partyPOST /v2/parties — returns the fully-qualified party id <hint>::<participant-hash>
  • canton_grant_user_rightsPOST /v2/users/{userId}/rights — grant CanActAs / CanReadAs on a party to a tenant’s user (CIP-26). Required before a newly-allocated party can be acted on. Users under a non-default identity provider also need identity_provider_id.
  • canton_list_user_rightsGET /v2/users/{userId}/rights — inspect what a tenant can act/read as.
  • canton_upload_dar — DAR upload via POST /v2/packages with a single Content-Type: application/octet-stream header. Canton 3.5+ rejects requests carrying duplicate Content-Type headers.
  • canton_transfer, canton_create_asset, canton_dvp_settle
  • canton_reconnect_synchronizer — submit POST /admin/participant/synchronizer/{alias}/reconnect via the Admin API; used after a synchronizer outage or planned disconnection
04

Operator-only writes

These methods write contracts under the operator’s participant-default party or mutate per-tenant IdentityProviderConfig entries. They are admin-token-gated at the node — non-admin callers see -32001. Tenant API keys cannot produce these contracts or registrations.

  • tenzro_mirrorWorkflowToCanton / tenzro_canton_mirrorReceipt — mirror a Tenzro workflow into a Canton synchronizer as a Tenzro.Workflow:WorkflowAnchor contract. The contract owner is the operator’s participant-default party — the mirrored payload is internal node state.
  • tenzro_mirrorObligationToCanton — mirror an Obligation under an already-mirrored workflow as a Tenzro.Workflow:ObligationAnchor contract.
  • canton_create_idp / canton_delete_idp — manage Canton IdentityProviderConfig entries. Required for the Stage 2.b per-tenant IDP flow.
  • tenzro_canton_listApiKeyAnalytics / tenzro_canton_aggregateAnalytics — operator admin-read across every tenant’s call counters.
05

Verified wire-level facts (Canton 3.5+)

Four verified facts pin the adapter to the live Canton 3.5+ contract:

  • Active-contracts requests use the eventFormat wrapper. Canton 3.5 dropped the legacy top-level filter field. POST /v2/state/active-contracts now expects { eventFormat: { filtersByParty, filtersForAnyParty, verbose }, activeAtOffset }. The legacy filter + top-level verbose shape is rejected with Invalid value for: body.
  • activeAtOffset is a JSON number. POST /v2/state/active-contracts rejects null, empty string, or negative values with HTTP 400. The adapter fetches the current ledger-end via GET /v2/state/ledger-end before every query and serializes the result as a number.
  • Party ids must be fully qualified. The filtersByParty map keys and requestingParties arrays must use <hint>::<participant-hash> form. The adapter resolves the bare party hint to its FQ form via CIP-26 User Management on first call and caches the result.
  • DAR upload requires a single Content-Type header. The bridge constructs the upload request directly (not through the JSON-tagged helper) so the only Content-Type sent is application/octet-stream.
06

Access — operator API key

Access is granted via a scoped tnz_<base64url> API key with scope canton, presented as the X-Tenzro-Api-Key header (REST) or api_key JSON-RPC param. Methods gated by this scope match the patterns tenzro_*Canton*, tenzro_canton_*, and tenzro_*Daml* (case-insensitive). See auth surfaces for the contract.

07

Multi-tenant isolation

Each developer team is isolated behind its own API key. The node refuses Canton submissions from any key that does not carry a bound canton_user_id — there is no fallback to an operator-default party. When the key is bound, the node resolves the user’s primaryParty via CIP-26 User Management and uses it as actAs. Canton’s AuthService enforces per-user CanActAs rights server-side as defence-in-depth.

Callers can override the default by passing act_as: <party> on the JSON-RPC call (or as the trailing argument on the Rust / TS SDK methods). The node authorizes the override against the key: either the party matches the key’s resolved primaryParty, or it appears on the key’s can_act_as_parties agent-delegation whitelist. Anything else is rejected with -32004. The operator’s own party can never be used by a tenant key.

Tenant onboarding is automated in a single operator call. When the operator mints an API key with the Canton binding set, the node atomically allocates a tenant party, registers the Canton user with that party as primaryParty, and grants the user CanActAs on its primary party. After issuance, the bound key auto-resolves actAs / requestingPartiesas the tenant’s party on every Canton call.

The response includes canton_primary_party and a canton_provisioning summary. Pass auto_provision_canton: false in the JSON-RPC params to opt out (e.g. when the tenant has been pre-provisioned out of band). The provision step is idempotent: re-issuing a key for an existing Canton user reports status: already_exists and reuses the existing primary party.

08

Per-tenant analytics

Every canton-scoped RPC call increments a per-API-key counter in RocksDB (CF_CANTON_ANALYTICS). The aggregate captures calls_total, errors_total, per-method counts, plus first_seen_at and last_called_at— the answer to “how many DAML transactions has the tenzro-labs team submitted this month?” Two RPCs surface the counters:

  • tenzro_canton_getMyAnalytics — subject self-read for the presented API key. No admin token required.
  • tenzro_canton_listApiKeyAnalytics — operator admin-read across every tenant. Admin-token-gated.

CLI: tenzro canton my-analytics for self-read, tenzro canton list-analytics for operator admin-read. SDKs expose getMyAnalytics() / listApiKeyAnalytics(keyId?) on CantonClient. The Python MCP exposes canton_get_my_analytics / canton_list_api_key_analytics.

09

Per-tenant OAuth isolation

For deployments that require dedicated OAuth clients per tenant rather than a shared operator principal, the node provisions per-tenant OAuth isolation automatically. With canton.identity_providers.enabled set, a single tenzro_createApiKey call atomically mints a per-tenant upstream OAuth2 client via the operator’s configured identity-provider management API, allocates the tenant party, creates the Canton user <client_id>@clients, and grants CanActAs. The minted credentials are stored on the key record — the tenant’s only credential is the tnz_... API key. The response’s tenant_oauth_clientfield carries client_id, issuer URL, and audience for the operator’s records; the client secret never leaves the node.

On every canton-scoped call the node mints and caches the tenant’s Canton JWT server-side from the stored credentials and forwards it via CantonAdapter::with_tenant_jwt, so the operator credential never appears on the wire for tenant requests — a leaked operator JWT does not touch tenant data, and a tenant never runs a token-acquisition loop. Tenants on their own upstream issuer may instead present a JWT via the X-Canton-Auth: Bearer <jwt> header. Canton’s AuthService matches the JWT’s sub to the tenant’s user and enforces CanActAs server-side. Revoking the Tenzro API key tears down the upstream OAuth client and the Canton-side rights in lockstep.

Three operator RPCs back the IDP lifecycle: tenzro_canton_createIdp, tenzro_canton_listIdps, tenzro_canton_deleteIdp — admin-token-gated. Configure via the CANTON_IDP_* environment variables on the node.

10

Verified Canton 3.5 wire shape

The POST /v2/commands/submit-and-wait-for-transaction request body has two non-obvious wire constraints that Canton 3.5 strictly enforces:

  • Outer commands wrapper. The JsCommands object is nested under a top-level commands key: { "commands": { "commands": [...], "commandId": ..., "userId": ..., "actAs": [...] } }. A flat body (the JsCommands fields at the root) is rejected with HTTP 400 and surface error JSON decoding to CNil should never happen at 'commands.commands[0]'.
  • Externally-tagged JsCommand. Each entry in the inner commands array is encoded by circe as an externally-tagged union: { "CreateCommand": { "templateId": ..., "createArguments": {...} } } or { "ExerciseCommand": { "templateId": ..., "contractId": ..., "choice": ..., "choiceArgument": ... } }. The internally-tagged shape ({ "commandType": "create", "templateId": ..., ... }) is silently rejected as none of the union variants match.

The bridge produces both shapes correctly. A regression test in tenzro-bridge::canton::tests::submit_request_nests_jscommands_under_commands_key pins the serialised bytes offline.

11

DvP

Delivery vs Payment via on-Canton atomic swap of asset against payment. Choreographed through DAML templates and exercised via canton_dvp_settle.

Related
← All docs