Galanthus Architecture Reference
This document explains how the current codebase is structured internally. For command-line usage and concrete option details, see the CLI reference. For the current v5 public library surface, see the library reference and C ABI conventions. For a higher-level project overview, start with the README.
Protocol and payload references:
Purpose
Galanthus is a C++ FinTS 3.0/4.1 client with a deliberately small layer stack:
- CLI entrypoints parse arguments and dispatch commands.
protocolbuilds messages, parses responses, and manages dialog/TAN state.wireturns FinTS messages into raw bytes and back.transportsends one HTTPS request and returns raw bytes.storagepersists the encrypted state and resume artifacts.domainholds the shared data model used by all layers.
That split is intentional. The code avoids a large application-service layer and keeps FinTS-specific logic close to the wire format.
The codebase is private and under active development. When a newer library-facing contract is demonstrably better and already tested, the older CLI- or migration-specific behavior should be removed rather than preserved by default.
Top-Level Module Layout
src/galanthus/domain
The domain layer contains the shared value types and security helpers used throughout the project:
types.hppdefines the canonical data model:Tan_mechanism,Tan_challengeBank_parameter_data,User_parameter_dataAccount_info,Balance_info,Transaction_infoStanding_order_info,Security_holding_info,Tan_media_infoPersistent_state,Dialog_state
secure_string.hppandsecure_erase.hppprovide secret handling.error.hppdefines the exception hierarchy.
These types are the boundary between protocol parsing and the CLI JSON layer.
src/galanthus/wire
The wire layer is the generic FinTS/HBCI message representation:
wire.hppdefinesMessage,Segment,Group,Component, andBinary_element.parser.cppparses raw bytes or text into aMessage.serializer.cppturnsMessageback into raw bytes.encoding.cpphandles ISO-8859 encoding and base64 helpers.
Everything above the wire layer works in terms of wire::Message and wire::Segment before it becomes domain data.
src/galanthus/transport
The transport layer is intentionally narrow:
transport.hppdeclares theTransportinterface with one method:send(endpoint, payload) -> response bytes
https_transport.cppimplements that interface usinglibcurl.
The project does not own a generic networking stack. It needs one HTTPS POST client with certificate validation, timeout handling, and response-body capture.
src/galanthus/protocol
This is the main application logic layer. It knows about FinTS dialogs, command families, response parsing, and TAN handling.
backend.*is the protocol-family selection seam introduced for staged FinTS 4.1 work. It extracts advertised backend capability from BPD and resolves the locally supported backend conservatively before requests are dispatched.bootstrap.*handles anonymous bootstrap, authenticated bootstrap parsing, BPD/UPD/TAN discovery, segment-version selection, and dialog end for the anonymous path.auth.*handles authenticated bootstrap, authenticated dialog end,send_and_parse, HNVSD payload extraction, and TAN challenge extraction.readonly.*builds and parses read-only operations:- accounts
- balances
- transactions
- holdings
- TAN media
- standing orders
transfer.*builds and parses write operations:- single SEPA credit transfer
- batch SEPA credit transfer
- standing order create / modify / delete
- direct debit single / batch / amendment
- prepaid top-up
resume.*serializes and deserializes the resume artifact used by interrupted TAN flows.
The protocol layer is where FinTS segment identifiers, BPD-selected versions, TAN requirements, and JSON result classification are turned into actual command behavior.
src/galanthus/camt and src/galanthus/mt940
These are payload parsers for bank statement formats:
camt_parser.*parses CAMT XML payloads and turns them into transaction or balance data.mt940_parser.*parses MT940 statement text.
The read-only transaction path uses these parsers as a fallback/selection mechanism depending on what the bank exposes.
CAMT payload handling follows the ISO 20022 message families published in the ISO 20022 message catalogue.
src/galanthus/storage
This layer handles local persistence and secret-protected files:
crypto.cppimplements key generation/loading, AES-GCM packing/unpacking, and file-name resolution for the key file.compression.cppimplements the compression layer used inside encrypted blobs.state.cpppersists the durable client state.resume_file.cpppersists TAN resume artifacts.
Storage is versioned, encrypted, compressed, size-limited, and protected with file locks and restrictive permissions.
cli
The CLI layer contains the user-facing command handling:
main.cppdefines the CLI11 command tree and global options.commands.cppimplements the actual command logic.profile_registry.cpppersists and resolves CLI profiles.json_output.cppserializes command results and errors to JSON.secret_input.cppreads PIN and TAN from CLI options, stdin, or an interactive hidden prompt.
The CLI does not contain FinTS parsing logic itself. It orchestrates protocol calls and formats results.
That orchestration ownership is currently being reduced. The target architecture is for the CLI to
be a thin adapter over the client layer, not the long-term home of reusable banking workflow
logic.
Shared Follow-Up Layout
Shared runtime follow-up state lives in src/galanthus/runtime/common_outcome.hpp as
runtime::Follow_up_required<T>. That object is the backend-agnostic carrier for
"typed result exists, but it is not operational yet."
Backend adapters populate it in the runtime layer, for example in protocol-family adapters such as
src/galanthus/runtime/ebics_common_adapter.cpp, by setting the typed value plus stable follow-up
metadata such as message, hint, resource_kind, and resource_id.
The CLI edge is where backend-specific action framing is added when needed. Local command support
lives in cli/commands.cpp, while operational command implementations that open a backend live in
cli/commands_capi.cpp and go through the public C API. Shared schema-v2 helpers only transport
that metadata into the JSON envelope.
Shared schema-v2 envelope building lives in cli/json_output.cpp. It turns runtime follow-up into
top-level status: "follow_up_required" plus action.type: "manual_follow_up_required" without
inventing backend workflow semantics.
The normative field meaning for this boundary is documented in runtime_common_follow_up_semantics.md.
Request / Response Flow
The runtime path for a command is usually:
- Parse CLI options in
main.cpp. - Build or load the persistent state in
commands.cpp. - Create a bootstrap config and authenticate against the bank.
- Build a
Readonly_sessionfrom the bootstrap/auth result. - Build the command-specific FinTS message.
- Send it through the
Transportinterface. - Parse the returned
Auth_resultor command-specific payload. - If TAN is required, persist a resume artifact and return an interruption outcome.
On the CLI this now surfaces as top-level
status: "action_required"withaction.type: "tan_required". - If the command completes, end the dialog and persist updated state.
The core transport boundary is always:
- protocol code builds a
wire::Message wire::serialize_message()turns it into bytes- transport sends bytes to the endpoint
wire::parse_message()reconstructs the response- protocol parsers turn response segments into domain output
Bootstrap, Auth, and Session Model
There are three related session/config layers:
Bootstrap_config/Bootstrap_result- used for anonymous bootstrap and initial capability discovery
- parses BPD, supported HBCI versions, and TAN mechanism metadata
Authenticated_bootstrap_config/Auth_result- used for the authenticated bootstrap path
- carries endpoint, bank code, user ID, PIN, product info, selected TAN mode, and timeout
Auth_resultholds the dialog ID, message number, BPD/UPD, segment versions, TAN required segments, and challenge data
Readonly_session- a live command session used after bootstrap
- contains the authenticated config, the latest auth result, and mutable dialog state
Readonly_session is the standard operational state object. It is passed into most protocol builders so they can choose segment versions, append TAN segments when required, and advance dialog numbering correctly.
send_and_parse() in auth.cpp is the central authenticated exchange helper. It:
- serializes a message
- sends it
- parses the response
- advances dialog counters when a response is received
- sets
dialog_abortedon9800 - optionally rejects status errors immediately
Stage 1 FinTS 4.1 groundwork now routes that exchange through an explicit backend seam:
protocol/backend.*resolves the locally supported protocol family and versionAuthenticated_exchange_requestinauth.hppis the extensible request envelopesend_and_parse()dispatches to the appropriate backend implementation
The dispatch seam is now real for both the FinTS 3.0 binary path and the currently implemented FinTS 4.1 feature families. Runtime activation still remains conservative and feature-specific.
TAN and Resume Model
TAN handling is split into two pieces:
-
protocol::Tan_challenge- returned from bootstrap or command parsing when the bank requires TAN
- contains the human-readable challenge, optional HTML, optional HHD_UC payload, task reference, TAN medium name, and flags such as
decoupled
-
protocol::Resume_artifact- encrypted on-disk artifact used to continue an interrupted command
- stores the command name, dialog identifiers, message and security counters, cached BPD/UPD, TAN metadata, and command arguments
The TAN/resume flow is:
- a command sends a FinTS request
- if the response requires TAN, the command serializes a
Resume_artifact save_resume()writes that artifact to disk using the storage layer- the CLI returns top-level
status: "action_required"withaction.type: "tan_required" tan resumelater loads the artifact, validates the identity, reconstructs the session, and continues the operation
tan resume is not a special-case one-off; it is the continuation mechanism for any command that can be interrupted by TAN.
Persistence Model
There are two persistent file types:
- durable state file
- CLI profiles normally resolve this to a per-profile state path under the platform config root
- contains
Persistent_state - encrypted/compressed with a versioned magic string
- stored alongside a key file resolved by
resolve_key_path()(currently.galanthus_fints.key)
- resume artifact
- file name is command-specific, typically
*.resume - contains the encrypted
Resume_artifact
- file name is command-specific, typically
Important properties:
- storage uses
File_locksidecar locking to avoid concurrent writers - file size is bounded before decompression
- resume/state blobs are encrypted with authenticated encryption
- file permissions are restricted after writes
- the system uses fixed magic strings:
GALANTHUS_FINTS_STATEGALANTHUS_FINTS_RESUME
Persistent_state stores long-lived connection identity and capability data:
- endpoint, bank code, user ID, customer ID
- product ID and version
- system ID
- cached BPD/UPD
- selected TAN mechanism / medium
- supported segment versions and TAN-required segments
The state file is not the same thing as the resume artifact. The state file is the long-lived client memory; the resume artifact is the short-lived continuation snapshot.
Parser Layers
Parsing is layered and deliberately conservative.
1. Wire parsing
wire::parse_message() reconstructs a Message from raw bytes. At this layer, the code only knows about segments, groups, components, and binary payloads.
2. Authenticated envelope parsing
protocol::extract_hnvsd_payload() unwraps the authenticated HNVSD payload and returns the inner message. This is the bridge between the outer FinTS envelope and the command-specific body.
3. Status extraction
protocol::extract_statuses() and the Fints_status type capture the status codes, messages, reference segments, and parameters that drive result classification and continuation handling.
4. Domain parsers
Each FinTS family has dedicated builders/parsers:
bootstrap.*parses BPD/UPD/TAN capability datareadonly.*parses accounts, balances, transactions, holdings, TAN media, and standing orderstransfer.*parses transfer/direct-debit/prepaid results and classifies them as accepted, rejected, partial, or ambiguouscamt_parser.*andmt940_parser.*parse statement payloads into transaction records
The parsers are intentionally not generic XML mappers. They are practical, family-specific parsers that understand exactly the segment layouts the project supports.
Testing Structure
Testing is feature-oriented.
Core test files
test_wire_parser.cppandtest_wire_serializer.cpp- wire format and low-level serialization
test_encoding.cpp- encoding helpers
test_bootstrap.cppandtest_auth.cpp- bootstrap/auth envelope and capability parsing
test_readonly*.cpp- read-only command builders and parsers
test_transfer*.cpp- transfers, batch transfers, standing orders, direct debits, prepaid top-up
test_resume.cpp- resume artifact format and roundtrips
test_mt940.cppandtest_camt.cpp- payload parsing
test_storage.cpp- state/resume/key handling, file locks, compression, encryption
test_cli_json_output.cpp,test_cli_contract.cpp,test_gln_cli.cpp,test_cli_profile_registry.cpp, andtest_cli_runtime_payments.cpp- JSON output, schema contracts, profile plumbing, command dispatch, and runtime-envelope behavior for dispatched CLI commands
Shared support
Shared helpers live under tests/support/:
common_test_support.hppreadonly_test_support.hpptransfer_test_support.hpp
The project prefers shared helpers over copy-pasted fixtures. The readonly tests now use split feature-family fragments that are included from test_readonly.cpp, because the test target is still enumerated explicitly in CMake.
Test philosophy
- Test behavior, not just serialization shape.
- Keep parser tests realistic.
- Keep runtime/CLI tests focused on command outcomes.
- Use regression tests for previously observed bugs.
- Avoid redundant tests that assert the same thing in multiple layers without a reason.
Build and Dependency Model
The build is CMake-based and produces:
galanthus_libstatic libraryglnexecutablegalanthus-live-suitelive-test runner executablegalanthus_teststest executablegalanthus_capishared library
The current dependency model is:
libcurlfor HTTPS transport- fetched with
FetchContent - Windows build uses Schannel instead of OpenSSL
- fetched with
glazefor JSON serialization/deserializationpugixmlfor XML parsingminizfor compressionCLI11for CLI parsingdoctestfor tests
tests/CMakeLists.txt explicitly enumerates the test translation units. That is why some test families are split into include fragments rather than separate compiled test files.
Key Design Tradeoffs
Narrow abstractions over generality
The project uses a small number of concrete layers instead of a heavy service/adapter architecture. That keeps FinTS behavior easy to reason about and avoids a large amount of scaffolding.
Protocol builders live close to the wire
Request builders and response parsers are explicit about segment names, versions, and required fields. This is less abstract than code generated from schemas, but it makes the actual supported FinTS subset visible in code.
Persistent resume artifacts instead of in-memory retries
TAN interruption is treated as a real persisted continuation state, not as a transient retry loop. That is important because some TAN flows are decoupled and can survive process boundaries.
CLI result structs mirror output schema
The CLI result types are shaped to match the JSON output that the user sees. That keeps the command layer thin and makes contract tests meaningful.
Feature-oriented tests over giant mixed files
The test suite is being pushed toward feature-family files plus shared helpers. The goal is to keep each test translation unit understandable and reduce accidental duplication.
Conservative parsing and validation
Where the protocol is ambiguous, Galanthus prefers explicit validation and conservative result classification over guessing. That is visible in the transport result types, TAN handling, and bank statement parsing.
Practical Reading Order
If you want to understand the project quickly, read it in this order:
src/galanthus/domain/types.hppsrc/galanthus/wire/wire.hppsrc/galanthus/protocol/bootstrap.hppandauth.hppsrc/galanthus/protocol/readonly.hppsrc/galanthus/protocol/transfer.hppcli/commands.hppandcli/commands.cpptests/test_readonly.cppand the split readonly fragments