Skip to content
Closed
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
9 changes: 9 additions & 0 deletions lib/core/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:mostro_mobile/shared/providers/app_init_provider.dart';
import 'package:mostro_mobile/features/settings/settings_provider.dart';
import 'package:mostro_mobile/shared/notifiers/locale_notifier.dart';
import 'package:mostro_mobile/features/walkthrough/providers/first_run_provider.dart';
import 'package:mostro_mobile/features/restore/restore_overlay.dart';

class MostroApp extends ConsumerStatefulWidget {
const MostroApp({super.key});
Expand Down Expand Up @@ -163,6 +164,14 @@ class _MostroAppState extends ConsumerState<MostroApp> {
theme: AppTheme.theme,
darkTheme: AppTheme.theme,
routerConfig: _router!,
builder: (context, child) {
return Stack(
children: [
child ?? const SizedBox.shrink(),
const RestoreOverlay(),
],
);
},
// Use language override from settings if available, otherwise let callback handle detection
locale: settings.selectedLanguage != null
? Locale(settings.selectedLanguage!)
Expand Down
1 change: 1 addition & 0 deletions lib/data/models/enums/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ enum Action {
paymentFailed('payment-failed'),
invoiceUpdated('invoice-updated'),
sendDm('send-dm'),
restoreSession('restore-session'),
tradePubkey('trade-pubkey');

final String value;
Expand Down
45 changes: 45 additions & 0 deletions lib/data/models/last_trade_index_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'dart:convert';

class LastTradeIndexRequest {
final int version;
final String action;

LastTradeIndexRequest({
this.version = 1,
this.action = 'last-trade-index',
});

String toJsonString() {
return jsonEncode([
{
'restore': {
'version': version,
'action': action,
'payload': null,
}
},
null
]);
}
}

class LastTradeIndexResponse {
final int version;
final String action;
final int tradeIndex;

LastTradeIndexResponse({
required this.version,
required this.action,
required this.tradeIndex,
});

factory LastTradeIndexResponse.fromJson(Map<String, dynamic> json) {
final restore = json['restore'] as Map<String, dynamic>;
return LastTradeIndexResponse(
version: restore['version'] as int,
action: restore['action'] as String,
tradeIndex: restore['trade_index'] as int,
);
}
}
2 changes: 1 addition & 1 deletion lib/data/models/mostro_message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class MostroMessage<T extends Payload> {

factory MostroMessage.fromJson(Map<String, dynamic> json) {
final timestamp = json['timestamp'];
json = json['order'] ?? json['cant-do'] ?? json;
json = json['order'] ?? json['cant-do'] ?? json['restore'] ?? json;
final num requestId = json['request_id'] ?? 0;

return MostroMessage(
Expand Down
86 changes: 86 additions & 0 deletions lib/data/models/nostr_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,92 @@ extension NostrEventExtensions on NostrEvent {
return now.subtract(Duration(seconds: randomSeconds));
}

/// Wraps a RUMOR (kind 1) into a Gift Wrap (kind 1059) with separate keys for RUMOR and SEAL
///
/// This is used for Mostro restore-session and similar operations where:
/// - RUMOR is signed with trade key (index 0)
/// - SEAL is signed with master key (identity key)
///
/// Flow:
/// 1. Create RUMOR (kind 1, unsigned) with rumor keys
/// 2. Encrypt RUMOR with seal keys → SEAL (13)
/// 3. Encrypt SEAL with ephemeral key → Gift Wrap (1059)
///
/// Parameters:
/// - rumorKeys: The keys used to sign the rumor (trade key)
/// - sealKeys: The keys used to sign the seal (master/identity key)
/// - receiverPubkey: The receiver's public key (Mostro pubkey)
Future<NostrEvent> mostroWrapWithSeparateKeys({
Copy link
Member

Choose a reason for hiding this comment

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

why create a new function to build gift wrap event there is mostroWrap() in this same file?

required NostrKeyPairs rumorKeys,
required NostrKeyPairs sealKeys,
required String receiverPubkey,
}) async {
if (kind != 1) {
throw ArgumentError('Expected kind 1 (RUMOR), got: $kind');
}

if (content == null || content!.isEmpty) {
throw ArgumentError('RUMOR content is empty');
}

try {
// STEP 1: Prepare the RUMOR (kind 1, unsigned) with rumor keys
final rumorMap = {
'kind': 1,
'content': content,
'pubkey': rumorKeys.public,
'created_at': ((createdAt ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000),
'tags': tags ?? [],
};

final rumorJson = jsonEncode(rumorMap);

// STEP 2: Create SEAL (kind 13) signed with SEAL KEYS
// Encrypt the rumor with seal private key + receiver's public key
final encryptedRumor = await NostrUtils.encryptNIP44(
rumorJson,
sealKeys.private,
receiverPubkey,
);

final seal = NostrEvent.fromPartialData(
kind: 13,
content: encryptedRumor,
keyPairs: sealKeys, // Use seal keys (master/identity key)
tags: [],
createdAt: DateTime.now(),
);

final sealJson = jsonEncode(seal.toMap());

// STEP 3: Create Gift Wrap (kind 1059)
// Generate ephemeral key pair (single-use)
final ephemeralKeyPair = NostrUtils.generateKeyPair();

// Encrypt the seal with ephemeral key + receiver's public key
final encryptedSeal = await NostrUtils.encryptNIP44(
sealJson,
ephemeralKeyPair.private,
receiverPubkey,
);

// Create Gift Wrap with randomized timestamp
final giftWrap = NostrEvent.fromPartialData(
kind: 1059,
content: encryptedSeal,
keyPairs: ephemeralKeyPair,
tags: [
["p", receiverPubkey],
],
createdAt: _randomizedTimestamp(),
);

return giftWrap;
} catch (e) {
throw Exception('Failed to wrap with separate keys: $e');
}
}

/// P2P Chat: Simplified NIP-59 wrapper for peer-to-peer chat
/// Wraps a signed kind 1 event directly in a kind 1059 wrapper
/// This is different from mostroWrap which uses a SEAL intermediate layer
Expand Down
28 changes: 28 additions & 0 deletions lib/data/models/orders_request_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'dart:convert';

class OrdersRequestMessage {
final int version;
final int requestId;
final String action;
final List<String> orderIds;

OrdersRequestMessage({
this.version = 1,
required this.requestId,
this.action = 'orders',
required this.orderIds,
});

Map<String, dynamic> toJson() => {
'order': {
'version': version,
'request_id': requestId,
'action': action,
'payload': {
'ids': orderIds,
},
},
};

String toJsonString() => jsonEncode([toJson(), null]);
}
3 changes: 3 additions & 0 deletions lib/data/models/payload.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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';
import 'package:mostro_mobile/data/models/restore_data.dart';
import 'package:mostro_mobile/data/models/text_message.dart';

abstract class Payload {
Expand All @@ -17,6 +18,8 @@ abstract class Payload {
return Order.fromJson(json['order']);
} else if (json.containsKey('payment_request')) {
return PaymentRequest.fromJson(json['payment_request']);
} else if (json.containsKey('restore_data')) {
return RestoreData.fromJson(json);
} else if (json.containsKey('cant_do')) {
return CantDo.fromJson(json);
} else if (json.containsKey('peer')) {
Expand Down
91 changes: 91 additions & 0 deletions lib/data/models/restore_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'package:mostro_mobile/data/models/payload.dart';

class RestoreData implements Payload {
final List<RestoredOrder> orders;
final List<RestoredDispute> disputes;

RestoreData({
required this.orders,
required this.disputes,
});

@override
String get type => 'restore_data';

factory RestoreData.fromJson(Map<String, dynamic> json) {
final restoreData = json['restore_data'] as Map<String, dynamic>;

return RestoreData(
orders: (restoreData['orders'] as List<dynamic>?)
?.map((o) => RestoredOrder.fromJson(o as Map<String, dynamic>))
.toList() ?? [],
disputes: (restoreData['disputes'] as List<dynamic>?)
?.map((d) => RestoredDispute.fromJson(d as Map<String, dynamic>))
.toList() ?? [],
);
}

@override
Map<String, dynamic> toJson() => {
'restore_data': {
'orders': orders.map((o) => o.toJson()).toList(),
'disputes': disputes.map((d) => d.toJson()).toList(),
}
};
}

class RestoredOrder {
final String id;
final int tradeIndex;
final String status;

RestoredOrder({
required this.id,
required this.tradeIndex,
required this.status,
});

factory RestoredOrder.fromJson(Map<String, dynamic> json) {
return RestoredOrder(
id: json['order_id'] as String,
tradeIndex: json['trade_index'] as int,
status: json['status'] as String,
);
}

Map<String, dynamic> toJson() => {
'id': id,
'trade_index': tradeIndex,
'status': status,
};
}

class RestoredDispute {
final String disputeId;
final String orderId;
final int tradeIndex;
final String status;

RestoredDispute({
required this.disputeId,
required this.orderId,
required this.tradeIndex,
required this.status,
});

factory RestoredDispute.fromJson(Map<String, dynamic> json) {
return RestoredDispute(
disputeId: json['dispute_id'] as String,
orderId: json['order_id'] as String,
tradeIndex: json['trade_index'] as int,
status: json['status'] as String,
);
}

Map<String, dynamic> toJson() => {
'dispute_id': disputeId,
'order_id': orderId,
'trade_index': tradeIndex,
'status': status,
};
}
21 changes: 21 additions & 0 deletions lib/data/models/restore_message.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'dart:convert';

class RestoreMessage {
final int version;
final String action;

RestoreMessage({
this.version = 1,
this.action = 'restore-session',
});

Map<String, dynamic> toJson() => {
'restore': {
'version': version,
'action': action,
'payload': null,
},
};

String toJsonString() => jsonEncode([toJson(), null]);
}
2 changes: 1 addition & 1 deletion lib/data/repositories/event_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ class EventStorage extends BaseStorage<Map<String, dynamic>> {
Map<String, dynamic> toDbMap(Map<String, dynamic> event) {
return event;
}
}
}
Loading