Overview
The domain layer (io.muun.apollo.domain) is the heart of Muun Wallet. It contains all business logic, domain models, and use cases. This layer is framework-independent and focuses purely on business rules.
The domain layer depends on the data layer for data access but has no knowledge of Android framework classes or UI components.
Layer Structure
domain/
├── action/ # Use cases (business operations)
│ ├── base/ # Base action classes
│ ├── address/ # Address generation actions
│ ├── challenge_keys/ # Authentication actions
│ ├── operation/ # Transaction actions
│ ├── ContactActions # Contact management
│ ├── SigninActions # Sign-in operations
│ └── UserActions # User management
├── model/ # Domain models
│ ├── user/ # User entity models
│ ├── operation/ # Transaction models
│ └── *.kt # Other domain entities
├── selector/ # Data selectors/queries
├── errors/ # Domain-specific exceptions
├── libwallet/ # Bitcoin wallet operations
├── analytics/ # Analytics tracking
├── sync/ # Data synchronization
└── utils/ # Domain utilities
Actions: The Use Case Pattern
Actions are Muun’s implementation of the Use Case pattern from clean architecture. Each action represents a single business operation.
Action Hierarchy
BaseAsyncAction
├── BaseAsyncAction0 (no parameters)
├── BaseAsyncAction1 (1 parameter)
├── BaseAsyncAction2 (2 parameters)
├── BaseAsyncAction3 (3 parameters)
└── BaseAsyncAction4 (4 parameters)
BaseAsyncAction1 Example
public abstract class BaseAsyncAction1 < T , R > extends BaseAsyncAction < R > {
public abstract Observable < R > action ( T t );
public void run ( T t ) {
super . run ( action (t));
}
public R actionNow ( T t ) {
return action (t). toBlocking (). first ();
}
}
Example from: domain/action/base/BaseAsyncAction1.java:5
Action Naming Convention : Actions are grouped by feature (e.g., SigninActions, OperationActions) and contain related business operations.
Synchronous Actions
Simple actions that don’t require async operations:
@ Singleton
public class SigninActions {
private final AuthRepository authRepository ;
@ Inject
public SigninActions ( AuthRepository authRepository ) {
this . authRepository = authRepository;
}
public Optional < SessionStatus > getSessionStatus () {
return authRepository . getSessionStatus ();
}
public void clearSession () {
authRepository . clearSession ();
}
public void reportAuthorizedByEmail () {
authRepository . storeSessionStatus ( SessionStatus . AUTHORIZED_BY_EMAIL );
}
}
Example from: domain/action/SigninActions.java:12
Asynchronous Actions
Complex operations that return RxJava Observables:
public class CreateAddressAction extends BaseAsyncAction1 < Void , String > {
private final AddressRepository addressRepository ;
private final UserRepository userRepository ;
@ Inject
public CreateAddressAction ( AddressRepository addressRepository ,
UserRepository userRepository ) {
this . addressRepository = addressRepository;
this . userRepository = userRepository;
}
@ Override
public Observable < String > action ( Void params ) {
return userRepository . getUser ()
. flatMap (user -> addressRepository . generateNewAddress (user))
. doOnNext (address -> addressRepository . save (address));
}
}
Action State Management
Actions can maintain state during execution:
public class ActionState < T > {
public enum Kind {
LOADING ,
VALUE ,
ERROR ,
EMPTY
}
private final Kind kind ;
private final T value ;
private final Throwable error ;
}
Example from: domain/action/base/ActionState.java
Action state allows UI to react to loading, success, error, and empty states consistently across all operations.
Domain Models
Domain models represent core business entities. They are pure Java/Kotlin classes with no Android dependencies.
User Model
public class User {
@ NotNull
public final Long hid ;
@ NotNull
public Optional < String > email ;
public boolean isEmailVerified ;
public final Optional < UserPhoneNumber > phoneNumber ;
public final Optional < UserProfile > profile ;
private final CurrencyUnit primaryCurrency ;
public boolean hasRecoveryCode ;
public boolean hasPassword ;
public final boolean hasP2PEnabled ;
public Optional < EmergencyKit > emergencyKit ;
@ NotNull
public final SortedSet < Integer > emergencyKitVersions ;
@ Since ( apolloVersion = 46 )
public final Optional < ZonedDateTime > createdAt ;
// Factory method for Houston data
public static User fromHouston (@ NotNull Long hid ,
Optional < String > email ,
boolean isEmailVerified ,
// ... more parameters
) {
return new User (...);
}
}
Example from: domain/model/user/User.java:18
Operation Model
Represents a Bitcoin transaction:
public class Operation {
public final Long id ;
public final String txId ;
public final MonetaryAmount amount ;
public final MonetaryAmount fee ;
public final String address ;
public final Direction direction ;
public final OperationStatus status ;
public final ZonedDateTime createdAt ;
public enum Direction {
INCOMING ,
OUTGOING ,
CYCLICAL
}
public enum OperationStatus {
CREATED ,
SIGNING ,
SIGNED ,
BROADCASTED ,
CONFIRMED ,
DROPPED ,
FAILED
}
}
Represents a wallet contact:
public class Contact {
public final Long id ;
public final PublicProfile publicProfile ;
public final Optional < PhoneNumber > phoneNumber ;
public final Optional < String > email ;
public final int maxAddressVersion ;
public final PublicKey publicKey ;
public final Optional < ZonedDateTime > lastActivityAt ;
}
Immutability : Most domain models use final fields and immutable patterns to prevent accidental state changes and make code more predictable.
Business Logic Managers
Managers handle complex cross-cutting concerns:
ApplicationLockManager
Manages app lock state and PIN/biometric authentication:
public class ApplicationLockManager {
private final UserRepository userRepository ;
private final PreferencesRepository preferencesRepository ;
public Observable < Boolean > isLockConfigured () {
return userRepository . getUser ()
. map (user -> user . hasPassword || user . hasRecoveryCode );
}
public void setLockTimeout ( int minutes ) {
preferencesRepository . setLockTimeout (minutes);
}
public boolean shouldLock () {
long lastActive = preferencesRepository . getLastActiveTime ();
long timeout = preferencesRepository . getLockTimeout ();
return System . currentTimeMillis () - lastActive > timeout;
}
}
Example from: domain/ApplicationLockManager.java
SignupDraftManager
Manages the multi-step signup process:
public class SignupDraftManager {
private final PreferencesRepository preferencesRepository ;
public void saveEmail ( String email ) {
SignupDraft draft = getDraft ();
draft . email = email;
saveDraft (draft);
}
public void savePassword ( String password ) {
SignupDraft draft = getDraft ();
draft . hasPassword = true ;
saveDraft (draft);
}
public boolean isSignupComplete () {
SignupDraft draft = getDraft ();
return draft . email != null
&& draft . hasPassword
&& draft . hasRecoveryCode ;
}
}
Example from: domain/SignupDraftManager.kt
NotificationProcessor
Processes push notifications and triggers appropriate actions:
public class NotificationProcessor {
private final OperationActions operationActions ;
private final ContactActions contactActions ;
private final UserActions userActions ;
public Completable process ( Notification notification ) {
switch ( notification . type ) {
case NEW_OPERATION :
return operationActions . syncOperations ();
case NEW_CONTACT :
return contactActions . syncContacts ();
case USER_UPDATED :
return userActions . syncUser ();
default:
return Completable . complete ();
}
}
}
Example from: domain/NotificationProcessor.java
Selectors: Reactive Data Queries
Selectors provide reactive access to data with business logic applied:
public class OperationSelector {
private final OperationRepository repository ;
/**
* Watch all operations, sorted by date
*/
public Observable < List < Operation >> watchAll () {
return repository . watchOperations ()
. map (operations -> {
Collections . sort (operations,
(a, b) -> b . createdAt . compareTo ( a . createdAt ));
return operations;
});
}
/**
* Watch pending operations only
*/
public Observable < List < Operation >> watchPending () {
return watchAll ()
. map (operations -> operations . stream ()
. filter (op -> op . status == OperationStatus . CREATED
|| op . status == OperationStatus . BROADCASTED )
. collect ( Collectors . toList ()));
}
/**
* Calculate total balance
*/
public Observable < MonetaryAmount > watchBalance () {
return watchAll ()
. map (operations -> {
MonetaryAmount total = MonetaryAmount . ZERO ;
for ( Operation op : operations) {
if ( op . status == OperationStatus . CONFIRMED ) {
total = op . direction == Direction . INCOMING
? total . add ( op . amount )
: total . subtract ( op . amount );
}
}
return total;
});
}
}
Selectors encapsulate business rules for data queries, keeping repositories simple and focused on data access.
Error Handling
The errors package defines domain-specific exceptions:
errors/
├── BugDetected # Unexpected state errors
├── SecureStorageError # Key storage failures
├── UserFacingError # Errors to show users
├── newop/ # Operation creation errors
│ ├── InvalidInvoiceException
│ ├── InvoiceExpiredException
│ └── InsufficientFundsException
└── delete_wallet/ # Wallet deletion errors
├── NonEmptyWalletDeleteException
└── UnsettledOperationsWalletDeleteException
Error Hierarchy
public abstract class MuunError extends RuntimeException {
// Base error class
}
public class UserFacingError extends MuunError {
// Errors that should be shown to users
public String getUserFacingMessage ();
}
public class BugDetected extends MuunError {
// Programming errors that shouldn't happen
}
Error Handling Best Practice : Always use domain-specific exceptions. Generic exceptions make it hard to understand what went wrong and how to handle it.
LibWallet Integration
The libwallet package integrates the Bitcoin wallet library (written in Go, compiled to native code):
libwallet/
├── LibwalletBridge # JNI bridge to native code
├── AddressHelper # Address generation
├── TransactionHelper # Transaction building
├── InvoiceHelper # Lightning invoice parsing
└── KeyCrypter # Key encryption/decryption
Address Generation
public class AddressHelper {
/**
* Generate a new Bitcoin address
*/
public static String generateAddress (
PublicKey userKey ,
PublicKey muunKey ,
int derivationIndex
) {
// Calls native libwallet code
return Libwallet . createAddress (
userKey . serializeBase58 (),
muunKey . serializeBase58 (),
derivationIndex
);
}
}
Transaction Creation
public class TransactionHelper {
/**
* Build a Bitcoin transaction
*/
public static Transaction buildTransaction (
List < TransactionInput > inputs ,
List < TransactionOutput > outputs ,
long feeRate
) {
// Native transaction building
return Libwallet . buildTransaction (
serializeInputs (inputs),
serializeOutputs (outputs),
feeRate
);
}
}
Security Critical : Most cryptographic operations happen in the native libwallet library. This code requires careful security audits.
Business Logic Examples
Creating a New Operation
public class CreateOperationAction extends BaseAsyncAction2 < Address , MonetaryAmount , Operation > {
private final OperationRepository operationRepository ;
private final FeeRepository feeRepository ;
private final TransactionHelper transactionHelper ;
@ Override
public Observable < Operation > action ( Address address , MonetaryAmount amount ) {
return Observable . zip (
operationRepository . getAvailableInputs (),
feeRepository . getCurrentFeeRate (),
(inputs, feeRate) -> {
// Business logic: validate amount
MonetaryAmount total = calculateTotal (inputs);
if ( amount . isGreaterThan (total)) {
throw new InsufficientFundsException ();
}
// Build transaction draft
Transaction tx = transactionHelper . buildTransaction (
inputs,
createOutputs (address, amount),
feeRate
);
// Create operation
Operation operation = new Operation (
null , // No ID yet
tx . getTxId (),
amount,
tx . getFee (),
address . address ,
Direction . OUTGOING ,
OperationStatus . CREATED ,
ZonedDateTime . now ()
);
return operation;
}
)
. flatMap (operation ->
// Save to repository
operationRepository . save (operation)
);
}
}
User Authentication Flow
public class AuthenticationFlow {
private final SigninActions signinActions ;
private final ChallengeActions challengeActions ;
private final UserRepository userRepository ;
public Observable < User > authenticate ( String email , String password ) {
return challengeActions . requestChallenge ( ChallengeType . PASSWORD )
. flatMap (challenge -> {
// Sign challenge with password
ChallengeSignature signature =
signChallenge (challenge, password);
// Submit signature
return challengeActions . submitSignature (signature);
})
. flatMap (keySet -> {
// Store authentication keys
signinActions . storeKeySet (keySet);
// Fetch user data
return userRepository . fetchUser ();
});
}
}
Lightning Invoice Processing
public class ProcessInvoiceAction extends BaseAsyncAction1 < String , SubmarineSwap > {
private final InvoiceHelper invoiceHelper ;
private final SubmarineSwapRepository swapRepository ;
private final BalanceRepository balanceRepository ;
@ Override
public Observable < SubmarineSwap > action ( String invoiceString ) {
// Parse invoice
Invoice invoice = invoiceHelper . parseInvoice (invoiceString);
return Observable . just (invoice)
. flatMap (inv -> {
// Validate invoice
if ( inv . isExpired ()) {
throw new InvoiceExpiredException (inv);
}
if ( ! inv . hasAmount ()) {
throw new InvoiceMissingAmountException (inv);
}
// Check balance
return balanceRepository . getBalance ()
. map (balance -> {
if ( inv . amount . isGreaterThan (balance)) {
throw new InsufficientFundsException ();
}
return inv;
});
})
. flatMap (inv ->
// Create submarine swap
swapRepository . createSwap (inv)
);
}
}
Testing Domain Layer
Unit Testing Actions
public class SigninActionsTest {
@ Mock
private AuthRepository authRepository ;
private SigninActions signinActions ;
@ Before
public void setUp () {
MockitoAnnotations . initMocks ( this );
signinActions = new SigninActions (authRepository);
}
@ Test
public void getSessionStatus_returnsRepositoryValue () {
// Given
SessionStatus expected = SessionStatus . LOGGED_IN ;
when ( authRepository . getSessionStatus ())
. thenReturn ( Optional . of (expected));
// When
Optional < SessionStatus > result = signinActions . getSessionStatus ();
// Then
assertEquals ( Optional . of (expected), result);
}
}
Testing Business Logic
public class BalanceCalculationTest {
@ Test
public void calculateBalance_withMixedOperations () {
// Given
List < Operation > operations = Arrays . asList (
createOperation ( Direction . INCOMING , btc ( 1.0 ), CONFIRMED),
createOperation ( Direction . OUTGOING , btc ( 0.3 ), CONFIRMED),
createOperation ( Direction . INCOMING , btc ( 0.5 ), CONFIRMED)
);
// When
MonetaryAmount balance = calculateBalance (operations);
// Then
assertEquals ( btc ( 1.2 ), balance);
}
}
Best Practices
Domain Layer Guidelines
Pure Business Logic : No Android dependencies
Testable : All actions testable with mocked repositories
Immutable Models : Use final fields and immutable patterns
Reactive Streams : Use RxJava for async operations
Domain Errors : Use specific exception types
Single Responsibility : Each action does one thing
Security Considerations
Domain Layer Security Checklist
✅ All transaction signing logic reviewed
✅ Amount validations prevent overflow
✅ Authorization checks before sensitive operations
✅ Cryptographic operations use libwallet
✅ No secrets in logs or error messages
✅ Challenge-response authentication properly implemented
Common Patterns
Action Composition
public Observable < Result > complexOperation () {
return action1 . execute ()
. flatMap (result1 -> action2 . execute (result1))
. flatMap (result2 -> action3 . execute (result2));
}
Error Recovery
public Observable < Data > fetchWithFallback () {
return networkAction . execute ()
. onErrorResumeNext (error -> {
if (error instanceof NetworkError) {
return cacheAction . execute ();
}
return Observable . error (error);
});
}
Data Layer See how data layer provides repositories
Presentation Layer Learn how UI layer uses actions