zec_wallet_ui

A complete shielded Zcash wallet for your Flutter app, as screens you drop in. Onboarding, backup and restore, balance, send and receive, history, shielding and swaps are ready to mount under your router, styled by your theme, in 16 languages. Wiring it takes about a page of code.

It is optional: build your own screens on zec_wallet instead if you prefer. The UI holds no keys and no wallet logic of its own; everything goes through the SDK, whose Rust core keeps the seed and spending keys out of Dart.

Part of the zec_wallet family: zec_wallet (the SDK), this package, zec_wallet_ui_platform (optional native screen protection for these screens), and zec_wallet_tor (optional Tor).

Status: pre-release (0.0.1), not yet independently audited. It moves real funds through zec_wallet; see SECURITY.md. The SDK example app (zec_wallet/example) is the reference consumer: its main.dart is the canonical "page of glue", and its thin test suite pins what a consumer relies on (font-manifest resolution, delegate composition, shell routing).

What a host wires (the whole glue surface)

  1. A ProviderScope spreading walletOnboardingOverrides(config: …). You own the WalletConfig: network, lightwalletd endpointUrl, TorPolicy, SeedPersistence, broadcast jitter, and the resolved dbDir all come from the host (the SDK validates the config; it never chooses it):

    // All config vocabulary (WalletConfig, Network, TorPolicy, …) is reachable
    // through this package's barrel; the full SDK surface (incl. RustLib.init)
    // comes from `package:zec_wallet/zec_wallet.dart`.
    import 'package:zec_wallet_ui/zec_wallet_ui.dart';
    
    // Bring-your-own policy (production host). dbDir MUST be absolute: a
    // relative path throws at wiring time (on desktop it would otherwise pin
    // the wallet to the launch cwd). On iOS, a bring-your-own dbDir must also
    // be excluded from backups yourself: `await BackupExclusion().exclude(dbDir)`
    // (the reference resolveWalletDbDir does both for you).
    final config = WalletConfig(
      dbDir: myDbDir,                       // your resolved path
      network: Network.main,
      endpointUrl: 'https://your-lightwalletd',
      tor: TorPolicy.required_(runtime: myTorRuntimeConfig),
      seedPersistence: SeedPersistence.sealedKeychain,
      birthdayHeight: null,                 // create at tip; restore overrides
      broadcastJitter: const JitterPolicy.uniform(maxMs: 10000),
      machineMemoPrefixes: const [],        // empty = machineMemos stays closed
    );
    return walletOnboardingOverrides(config: config);
    
    // Or ride the reference defaults verbatim (zec.rocks / Tor off). CAVEAT:
    // buildWalletConfig fixes the network at BUILD time from --dart-define
    // (ZEC_NETWORK, default mainnet; ZEC_ENDPOINT override), so there is no
    // runtime network selection on this path. Bring your own WalletConfig if
    // your product chooses network/endpoint at runtime.
    final dbDir = await resolveWalletDbDir();               // reference helper
    return walletOnboardingOverrides(config: buildWalletConfig(dbDir: dbDir));
    

    walletOnboardingOverrides co-wires the provisioner, onboarding store, and screen-security adapter together (the H2 co-wiring invariant). For finer control, override the individual seams (walletProvisionerProvider, onboardingStoreProvider, screenSecurityProvider) directly; the barrel exports them all.

  2. Swap, only if wanted: walletSwapOverrides() is the REFERENCE policy. It enables the swap surface against the reference 1Click endpoint. That is a product decision, not a default: hosts construct their own SwapHostPolicy (or skip the override entirely; the swap surface then stays honestly disabled).

  3. The theme. Either:

    • (a) buildTheme(WalletColors.light) (or .dark / .darkAmoled, or your own WalletColors), optionally with textTheme: and extensions:; or
    • (b) your own ThemeData with a WalletColors extension registered (map your palette into the package tokens, all of them, since the constructor requires every field).

    Under (b), your theme styles the wallet's components: its buttons, fields, sheets, dialogs, chips and progress bars read your component themes, because no SDK call site sets a size, border or colour of its own (the few exceptions are colours that carry meaning, such as a destructive red). Keep a 48 dp button minimum or Material's padded tap target: a materialTapTargetSize: shrinkWrap theme with no minimumSize shrinks the wallet's Send and Delete buttons under 44 dp. buildTheme sets all of these for you.

    Four hooks restyle the rest; each is optional and falls back to the SDK's default:

    • WalletShapes: the corner radii, one per role (hero, group, tile, bar, notice, qr, field, chip, sheet, dialog, progress). The defaults follow the platform (a group is 26 and a sheet 38 on iOS, 24 and 28 elsewhere).
    • Fonts. The package bundles NO font. Bundle your own and pass a TextTheme to buildTheme(textTheme:); its families are merged over the SDK's sizes. With none, the platform's default face is used.
    • WalletTypography: the mono face for addresses and other identifiers (default: the platform's generic monospace).
    • WalletIcons(builder:): one function from a WalletGlyph (every glyph the wallet draws, named for its meaning: send, receive, shielded, transparent, syncProblem, …) to any widget. Return null for a glyph you don't map and the SDK draws its Material default. Honour the ambient IconTheme when size or colour is null, and announce a non-null semanticLabel.

    WalletColors.qrInk colours the QR modules (default black). The tile stays white, and an ink paler than 7:1 on white falls back to black, because a code that does not scan is an address nobody can pay. The balance card's coin takes coinFace, coinEdge and coinRim (optional; derived from deep and accent when unset). The presets are the refreshed green design (ADR-0564).

    Hide balance. The eye in the wallet header masks every amount on the glance surfaces (the balance card, activity, the transaction sheet, the in-flight note), and screen readers hear "Balance hidden". It never masks an amount the user is acting on (a form, a review, a parked send). The state is walletBalanceHiddenProvider: session-only by default; override it to persist the choice, and read it to mask your own amount surfaces.

    The sync bar hides when the wallet is synced and healthy, and the sync-status sheet then sits in the overflow menu. It hides only on a transport claim that cannot go stale while the tab is open: a direct connection, or a protection your walletHostTransportProvider override declares. A host transport claim overrides every SDK Tor state, the hide included, so declare protection only when your transport guarantees it.

  4. walletLocalizationsFallbackDelegate added to localizationsDelegates next to the host's own generated class (distinct types, so no collision). Use the fallback delegate, NOT the raw WalletLocalizations.delegate: with a host supportedLocales wider than the wallet ARB set, the raw delegate is skipped on the untranslated locale and the first wallet frame throws. The fallback serves English instead. 16 locales ship (lib/l10n/wallet_*.arb), each carrying every string.

  5. walletRoutes() mounted under the host router. go_router is required: the wallet screens navigate via go_router context extensions, so they must live under the host's GoRouter. Navigate via the WalletRoutes constants. The ONE wallet→host navigation is the settings seam: walletAppearanceRoutePathProvider (its historical name) defaults to null (entry points hidden); override it with your settings route to surface the wallet's "Settings" entries.

  6. Optional: sync whenever the app is in the foreground. By default the sync loop starts the first time the wallet screen renders. To sync from the moment your wallet is open, on any screen, listen to the drive once at your root:

    ref.listen(walletSyncDriveProvider, (_, __) {});
    

    It runs while walletSessionProvider is non-null, your sync policy (walletSyncPolicyProvider) is on and the app is not paused. It suspends on paused and resumes on return. With no session it is idle and costs nothing. It does not open the wallet: when the wallet opens (at launch, at unlock, or on first use) is your decision, and sync can start no earlier.

What the package promises a host about lifecycle, reveals and sends

  • One lifecycle mutation at a time. Switch server, delete, rescan, restore, confirm-backup and re-provision run under one operation generation: a second mutation while one is in flight is REFUSED typed (a delete during a server switch answers WalletDeletionOutcome.notDeletable with "finish the server switch first"), and a completion from a superseded operation lands nowhere. A host that chains delete-after-switch must await the switch.
  • A reveal grant is valid for one wallet session instance, one operation generation and one foreground session. Your WalletRevealAuthorizer's shape is unchanged; the package binds what it returns. If the wallet identity changes under a mounted reveal screen (a switch, a delete, the same wallet re-opened under a fresh session), the grant is void, nothing of the new wallet is shown without its own prompt, and a prompt answered after the change reveals nothing. Backgrounding still hides a revealed secret and re-prompts on resume.
  • A tagged send names what it paid. When a push you tagged with a correlationId ends in WalletSendTransactionCreated, recipientAmountZat is the total paid to the send's ONE recipient address, in zatoshis, as signed (fee excluded), or null when the send paid more than one address (a unified address and one of its own receivers count as two). It states what was SIGNED, never that it arrived: gate delivery copy on motion and the wallet's own surfaces, never on the amount's presence. An untagged push gets no amount.

Native handlers: zec_wallet_ui + the optional zec_wallet_ui_platform plugin

Why there are two packages. zec_wallet_ui ships no platform channel handlers of its own. (Its native code comes only through its dependencies: the Rust core in zec_wallet and the QR reader, flutter_zxing.) That is deliberate: it adds no screen-protection policy to your app, and the package stays usable on desktop, where none of the mobile screen protection applies. But two of the wallet's guarantees, Android FLAG_SECURE and the iOS backup exclusion + app-switcher privacy cover, are native and cannot be expressed in Dart. Baking a fixed native policy into the UI package would force it on every host and turn a cross-platform package into a mobile plugin. So that native code lives in a separate, optional companion plugin, zec_wallet_ui_platform, which answers the two platform channels zec_wallet_ui speaks. You pick how to supply them:

  • Most hosts → add the companion plugin. One pubspec.yaml dependency, zero native code in your shell (it self-registers through the generated plugin registrant). This is the supported default and what the reference consumer (zec_wallet/example) uses.
  • A host with its own capture policy → omit it and answer the channels yourself. If your app already manages FLAG_SECURE / app-switcher privacy, or a duress/decoy shell needs a different posture, do not add the plugin and wire your own handlers against the channel contracts (see the hand-wire note below). Keeping the native side out of zec_wallet_ui is what makes this option possible.
  • Desktop → nothing to add. The plugin is Android/iOS-only. On macOS zec_wallet_ui is all you need, and the screen_security channel goes unanswered; the UI degrades honestly (see per-channel behavior below). Linux and Windows cannot hold a wallet yet: zec_wallet has no key store adapter for them, so it refuses to create or open one there (vaultAbsent) and onboarding says the device has no secure key store. Windows is not yet built or run.

The companion plugin ships:

  • Android: the zec_wallet_ui/screen_security handler, FLAG_SECURE over the recovery-phrase (and other secure-tier) screens, re-asserted across Activity-recreating config changes. Device-verified: fl=SECURE on the window, black screenshots/recents thumbnail, survives rotation.
  • iOS: the zec_wallet_ui/backup_exclusion handler (excludes the wallet data dir from iCloud/device backups) plus an app-switcher privacy cover on resign-active.
  • Android AND iOS: the zec_wallet_ui/network_reachability EventChannel, a payload-free tick when the device regains a usable network, so the sync loop resets its retry backoff instead of leaving the badge on "Sync paused" for minutes after a foreground reconnect (airplane toggle, wifi switch, tunnel exit). Android registers a ConnectivityManager.NetworkCallback, which means an Android host merging the plugin inherits the normal-level ACCESS_NETWORK_STATE permission (install-time, no runtime prompt; Play has historically surfaced it as "view network connections", so do not tell your users it is invisible). iOS uses NWPathMonitor. Android's NetworkRequest defaults exclude VPN transports, so a VPN-only reconnect over otherwise-stable wifi ticks on iOS and not on Android. That is worth knowing for a Tor/VPN-heavy userbase; the wallet just falls back to its ladder there. See the plugin's AndroidManifest.xml for the opt-out.

The reference consumer (zec_wallet/example) depends on the plugin and carries no hand-written handlers: its MainActivity.kt/ AppDelegate.swift are deliberately bare. Do NOT add your own handler on either channel alongside the plugin: a handler registered after the plugin registrant silently REPLACES the plugin's (last registration wins on a method channel).

Why this matters, per channel:

  • screen_security fails HONEST without a handler: enable() reports false and the seed screen keeps the "screenshots possible" copy. It never claims a protection that isn't running. The plugin is what turns the copy into a real OS block.
  • network_reachability fails QUIETLY BY DESIGN without a handler: the stream is empty, and nothing is reported to FlutterError either (the adapter awaits its own channel activation so a MissingPluginException is an expected answer rather than a crash-reporter beacon naming our channel). The wallet keeps its pre-plugin behaviour. How much that costs you depends on the platform. On MOBILE a real background/resume still self-heals (backgrounding stops the loop, so the resume spawns a fresh one with a fresh ladder), and you lose only the automatic recovery on a foreground reconnect. On DESKTOP there is no such fallback at all: paused never fires there, so the loop is never stopped and never respawned, and the sync sheet's manual "Try now" is the only recovery. That is how the wallet behaved before this plugin existed, not a regression, but plan for it. A host that already runs its own connectivity listener should override walletNetworkReachabilityProvider instead of adding a second one.
  • backup_exclusion fails SILENT without a handler: nothing in the UI tells you the wallet DB is riding into iCloud/device backups, and a backed-up DB restored onto a new device cannot be opened (its key is a ThisDeviceOnly keychain key that does not travel), stranding the user on the needs-recovery screen. An iOS host without the plugin MUST implement this channel itself.

Only if your shell cannot take the plugin (e.g. a bespoke native embedding): implement the channels by hand against the contracts in screen_security_channel.dart / backup_exclusion.dart / reconnect_kick.dart, using the plugin's Kotlin/Swift sources as the reference implementation. Keep the config-change re-assert (Android) and the per-scene cover (iPad multi-window) behaviors, both of which the naive one-Activity/one-window port drops.

Testing a host integration

import 'package:zec_wallet_ui/testing.dart'; gives you FakeWalletSession (a scripted WalletSession) and the fake onboarding seams, which let a host pump every wallet surface in plain widget tests, no native library needed. The library deliberately imports no flutter_test, so it never constrains a consumer's test-framework solve.

Platform notes

  • No FFI initialization happens at package import; a host without the native library gets the honest "wallet unavailable" surface instead of a crash.
  • Android: flutter_zxing (the on-device QR reader) fails to build on NDK r28+ (upstream #225), so pin NDK r27 in the app's android build config, as the reference consumer does. The QR scan is additive; paste/type always works and is the only path on desktop (on macOS sheets cap at walletSheetMaxWidth on wide windows; Linux and Windows cannot hold a wallet yet).
  • iOS: the camera QR scan (the swap address fields AND the watch-only viewing-key import) is reachable by default on every mobile build, so the host app's Info.plist MUST carry NSCameraUsageDescription. Without it iOS hard-kills the app the moment the reader initializes (a crash, not a soft denial). The reference example app carries it; copy its wording or write your own. Android needs nothing (the camera permission arrives via the plugin manifest and denial degrades to the paste path honestly).
  • Keyboards: the recovery-word and viewing-key fields turn off autocorrect, suggestions, keyboard learning and autofill, but a third-party keyboard still receives every keystroke. On iOS a host can refuse custom keyboards app-wide (application(_:shouldAllowExtensionPointIdentifier:) returning false for .keyboard); Android has no equivalent, and keyboard learning can only be asked off, not enforced.

Independence and trademarks

This package is part of zec_wallet, an independent, open-source project. It is not affiliated with, endorsed by, or sponsored by the Zcash Foundation. "Zcash" is a trademark of the Zcash Foundation; it is used here only to describe the cryptocurrency this library works with. No Zcash logo is used. The project is unrelated to ZecWallet, the discontinued Zcash wallet application.

License

MIT; see LICENSE.

Libraries

core/lifecycle/app_lifecycle_provider
core/router/wallet_navigation
core/router/wallet_router
core/router/wallet_routes
core/theme/colors
core/theme/icons
core/theme/shapes
core/theme/sheet_layout
core/theme/theme
core/theme/typography
features/settings/backup_screen
features/settings/export_viewing_key_screen
features/settings/managed_by_host
features/settings/security_screen
features/wallet/arrival_cue
features/wallet/backup_exclusion
features/wallet/deshield_warning
features/wallet/diversifier_narrowing
features/wallet/ephemeral_sweep
features/wallet/frb_wallet_session
features/wallet/hide_balance
features/wallet/in_flight_sends_section
features/wallet/in_flight_swaps_section
features/wallet/labeled_zat_row
features/wallet/move_to_transparent/move_public_after
features/wallet/move_to_transparent/move_to_transparent_controller
features/wallet/move_to_transparent/move_to_transparent_sheet
features/wallet/move_to_transparent/move_to_transparent_state
features/wallet/onboarding/bip39_wordlist
features/wallet/onboarding/frb_wallet_provisioner
features/wallet/onboarding/mnemonic_input
Restore-input normalization — the ONE place the host honours the load-bearing BIP39 host-UI contract (spec §3.6; wallet-sdk §3.3). The SDK keeps the audited bip39 parser WHOLE and does NOT case-fold: Mnemonic::parse_in_normalized rejects an autocapitalized or space-padded word as InvalidMnemonic carrying its INDEX — so a CORRECT backup typed with a soft-keyboard's autocapitalization (or pasted with stray whitespace) would fail to restore unless the host normalizes first. That normalization is a money-reliability requirement, not a nicety; it lives here, once (DRY), and BOTH the restore screen (live word count) and the controller (the authoritative pre-restore pass — the chokepoint that guarantees the contract regardless of caller) call it.
features/wallet/onboarding/mnemonic_pill_field
features/wallet/onboarding/mnemonic_reveal
features/wallet/onboarding/onboarding_controller
features/wallet/onboarding/onboarding_providers
features/wallet/onboarding/onboarding_state
features/wallet/onboarding/onboarding_store
features/wallet/onboarding/onboarding_views
features/wallet/onboarding/screen_security_channel
features/wallet/onboarding/shared_prefs_onboarding_store
features/wallet/onboarding/ufvk_export
features/wallet/onboarding/wallet_provisioner
features/wallet/onboarding/wallet_startup_failed_screen
features/wallet/parked_sends_section
features/wallet/receive_screen
features/wallet/reclaim
features/wallet/reconnect_kick
features/wallet/recover_ephemeral_action
features/wallet/recoverable_ephemeral
features/wallet/recovery_phrase_widgets
features/wallet/rescan_sheet
features/wallet/reveal_authorization
features/wallet/reveal_grant
features/wallet/send/form_fault_view
features/wallet/send/large_send_confirm
features/wallet/send/send_controller
features/wallet/send/send_screen
features/wallet/send/send_state
features/wallet/send/wallet_send_entry
features/wallet/send/wallet_send_report
features/wallet/send/wallet_send_request
features/wallet/send/zec_amount
Parse a user-typed ZEC amount string into integer zatoshis — INTEGER MATH ONLY, the strict inverse of formatZec (zat_format.dart). A send amount is money, so it NEVER routes through double: IEEE-754 cannot represent most decimal ZEC values exactly, and a float path would silently send the wrong amount. This is the host-side first gate before a PaymentDraft is composed; the SDK re-validates every figure, but parsing here gives an honest inline message before any bridge call.
features/wallet/send_authorization
The host send-authorization seam (#327 — security review F1).
features/wallet/shield/shield_controller
features/wallet/shield/shield_sheet
features/wallet/shield/shield_state
features/wallet/started_spend
The started-spend bookkeeping every money controller shares (R13 §4.2): whether the spend closure ever reached the SDK call, what that call returned, and whether the error that came back is the SDK's own. One copy, used by send's confirm and queue and by the shield and move confirms.
features/wallet/swap/swap_activation
features/wallet/swap/swap_address_scanner
features/wallet/swap/swap_assets
features/wallet/swap/swap_chains
Human-readable chain names for the provider's short chain codes (eth, btc, …). The 1Click token list speaks codes; a non-crypto user reads "Ethereum", not "ETH", and "BTC on Bitcoin" not the nonsensical "BTC on BTC". This matches the convention the curated OutOfZec list already uses ("USDC on Ethereum").
features/wallet/swap/swap_config
features/wallet/swap/swap_controller
features/wallet/swap/swap_deposit_screen
features/wallet/swap/swap_enabled_provider
features/wallet/swap/swap_scanned_address
Pure, chain-agnostic normalization of a RAW scanned QR payload into a bare foreign-chain address (wallet spec §3.3b D6/L8). Shared by the IntoZec refund-address scan and the OutOfZec destination-address scan — both unwrap the same standard wallet-QR envelopes. No Flutter, no dart:io, no camera imports — unit-testable in isolation.
features/wallet/swap/swap_screen
features/wallet/swap/swap_slippage
features/wallet/swap/swap_state
features/wallet/swap/swap_status_provider
features/wallet/swap/swap_token_icon
features/wallet/swap/swap_token_picker
features/wallet/swap/swap_tokens_provider
features/wallet/swap_deep_scan
features/wallet/sync_server_sheet
features/wallet/sync_status_presentation
features/wallet/sync_status_sheet
features/wallet/transparent_funds/auto_shield_controller
features/wallet/transparent_funds/transparent_funds_providers
The transparent-funds policy seams (§3.2i-3 / #328) — the expert gate, the auto-shield switch, and the two host-wired policy inputs (threshold, power-save). All host-overridable at the ProviderScope root, following the wallet_providers.dart seam conventions.
features/wallet/transparent_funds/transparent_funds_sheet
features/wallet/transparent_funds/wallet_settings_store
Persists the transparent-funds POLICY toggles (§3.2i-3) across launches: the expert gate ("Advanced: transparent funds") and the auto-shield switch.
features/wallet/tx_detail_sheet
features/wallet/wallet_activity_controller
features/wallet/wallet_coin
features/wallet/wallet_composition
features/wallet/wallet_config
features/wallet/wallet_display_sync_status
features/wallet/wallet_health
features/wallet/wallet_providers
features/wallet/wallet_rescan_controller
features/wallet/wallet_screen
features/wallet/wallet_session
features/wallet/wallet_sync_controller
features/wallet/wallet_tip_follow
features/wallet/wallet_ui_config
features/wallet/zat_format
Money formatting for ZEC amounts — INTEGER MATH ONLY.
l10n/wallet_localizations
l10n/wallet_localizations_ar
l10n/wallet_localizations_de
l10n/wallet_localizations_en
l10n/wallet_localizations_es
l10n/wallet_localizations_fallback
l10n/wallet_localizations_fi
l10n/wallet_localizations_fr
l10n/wallet_localizations_he
l10n/wallet_localizations_it
l10n/wallet_localizations_ja
l10n/wallet_localizations_nb
l10n/wallet_localizations_nl
l10n/wallet_localizations_pl
l10n/wallet_localizations_pt
l10n/wallet_localizations_ru
l10n/wallet_localizations_uk
l10n/wallet_localizations_zh
shared/action_row_layout
ONE source of truth for "the trailing element can no longer share this row" (duplicates are bugs). Four sites carried their own copy of this rule before #409 R3 — two of them written as scale(100) > 140, which probes the WRONG font size (see walletTextScaleForcesStack).
shared/address_text
shared/decimal_input_formatter
shared/qr_tile
shared/settings_section_header
shared/sheet_states
The two phases every money sheet shares (stage S11 C7): the spinner while it proposes or submits, and the outcome it ends on. The shield and move-to-transparent sheets each carried an identical private copy.
shared/theme/avatar_color
Deterministic avatar colour derived from a seed string. Single source of truth so every surface renders the same contact with the same colour.
shared/wallet_ack_checkbox
shared/wallet_cta
shared/wallet_dialog
The wallet's one confirm dialog (DESIGN.md §6.9, stage S11 C3). Every wallet dialog goes through showWalletConfirm; a source test keeps showDialog(/AlertDialog( out of the rest of the package.
shared/wallet_group
shared/wallet_info_button
shared/wallet_loading
A spinner with nothing beside it to say what it is (S13 §1.7).
shared/wallet_notice
shared/wallet_outcome_unknown
The honest "we lost the answer" terminal every money flow shares (S7 U1, R13 §4.3): the spend ran and something after it threw or declined, so the flow can say neither "done" nor "nothing happened". It points at where the truth is and offers Close only — a Try again here is how one spend gets made twice.
shared/wallet_sheet
The wallet's one bottom-sheet entry (DESIGN.md §6.8, stage S11 C4). Every wallet sheet opens through showWalletSheet; a source test keeps showModalBottomSheet( out of the rest of the package.
testing
Test-support library for hosts embedding the wallet UI: the fakes needed to pump wallet surfaces in widget tests without the native library — FakeWalletSession (a scripted WalletSession) and the fake onboarding seams (provisioner / store / screen-security). DELIBERATELY free of any flutter_test import, so depending on it never constrains a consumer's test-framework solve; import it from test code only.
testing/fake_onboarding
testing/fake_reveal_authorizer
testing/fake_send_authorizer
testing/fake_wallet_session
testing/fake_wallet_settings_store
zec_wallet_ui
zec_wallet_ui — the reusable wallet UI layer over the zec_wallet SDK.