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
Front the samepkg/cantonsdk/fee engine with a deployable gRPC service (pkg/feegate) that speaks the Canton Ledger API. Write submissions get a CC fee leg injected; every other call is a transparent passthrough. Any service, in any language, points its endpoint at the proxy and authenticates with the same JWT it would use against Canton. This is a front-end over the engine — not a rewrite.
Why a proxy in addition to the library
Phase 1 — library
Phase 2 — proxy
Integration
import a Go package
change one endpoint
Languages
Go only
any language
"No bypass" enforcement
discipline (use wrapped client)
network isolation (node reachable only via proxy)
API coverage
calls the middleware makes
all Ledger API calls, incl. future ones
Best when
Go-only, minimize moving parts
polyglot services, or fee guaranteed org-wide
Design
The proxy registers the Ledger API gRPC services and, for each, either injects a fee leg (writes) or forwards verbatim (everything else). It holds an upstream connection to the real Canton node and reuses the same fee.Gate built in Phase 1.
The interactive prepare path (InteractiveSubmissionService.PrepareSubmission) is intercepted the same way, so the fee leg is inside the hash the client signs.
Passthrough — everything else
StateService, UpdateService, PartyManagementService, reads, streams, and any future service are registered as thin forwarders. A generic streaming forwarder covers server-streaming calls (e.g. GetUpdates, GetActiveContracts):
The caller sends the same bearer JWT it would send to Canton; the proxy copies the authorization metadata onto the upstream context (forwardCtx) and does not mint its own token. The operator's fee-leg settlement relies on the pre-provisioned TransferPreapproval, not on the caller's identity.
gate:=fee.NewGate(scan.NewOracle(cfg.ScanURL), fee.NewSizer(), cfg.FeeParty, cfg.PreapprovalCID, calcCfg, cfg.Mode)
up:=ledger.New(cfg.Upstream) // connection to the real Canton nodes:=grpc.NewServer()
lapiv2.RegisterCommandServiceServer(s, &commandProxy{upstream: up.Command(), gate: gate})
lapiv2.RegisterInteractiveSubmissionServiceServer(s, &interactiveProxy{upstream: up.Interactive(), gate: gate})
lapiv2.RegisterStateServiceServer(s, &statePassthrough{upstream: up.State()})
lapiv2.RegisterUpdateServiceServer(s, &updatePassthrough{upstream: up.Update()})
// ...party mgmt, etc. — passthrough
Deployment
Run feegate as its own service; restrict the Canton node's Ledger API so it's reachable only via the proxy (network isolation). Services swap their ledger endpoint to the proxy — no other change. Same fee.mode staging (Observe → Quote-only → Collect) as Phase 1.
Acceptance criteria
pkg/feegate + cmd/feegate gRPC service reusing the Phase 1 fee.Gate unchanged (no engine changes required — if the engine needs edits, that's a Phase 1 gap).
CommandService and InteractiveSubmissionService write paths inject the CC fee leg; fee leg is inside the signed hash on the interactive path.
StateService, UpdateService, party mgmt, and streaming calls pass through transparently (byte-for-byte responses; streams relayed).
JWT metadata forwarded upstream unchanged; proxy mints no token of its own.
fee.mode staging works identically to Phase 1.
Documented deployment with the node reachable only via the proxy (no-bypass network isolation).
Integration test: a Go client pointed at the proxy completes a transfer with the fee leg present, and a read/stream call returns identical results to hitting the node directly.
Part of the Fee Module epic (#366). Depends on #368 (the engine). Design:
docs/fee-module-design.md§6.Front the same
pkg/cantonsdk/feeengine with a deployable gRPC service (pkg/feegate) that speaks the Canton Ledger API. Write submissions get a CC fee leg injected; every other call is a transparent passthrough. Any service, in any language, points its endpoint at the proxy and authenticates with the same JWT it would use against Canton. This is a front-end over the engine — not a rewrite.Why a proxy in addition to the library
Design
The proxy registers the Ledger API gRPC services and, for each, either injects a fee leg (writes) or forwards verbatim (everything else). It holds an upstream connection to the real Canton node and reuses the same
fee.Gatebuilt in Phase 1.flowchart TB IN["Incoming Ledger API call"] --> Q{"write submission?"} Q -->|yes| INJ["decode -> Gate.Process -> forward augmented tx"] Q -->|"no — reads, streams, party mgmt, ..."| PASS["transparent passthrough"]Write interception —
CommandServiceThe interactive prepare path (
InteractiveSubmissionService.PrepareSubmission) is intercepted the same way, so the fee leg is inside the hash the client signs.Passthrough — everything else
StateService,UpdateService,PartyManagementService, reads, streams, and any future service are registered as thin forwarders. A generic streaming forwarder covers server-streaming calls (e.g.GetUpdates,GetActiveContracts):Auth — JWT passthrough
The caller sends the same bearer JWT it would send to Canton; the proxy copies the
authorizationmetadata onto the upstream context (forwardCtx) and does not mint its own token. The operator's fee-leg settlement relies on the pre-provisionedTransferPreapproval, not on the caller's identity.Wiring —
cmd/feegateDeployment
Run
feegateas its own service; restrict the Canton node's Ledger API so it's reachable only via the proxy (network isolation). Services swap their ledger endpoint to the proxy — no other change. Samefee.modestaging (Observe → Quote-only → Collect) as Phase 1.Acceptance criteria
pkg/feegate+cmd/feegategRPC service reusing the Phase 1fee.Gateunchanged (no engine changes required — if the engine needs edits, that's a Phase 1 gap).CommandServiceandInteractiveSubmissionServicewrite paths inject the CC fee leg; fee leg is inside the signed hash on the interactive path.StateService,UpdateService, party mgmt, and streaming calls pass through transparently (byte-for-byte responses; streams relayed).fee.modestaging works identically to Phase 1.