feat(wallet): transaction builder, UTXO selection, and status poller (Task 4)

## What's new

### API module additions
- ProtocolParameters: data class for fee calculation params
- PaymentRequest: transaction request model
- SignedTransaction: signed transaction result model
- TransactionBuilder: interface for building/signing transactions
- PaymentStatusPoller: interface for polling tx confirmation

### CardanoClient updates
- Added getProtocolParameters() to interface
- Implemented in KoiosCardanoClient with retry logic

### Implementation
- DefaultTransactionBuilder: builds and signs transactions using cardano-client-lib
  - Largest-first UTXO selection
  - Fee calculation via protocol parameters
  - Min UTXO validation (1 ADA minimum)
  - Secure key handling (zeroed after use)
- DefaultPaymentStatusPoller: polls Koios for tx confirmation
  - 10s polling interval, 60 attempts max (~10 minutes)
  - Emits TxStatus.PENDING -> CONFIRMED/FAILED flow

### Test module
- FakeTransactionBuilder: configurable success/failure responses
- FakePaymentStatusPoller: configurable status sequences
- Updated FakeCardanoClient with getProtocolParameters()

### Unit tests
- TransactionBuilderTest: UTXO selection, fee calculation, error handling
- PaymentStatusPollerTest: polling behavior, error recovery
This commit is contained in:
Kayos 2026-03-27 10:52:15 -07:00
parent 19637833a6
commit 9439f5a227
15 changed files with 1298 additions and 0 deletions

View file

@ -44,4 +44,13 @@ interface CardanoClient {
* @return Current [TxStatus] of the transaction
*/
suspend fun getTxStatus(txHash: String): Result<TxStatus>
/**
* Get the current protocol parameters from the network.
*
* Protocol parameters are needed for fee calculation and transaction building.
*
* @return Current [ProtocolParameters] from the latest epoch
*/
suspend fun getProtocolParameters(): Result<ProtocolParameters>
}

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
import io.element.android.libraries.matrix.api.core.SessionId
/**
* A request to build and sign a Cardano payment transaction.
*
* @property fromAddress The sender's Cardano address (Bech32)
* @property toAddress The recipient's Cardano address (Bech32)
* @property amountLovelace The amount to send in lovelace (1 ADA = 1,000,000 lovelace)
* @property sessionId The Matrix session ID for key retrieval
*/
data class PaymentRequest(
val fromAddress: String,
val toAddress: String,
val amountLovelace: Long,
val sessionId: SessionId,
)

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
import kotlinx.coroutines.flow.Flow
/**
* Interface for polling transaction confirmation status.
*/
interface PaymentStatusPoller {
/**
* Polls for transaction confirmation status.
*
* Emits [TxStatus] changes as a Flow:
* - Initially PENDING
* - CONFIRMED when transaction is in a block
* - FAILED if confirmation times out or error occurs
*
* Polling behavior:
* - Poll every 10 seconds
* - Maximum 60 attempts (~10 minutes total)
* - Stops when status changes from PENDING
*
* @param txHash The transaction hash to poll
* @return Flow of [TxStatus] changes
*/
fun pollUntilConfirmed(txHash: String): Flow<TxStatus>
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
/**
* Cardano protocol parameters needed for transaction building and fee calculation.
*
* These parameters are set via governance and determine transaction costs
* and constraints on the network.
*
* @property minFeeA The linear fee coefficient (lovelace per byte)
* @property minFeeB The constant fee (base fee in lovelace)
* @property maxTxSize Maximum transaction size in bytes
* @property utxoCostPerByte Cost in lovelace per byte of UTXO storage (for min UTXO calculation)
*/
data class ProtocolParameters(
val minFeeA: Long,
val minFeeB: Long,
val maxTxSize: Int,
val utxoCostPerByte: Long,
)

View file

@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
/**
* A signed Cardano transaction ready for submission.
*
* @property txCbor The CBOR-encoded signed transaction as a hex string
* @property txHash The transaction hash (for tracking)
* @property fee The transaction fee in lovelace
* @property actualAmount The actual amount sent (may differ slightly from requested due to min UTXO rules)
*/
data class SignedTransaction(
val txCbor: String,
val txHash: String,
val fee: Long,
val actualAmount: Long,
)

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 Sulkta Coop.
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
package io.element.android.features.wallet.api
/**
* Interface for building and signing Cardano transactions.
*
* The implementation handles:
* - UTXO selection (largest-first coin selection)
* - Fee calculation based on protocol parameters
* - Change output calculation
* - Transaction signing with the spending key
*
* ## Error handling
* The following errors may be returned:
* - [CardanoException.InsufficientFundsException] - Not enough ADA in wallet
* - [CardanoException.InvalidAddressException] - Invalid address format
* - [CardanoException.ApiException] - Various API/build errors
*/
interface TransactionBuilder {
/**
* Builds and signs a payment transaction.
*
* This method will:
* 1. Fetch UTXOs for the sender address
* 2. Select UTXOs to cover amount + fee (largest-first)
* 3. Build the transaction with proper change output
* 4. Retrieve the spending key (triggers biometric prompt)
* 5. Sign the transaction
* 6. Return the signed transaction ready for submission
*
* @param request The payment request details
* @return [SignedTransaction] on success, error on failure
*/
suspend fun buildAndSign(request: PaymentRequest): Result<SignedTransaction>
}