The Transaction Controller is a centralized service that orchestrates the complete transaction flow for SwiftRemit. It provides a robust, fault-tolerant system for processing remittances with built-in validation, KYC checks, rollback handling, and retry logic.
┌─────────────────────────────────────────────────────────────┐
│ Transaction Controller │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Validate User Eligibility │
│ ├─ Check blacklist status │
│ └─ Verify user permissions │
│ │
│ 2. Confirm KYC Approval │
│ ├─ Check KYC approval status │
│ └─ Verify KYC expiry │
│ │
│ 3. Call Soroban Contract │
│ ├─ Validate amount and agent │
│ ├─ Transfer tokens to escrow │
│ └─ Create remittance record │
│ │
│ 4. Initiate Anchor Operation │
│ ├─ Generate anchor transaction ID │
│ └─ Store anchor mapping │
│ │
│ 5. Store Transaction Record │
│ └─ Save audit trail │
│ │
│ ✓ Transaction Complete │
│ │
│ [On Failure: Automatic Rollback] │
│ ├─ Cancel anchor operation │
│ ├─ Refund tokens │
│ └─ Update transaction state │
│ │
└─────────────────────────────────────────────────────────────┘
- Blacklist checking
- Permission verification
- Balance validation (via token contract)
- KYC status verification
- Expiry date checking
- Compliance enforcement
- Remittance creation
- Token escrow management
- Fee calculation and collection
- Withdrawal/deposit initiation
- Transaction ID generation
- Anchor mapping management
- Complete audit trail
- State tracking
- Retry count monitoring
- Automatic rollback on failure
- Partial failure recovery
- Transaction state management
- Configurable retry attempts (default: 3)
- Retry delay (default: 5 seconds)
- Transient error detection
- Non-retryable error handling
Execute a complete transaction with all validations and checks.
pub fn execute_transaction(
env: Env,
user: Address,
agent: Address,
amount: i128,
expiry: Option<u64>,
) -> Result<TransactionRecord, ContractError>Parameters:
user: User initiating the transactionagent: Agent receiving the payoutamount: Transaction amount in USDCexpiry: Optional expiry timestamp
Returns: TransactionRecord with complete transaction details
Errors:
UserBlacklisted(14) - User is blacklistedKycNotApproved(15) - User KYC not approvedKycExpired(16) - User KYC has expiredInvalidAmount(3) - Amount is zero or negativeAgentNotRegistered(5) - Agent not registeredOverflow(8) - Arithmetic overflow
Example:
let record = contract.execute_transaction(
&user,
&agent,
&1000,
&Some(expiry_time)
)?;Get the current status and details of a transaction.
pub fn get_transaction_status(
env: Env,
remittance_id: u64,
) -> Result<TransactionRecord, ContractError>Parameters:
remittance_id: ID of the remittance to query
Returns: TransactionRecord with current state
Errors:
TransactionNotFound(17) - Transaction record not found
Example:
let status = contract.get_transaction_status(&remittance_id)?;Retry a failed transaction.
pub fn retry_transaction(
env: Env,
remittance_id: u64,
) -> Result<TransactionRecord, ContractError>Parameters:
remittance_id: ID of the failed transaction
Returns: TransactionRecord with updated state
Errors:
InvalidStatus(7) - Transaction not in failed stateTransactionNotFound(17) - Transaction record not found
Example:
let record = contract.retry_transaction(&remittance_id)?;Set user blacklist status (admin only).
pub fn set_user_blacklisted(
env: Env,
user: Address,
blacklisted: bool
) -> Result<(), ContractError>Parameters:
user: User addressblacklisted: Blacklist status
Example:
contract.set_user_blacklisted(&user, &true)?;Check if user is blacklisted.
pub fn is_user_blacklisted(env: Env, user: Address) -> boolExample:
if contract.is_user_blacklisted(&user) {
// User is blacklisted
}Set user KYC approval status (admin only).
pub fn set_kyc_approved(
env: Env,
user: Address,
approved: bool,
expiry: u64
) -> Result<(), ContractError>Parameters:
user: User addressapproved: KYC approval statusexpiry: KYC expiry timestamp
Example:
let expiry = env.ledger().timestamp() + (365 * 24 * 60 * 60); // 1 year
contract.set_kyc_approved(&user, &true, &expiry)?;Check if user KYC is approved and not expired.
pub fn is_kyc_approved(env: Env, user: Address) -> boolExample:
if contract.is_kyc_approved(&user) {
// User KYC is valid
}Initial
↓
EligibilityValidated
↓
KycConfirmed
↓
ContractCalled { remittance_id }
↓
AnchorInitiated { anchor_tx_id }
↓
RecordStored
↓
Completed
[On Failure] → RolledBack
| State | Description |
|---|---|
Initial |
Transaction created, no operations performed |
EligibilityValidated |
User eligibility checks passed |
KycConfirmed |
KYC verification completed |
ContractCalled |
Soroban contract called, remittance created |
AnchorInitiated |
Anchor operation initiated |
RecordStored |
Transaction record saved |
Completed |
Transaction successfully completed |
RolledBack |
Transaction failed and rolled back |
pub struct TransactionRecord {
pub user: Address,
pub agent: Address,
pub amount: i128,
pub remittance_id: Option<u64>,
pub anchor_tx_id: Option<u64>,
pub state: TransactionState,
pub retry_count: u32,
pub timestamp: u64,
}The transaction controller automatically rolls back on failure:
-
AnchorInitiated/RecordStored State
- Cancel anchor operation
- Refund tokens to user
- Update remittance status to Cancelled
-
ContractCalled State
- Refund tokens to user
- Update remittance status to Cancelled
-
Earlier States
- No rollback needed (no state changes made)
Retryable Errors:
Overflow- Arithmetic overflow (transient)NotInitialized- Initialization issue (transient)
Non-Retryable Errors:
UserBlacklisted- User is blacklistedKycNotApproved- KYC not approvedKycExpired- KYC expiredInvalidAmount- Invalid amountAgentNotRegistered- Agent not registered
Retry Configuration:
- Maximum attempts: 3
- Retry delay: 5 seconds
- Exponential backoff: Not implemented (fixed delay)
// 1. Admin sets up user KYC
let expiry = env.ledger().timestamp() + (365 * 24 * 60 * 60);
contract.set_kyc_approved(&user, &true, &expiry)?;
// 2. Execute transaction
let record = contract.execute_transaction(
&user,
&agent,
&1000,
&None
)?;
// 3. Check transaction status
let status = contract.get_transaction_status(&record.remittance_id.unwrap())?;
assert_eq!(status.state, TransactionState::Completed);// Execute transaction (may fail)
match contract.execute_transaction(&user, &agent, &1000, &None) {
Ok(record) => {
// Transaction successful
log!("Transaction completed: {:?}", record.remittance_id);
}
Err(e) => {
// Transaction failed and rolled back
log!("Transaction failed: {:?}", e);
// Optionally retry if appropriate
if is_retryable(&e) {
let record = contract.retry_transaction(&remittance_id)?;
}
}
}// Blacklist a user
contract.set_user_blacklisted(&suspicious_user, &true)?;
// Check blacklist status
if contract.is_user_blacklisted(&user) {
return Err(ContractError::UserBlacklisted);
}
// Remove from blacklist
contract.set_user_blacklisted(&user, &false)?;-
Admin-Only Functions
set_user_blacklistedrequires admin authenticationset_kyc_approvedrequires admin authentication
-
KYC Enforcement
- All transactions require valid KYC
- KYC expiry is automatically checked
- Expired KYC blocks transactions
-
Blacklist Enforcement
- Blacklisted users cannot initiate transactions
- Checked before any state changes
-
Atomic Operations
- Token transfers are atomic
- Rollback ensures consistency
- No partial state on failure
-
Audit Trail
- All transactions are recorded
- State transitions are tracked
- Retry attempts are logged
// Execute transaction
const result = await contract.execute_transaction({
user: userAddress,
agent: agentAddress,
amount: 1000n,
expiry: null
});
// Check status
const status = await contract.get_transaction_status({
remittance_id: result.remittance_id
});
// Retry if needed
if (status.state === 'RolledBack') {
const retryResult = await contract.retry_transaction({
remittance_id: result.remittance_id
});
}// Set up user
let kyc_expiry = current_time + ONE_YEAR;
contract.set_kyc_approved(&user, &true, &kyc_expiry)?;
// Process transaction
let record = contract.execute_transaction(
&user,
&agent,
&amount,
&expiry
)?;
// Store in database
database.save_transaction(&record)?;
// Monitor status
let status = contract.get_transaction_status(&record.remittance_id.unwrap())?;-
Storage Efficiency
- Transaction records use persistent storage
- Anchor mappings are indexed for fast lookup
- KYC data is cached per user
-
Gas Optimization
- Validation checks are ordered by cost (cheapest first)
- Early returns on validation failures
- Minimal storage writes
-
Retry Strategy
- Fixed retry count prevents infinite loops
- Delay between retries reduces load
- Non-retryable errors fail fast
-
Transaction Success Rate
- Completed vs Failed transactions
- Rollback frequency
-
Retry Statistics
- Average retry count
- Retry success rate
-
Validation Failures
- Blacklist rejections
- KYC failures
- Eligibility issues
-
State Distribution
- Transactions per state
- Average time in each state
The transaction controller leverages existing events:
remittance_created- When contract is calledremittance_cancelled- On rollbackremittance_completed- On successful completion
Issue: Transaction fails with KycNotApproved
- Solution: Admin must approve user KYC first
Issue: Transaction fails with UserBlacklisted
- Solution: Admin must remove user from blacklist
Issue: Transaction fails with KycExpired
- Solution: Admin must renew user KYC with new expiry
Issue: Retry fails with InvalidStatus
- Solution: Only rolled-back transactions can be retried
-
Async Anchor Integration
- Real anchor API integration
- Webhook support for status updates
-
Advanced Retry Logic
- Exponential backoff
- Configurable retry policies
- Circuit breaker pattern
-
Enhanced Monitoring
- Detailed event emissions
- Performance metrics
- Health checks
-
Batch Processing
- Multiple transaction execution
- Bulk KYC updates
- Batch blacklist management
Version: 1.0.0
Last Updated: 2026-02-20
Status: Production Ready