Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0+4]

### Added
- Comprehensive relay URL validation with proper WebSocket protocol checking
- Real-time relay connectivity testing using direct WebSocket connections
- Loading indicators during relay validation and testing process
- Enhanced error messages for invalid relay URLs with helpful formatting hints
- Debug-only display mode for Current Trade Index Card (hidden in release builds)

### Fixed
- Relay connectivity testing now accurately detects non-existent or unreachable relays
- Invalid relay URLs (like "holahola" or "wss://xrelay.damus.io") now properly show as unhealthy
- Relay health status now reflects actual Nostr protocol compatibility
- False positive connectivity results for non-working relays eliminated
- Proper cleanup of WebSocket connections during relay testing

### Changed
- Replaced dart_nostr library-based testing with direct WebSocket implementation
- Improved relay validation logic with ws:// and wss:// protocol requirements
- Enhanced relay testing with real Nostr REQ/response message cycles
- Updated relay health checking to use actual connectivity verification
- Optimized relay testing timeouts for better user experience

### Security
- Current Trade Index Card now hidden in production builds for enhanced privacy
- Relay testing isolated from main app Nostr connections to prevent interference

## [1.0.0+3]

### Fixed
Expand Down
16 changes: 16 additions & 0 deletions lib/core/mostro_fsm.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,29 @@ class MostroFSM {
Status.waitingPayment: {
Role.seller: {
Action.payInvoice: Status.active,
Action.paymentFailed: Status.paymentFailed,
Action.cancel: Status.canceled,
},
Role.buyer: {
Action.paymentFailed: Status.paymentFailed,
Action.cancel: Status.canceled,
},
Role.admin: {},
},
// ───────────────────────── PAYMENT FAILED ────────────────────
Status.paymentFailed: {
Role.buyer: {
Action.addInvoice: Status.waitingPayment,
Action.cancel: Status.canceled,
Action.dispute: Status.dispute,
},
Role.seller: {
Action.payInvoice: Status.active,
Action.cancel: Status.canceled,
Action.dispute: Status.dispute,
},
Role.admin: {},
},
// ───────────────────────── ACTIVE ────────────────────────────
Status.active: {
Role.buyer: {
Expand Down
2 changes: 2 additions & 0 deletions lib/data/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export 'package:mostro_mobile/data/models/peer.dart';
export 'package:mostro_mobile/data/models/rating_user.dart';
export 'package:mostro_mobile/data/models/rating.dart';
export 'package:mostro_mobile/data/models/session.dart';
export 'package:mostro_mobile/data/models/payment_failed.dart';
export 'package:mostro_mobile/data/models/next_trade.dart';
1 change: 1 addition & 0 deletions lib/data/models/enums/status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ enum Status {
success('success'),
waitingBuyerInvoice('waiting-buyer-invoice'),
waitingPayment('waiting-payment'),
paymentFailed('payment-failed'),
cooperativelyCanceled('cooperatively-canceled'),
inProgress('in-progress');

Expand Down
26 changes: 26 additions & 0 deletions lib/data/models/next_trade.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:mostro_mobile/data/models/payload.dart';

class NextTrade implements Payload {
final String key;
final int index;

NextTrade({required this.key, required this.index});

@override
String get type => 'next_trade';

@override
Map<String, dynamic> toJson() {
return {
type: {'key': key, 'index': index},
};
}

factory NextTrade.fromJson(Map<String, dynamic> json) {
return NextTrade(
key: json['key'] as String,
index: json['index'] as int,
);
}

}
Comment on lines +1 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify model export through data/models.dart.

Based on the coding guidelines, models should be exported through data/models.dart. Please ensure this new NextTrade model is added to the exports in that file.


🏁 Script executed:

#!/bin/bash
# Check if NextTrade is exported through data/models.dart
echo "Checking if NextTrade model is exported through data/models.dart..."
rg "NextTrade" lib/data/models.dart || echo "NextTrade not found in models.dart exports"

# Also check if PaymentFailed is exported
echo "Checking if PaymentFailed model is exported through data/models.dart..."
rg "PaymentFailed" lib/data/models.dart || echo "PaymentFailed not found in models.dart exports"

Length of output: 571


Export the NextTrade model in data/models.dart

Please add the new NextTrade model to your central exports so it’s discoverable throughout the app. In lib/data/models.dart, include:

 export 'models/payload.dart';
+export 'models/next_trade.dart';
 // …other exports

This aligns with our guideline to re-export all models from data/models.dart.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import 'package:mostro_mobile/data/models/payload.dart';
class NextTrade implements Payload {
final String key;
final int index;
NextTrade({required this.key, required this.index});
@override
String get type => 'next_trade';
@override
Map<String, dynamic> toJson() {
return {
type: {'key': key, 'index': index},
};
}
factory NextTrade.fromJson(Map<String, dynamic> json) {
return NextTrade(
key: json['key'] as String,
index: json['index'] as int,
);
}
}
// lib/data/models.dart
export 'models/payload.dart';
export 'models/next_trade.dart';
// …other exports
🤖 Prompt for AI Agents
In lib/data/models/next_trade.dart lines 1 to 26, the NextTrade model is defined
but not exported centrally. To fix this, open lib/data/models.dart and add an
export statement for next_trade.dart, such as "export 'models/next_trade.dart';"
to ensure the NextTrade model is discoverable and accessible throughout the app
as per project guidelines.

6 changes: 6 additions & 0 deletions lib/data/models/payload.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:mostro_mobile/data/models/cant_do.dart';
import 'package:mostro_mobile/data/models/dispute.dart';
import 'package:mostro_mobile/data/models/next_trade.dart';
import 'package:mostro_mobile/data/models/order.dart';
import 'package:mostro_mobile/data/models/payment_failed.dart';
import 'package:mostro_mobile/data/models/payment_request.dart';
import 'package:mostro_mobile/data/models/peer.dart';
import 'package:mostro_mobile/data/models/rating_user.dart';
Expand All @@ -22,6 +24,10 @@ abstract class Payload {
return Dispute.fromJson(json);
} else if (json.containsKey('rating_user')) {
return RatingUser.fromJson(json['rating_user']);
} else if (json.containsKey('payment_failed')) {
return PaymentFailed.fromJson(json['payment_failed']);
} else if (json.containsKey('next_trade')) {
return NextTrade.fromJson(json['next_trade']);
} else {
throw UnsupportedError('Unknown payload type');
}
Expand Down
31 changes: 31 additions & 0 deletions lib/data/models/payment_failed.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:mostro_mobile/data/models/payload.dart';

class PaymentFailed implements Payload {
final int paymentAttempts;
final int paymentRetriesInterval;

PaymentFailed({
required this.paymentAttempts,
required this.paymentRetriesInterval,
});

factory PaymentFailed.fromJson(Map<String, dynamic> json) {
return PaymentFailed(
paymentAttempts: json['payment_attempts'] as int,
paymentRetriesInterval: json['payment_retries_interval'] as int,
);
}

@override
String get type => 'payment_failed';

@override
Map<String, dynamic> toJson() {
return {
type: {
'payment_attempts': paymentAttempts,
'payment_retries_interval': paymentRetriesInterval,
},
};
}
}
67 changes: 34 additions & 33 deletions lib/features/key_manager/key_management_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + MediaQuery.of(context).viewPadding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expand Down Expand Up @@ -643,43 +648,39 @@ class _KeyManagementScreenState extends ConsumerState<KeyManagementScreen> {
),
),
actions: [
Flexible(
child: TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(
S.of(context)!.cancel,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(
S.of(context)!.cancel,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
const SizedBox(width: 8),
Flexible(
child: ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
_generateNewMasterKey();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.activeColor,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
const SizedBox(width: 12),
ElevatedButton(
onPressed: () {
Navigator.of(dialogContext).pop();
_generateNewMasterKey();
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.activeColor,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Text(
S.of(context)!.continueButton,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
),
child: Text(
S.of(context)!.continueButton,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
Expand Down
47 changes: 45 additions & 2 deletions lib/features/order/models/order_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class OrderState {
final CantDo? cantDo;
final Dispute? dispute;
final Peer? peer;
final PaymentFailed? paymentFailed;
final _logger = Logger();

OrderState({
Expand All @@ -20,6 +21,7 @@ class OrderState {
this.cantDo,
this.dispute,
this.peer,
this.paymentFailed,
});

factory OrderState.fromMostroMessage(MostroMessage message) {
Expand All @@ -31,12 +33,13 @@ class OrderState {
cantDo: message.getPayload<CantDo>(),
dispute: message.getPayload<Dispute>(),
peer: message.getPayload<Peer>(),
paymentFailed: message.getPayload<PaymentFailed>(),
);
}

@override
String toString() =>
'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute, peer: $peer)';
'OrderState(status: $status, action: $action, order: $order, paymentRequest: $paymentRequest, cantDo: $cantDo, dispute: $dispute, peer: $peer, paymentFailed: $paymentFailed)';

@override
bool operator ==(Object other) =>
Expand All @@ -48,6 +51,7 @@ class OrderState {
other.paymentRequest == paymentRequest &&
other.cantDo == cantDo &&
other.dispute == dispute &&
other.paymentFailed == paymentFailed &&
other.peer == peer;

@override
Expand All @@ -59,6 +63,7 @@ class OrderState {
cantDo,
dispute,
peer,
paymentFailed,
);

OrderState copyWith({
Expand All @@ -69,6 +74,7 @@ class OrderState {
CantDo? cantDo,
Dispute? dispute,
Peer? peer,
PaymentFailed? paymentFailed,
}) {
return OrderState(
status: status ?? this.status,
Expand All @@ -78,6 +84,7 @@ class OrderState {
cantDo: cantDo ?? this.cantDo,
dispute: dispute ?? this.dispute,
peer: peer ?? this.peer,
paymentFailed: paymentFailed ?? this.paymentFailed,
);
}

Expand Down Expand Up @@ -135,6 +142,7 @@ class OrderState {
cantDo: message.getPayload<CantDo>() ?? cantDo,
dispute: message.getPayload<Dispute>() ?? dispute,
peer: newPeer,
paymentFailed: message.getPayload<PaymentFailed>() ?? paymentFailed,
);

return newState;
Expand All @@ -150,7 +158,14 @@ class OrderState {

// Actions that should set status to waiting-buyer-invoice
case Action.waitingBuyerInvoice:
return Status.waitingBuyerInvoice;

case Action.addInvoice:
// If current status is paymentFailed, maintain it for UI consistency
// Otherwise, transition to waitingBuyerInvoice for normal flow
if (status == Status.paymentFailed) {
return Status.paymentFailed;
}
return Status.waitingBuyerInvoice;

// ✅ FIX: Cuando alguien toma una orden, debe cambiar el status inmediatamente
Expand Down Expand Up @@ -209,9 +224,12 @@ class OrderState {
case Action.adminSettled:
return Status.settledByAdmin;

// Actions that should set status to payment failed
case Action.paymentFailed:
return Status.paymentFailed;

// Informational actions that should preserve current status
case Action.rateUser:
case Action.paymentFailed:
case Action.invoiceUpdated:
case Action.sendDm:
case Action.tradePubkey:
Expand Down Expand Up @@ -263,6 +281,12 @@ class OrderState {
Action.cancel,
],
},
Status.paymentFailed: {
Action.paymentFailed: [
// Only allow payment retry, no cancel or dispute during retrying
Action.payInvoice,
],
},
Status.active: {
Action.buyerTookOrder: [
Action.cancel,
Expand Down Expand Up @@ -350,6 +374,12 @@ class OrderState {
Action.release,
],
},
Status.settledHoldInvoice: {
Action.addInvoice: [
Action.addInvoice,
Action.cancel,
],
},
},
Role.buyer: {
Status.pending: {
Expand Down Expand Up @@ -377,6 +407,13 @@ class OrderState {
Action.cancel,
],
},
Status.paymentFailed: {
Action.addInvoice: [
// Only allow add invoice, no cancel or dispute during retrying
Action.addInvoice,
],
Action.paymentFailed: [],
},
Status.active: {
Action.holdInvoicePaymentAccepted: [
Action.fiatSent,
Expand Down Expand Up @@ -459,6 +496,12 @@ class OrderState {
Action.cancel,
],
},
Status.settledHoldInvoice: {
Action.addInvoice: [
Action.addInvoice,
Action.cancel,
],
},
},
};
}
Loading