Skip to content

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:

  1. CLI entrypoints parse arguments and dispatch commands.
  2. protocol builds messages, parses responses, and manages dialog/TAN state.
  3. wire turns FinTS messages into raw bytes and back.
  4. transport sends one HTTPS request and returns raw bytes.
  5. storage persists the encrypted state and resume artifacts.
  6. domain holds 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.hpp defines the canonical data model:
    • Tan_mechanism, Tan_challenge
    • Bank_parameter_data, User_parameter_data
    • Account_info, Balance_info, Transaction_info
    • Standing_order_info, Security_holding_info, Tan_media_info
    • Persistent_state, Dialog_state
  • secure_string.hpp and secure_erase.hpp provide secret handling.
  • error.hpp defines 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.hpp defines Message, Segment, Group, Component, and Binary_element.
  • parser.cpp parses raw bytes or text into a Message.
  • serializer.cpp turns Message back into raw bytes.
  • encoding.cpp handles 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.hpp declares the Transport interface with one method:
    • send(endpoint, payload) -> response bytes
  • https_transport.cpp implements that interface using libcurl.

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.cpp implements key generation/loading, AES-GCM packing/unpacking, and file-name resolution for the key file.
  • compression.cpp implements the compression layer used inside encrypted blobs.
  • state.cpp persists the durable client state.
  • resume_file.cpp persists 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.cpp defines the CLI11 command tree and global options.
  • commands.cpp implements the actual command logic.
  • profile_registry.cpp persists and resolves CLI profiles.
  • json_output.cpp serializes command results and errors to JSON.
  • secret_input.cpp reads 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:

  1. Parse CLI options in main.cpp.
  2. Build or load the persistent state in commands.cpp.
  3. Create a bootstrap config and authenticate against the bank.
  4. Build a Readonly_session from the bootstrap/auth result.
  5. Build the command-specific FinTS message.
  6. Send it through the Transport interface.
  7. Parse the returned Auth_result or command-specific payload.
  8. 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" with action.type: "tan_required".
  9. 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_result holds 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_aborted on 9800
  • 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 version
  • Authenticated_exchange_request in auth.hpp is the extensible request envelope
  • send_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:

  1. 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
  2. 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" with action.type: "tan_required"
  • tan resume later 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

Important properties:

  • storage uses File_lock sidecar 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_STATE
    • GALANTHUS_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 data
  • readonly.* parses accounts, balances, transactions, holdings, TAN media, and standing orders
  • transfer.* parses transfer/direct-debit/prepaid results and classifies them as accepted, rejected, partial, or ambiguous
  • camt_parser.* and mt940_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.cpp and test_wire_serializer.cpp
    • wire format and low-level serialization
  • test_encoding.cpp
    • encoding helpers
  • test_bootstrap.cpp and test_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.cpp and test_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, and test_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.hpp
  • readonly_test_support.hpp
  • transfer_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_lib static library
  • gln executable
  • galanthus-live-suite live-test runner executable
  • galanthus_tests test executable
  • galanthus_capi shared library

The current dependency model is:

  • libcurl for HTTPS transport
    • fetched with FetchContent
    • Windows build uses Schannel instead of OpenSSL
  • glaze for JSON serialization/deserialization
  • pugixml for XML parsing
  • miniz for compression
  • CLI11 for CLI parsing
  • doctest for 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:

  1. src/galanthus/domain/types.hpp
  2. src/galanthus/wire/wire.hpp
  3. src/galanthus/protocol/bootstrap.hpp and auth.hpp
  4. src/galanthus/protocol/readonly.hpp
  5. src/galanthus/protocol/transfer.hpp
  6. cli/commands.hpp and cli/commands.cpp
  7. tests/test_readonly.cpp and the split readonly fragments