Skip to content

Commit

Permalink
Rabbit suggestions + l10n
Browse files Browse the repository at this point in the history
  • Loading branch information
chebizarro committed Nov 20, 2024
1 parent c3244d6 commit 09f44ee
Show file tree
Hide file tree
Showing 39 changed files with 1,535 additions and 339 deletions.
3 changes: 3 additions & 0 deletions l10n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: intl_en.arb
output-dir: lib/generated
4 changes: 2 additions & 2 deletions lib/core/config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ class Config {
// Configuración de Nostr
static const List<String> nostrRelays = [
'ws://127.0.0.1:7000',
'ws://10.0.2.2:7000',
//'ws://10.0.2.2:7000',
//'wss://relay.damus.io',
//'wss://relay.mostro.network',
//'wss://relay.nostr.net',
// Agrega más relays aquí si es necesario
];

// Npub de Mostro
// hexkey de Mostro
static const String mostroPubKey =
'9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980';

Expand Down
8 changes: 5 additions & 3 deletions lib/core/utils/auth_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ class AuthUtils {
}

static Future<bool> verifyPin(String inputPin) async {
return true;
throw UnimplementedError('verifyPin is not implemented yet');
}

static Future<void> deleteCredentials() async {}
static Future<void> deleteCredentials() async {
throw UnimplementedError('deleteCredentials is not implemented yet');
}

static Future<void> enableBiometrics() async {}

static Future<bool> isBiometricsEnabled() async {
return true;
throw UnimplementedError('isBiometricsEnabled is not implemented yet');
}
}
146 changes: 106 additions & 40 deletions lib/core/utils/nostr_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -118,16 +118,35 @@ class NostrUtils {
return digest.toString(); // Devuelve el ID como una cadena hex
}

/// Generates a timestamp between now and 48 hours ago to enhance privacy
/// by decorrelating event timing from creation time.
/// @throws if system clock is ahead of network time
static DateTime randomNow() {
final now = DateTime.now();
final randomSeconds =
Random().nextInt(2 * 24 * 60 * 60);
// Validate system time isn't ahead
final networkTime = DateTime.now().toUtc();
if (now.isAfter(networkTime.add(Duration(minutes: 5)))) {
throw Exception('System clock is ahead of network time');
}
final randomSeconds = Random().nextInt(2 * 24 * 60 * 60);
return now.subtract(Duration(seconds: randomSeconds));
}

// NIP-59 y NIP-44 funciones
/// Creates a NIP-59 encrypted event with the following structure:
/// 1. Inner event (kind 1): Original content
/// 2. Seal event (kind 13): Encrypted inner event
/// 3. Wrapper event (kind 1059): Final encrypted package
static Future<NostrEvent> createNIP59Event(
String content, String recipientPubKey, String senderPrivateKey) async {
// Validate inputs
if (content.isEmpty) throw ArgumentError('Content cannot be empty');
if (recipientPubKey.length != 64) {
throw ArgumentError('Invalid recipient public key');
}
if (!isValidPrivateKey(senderPrivateKey)) {
throw ArgumentError('Invalid sender private key');
}

final senderKeyPair = generateKeyPairFromPrivateKey(senderPrivateKey);

final createdAt = DateTime.now();
Expand All @@ -141,8 +160,14 @@ class NostrUtils {
],
);

final encryptedContent = await _encryptNIP44(
jsonEncode(rumorEvent.toMap()), senderPrivateKey, '02$recipientPubKey');
String? encryptedContent;

try {
encryptedContent = await _encryptNIP44(
jsonEncode(rumorEvent.toMap()), senderPrivateKey, recipientPubKey);
} catch (e) {
throw Exception('Failed to encrypt content: $e');
}

final sealEvent = NostrEvent.fromPartialData(
kind: 13,
Expand Down Expand Up @@ -173,49 +198,90 @@ class NostrUtils {

static Future<NostrEvent> decryptNIP59Event(
NostrEvent event, String privateKey) async {
final decryptedContent =
await _decryptNIP44(event.content ?? '', privateKey, event.pubkey);

final rumorEvent =
NostrEvent.deserialized('["EVENT", "", $decryptedContent]');

final finalDecryptedContent = await _decryptNIP44(
rumorEvent.content ?? '', privateKey, rumorEvent.pubkey);

final wrap = jsonDecode(finalDecryptedContent) as Map<String, dynamic>;

return NostrEvent(
id: wrap['id'] as String,
kind: wrap['kind'] as int,
content: wrap['content'] as String,
sig: "",
pubkey: wrap['pubkey'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch(
(wrap['created_at'] as int) * 1000,
),
tags: List<List<String>>.from(
(wrap['tags'] as List)
.map(
(nestedElem) => (nestedElem as List)
.map(
(nestedElemContent) => nestedElemContent.toString(),
)
.toList(),
)
.toList(),
),
subscriptionId: '',
);
// Validate inputs
if (event.content == null || event.content!.isEmpty) {
throw ArgumentError('Event content is empty');
}
if (!isValidPrivateKey(privateKey)) {
throw ArgumentError('Invalid private key');
}

try {
final decryptedContent =
await _decryptNIP44(event.content ?? '', privateKey, event.pubkey);

final rumorEvent =
NostrEvent.deserialized('["EVENT", "", $decryptedContent]');

final finalDecryptedContent = await _decryptNIP44(
rumorEvent.content ?? '', privateKey, rumorEvent.pubkey);

final wrap = jsonDecode(finalDecryptedContent) as Map<String, dynamic>;

// Validate decrypted event structure
_validateEventStructure(wrap);

return NostrEvent(
id: wrap['id'] as String,
kind: wrap['kind'] as int,
content: wrap['content'] as String,
sig: "",
pubkey: wrap['pubkey'] as String,
createdAt: DateTime.fromMillisecondsSinceEpoch(
(wrap['created_at'] as int) * 1000,
),
tags: List<List<String>>.from(
(wrap['tags'] as List)
.map(
(nestedElem) => (nestedElem as List)
.map(
(nestedElemContent) => nestedElemContent.toString(),
)
.toList(),
)
.toList(),
),
subscriptionId: '',
);
} catch (e) {
throw Exception('Failed to decrypt NIP-59 event: $e');
}
}

/// Validates the structure of a decrypted event
static void _validateEventStructure(Map<String, dynamic> event) {
final requiredFields = [
'id',
'kind',
'content',
'pubkey',
'created_at',
'tags'
];
for (final field in requiredFields) {
if (!event.containsKey(field)) {
throw FormatException('Missing required field: $field');
}
}
}

static Future<String> _encryptNIP44(
String content, String privkey, String pubkey) async {
return await Nip44.encryptMessage(content, privkey, pubkey);
try {
return await Nip44.encryptMessage(content, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Encryption failed: $e');
}
}

static Future<String> _decryptNIP44(
String encryptedContent, String privkey, String pubkey) async {
return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
try {
return await Nip44.decryptMessage(encryptedContent, privkey, pubkey);
} catch (e) {
// Handle encryption error appropriately
throw Exception('Decryption failed: $e');
}
}
}
12 changes: 12 additions & 0 deletions lib/data/models/content.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import 'package:mostro_mobile/data/models/order.dart';
import 'package:mostro_mobile/data/models/payment_request.dart';

abstract class Content {
String get type;
Map<String, dynamic> toJson();

factory Content.fromJson(Map<String, dynamic> json) {
if (json.containsKey('order')) {
return Order.fromJson(json['order']);
} else if (json.containsKey('payment_request')) {
return PaymentRequest.fromJson(json['payment_request']);
}
throw UnsupportedError('Unknown content type');
}
}
99 changes: 90 additions & 9 deletions lib/data/models/conversion_result.dart
Original file line number Diff line number Diff line change
@@ -1,42 +1,123 @@
/// Represents the result of a currency conversion operation.
class ConversionResult {
/// The original conversion request
final ConversionRequest request;
/// The converted amount
final double result;
/// The conversion rate used
final double rate;
/// Unix timestamp of when the conversion was performed
final int timestamp;

ConversionResult({
required this.request,
required this.result,
required this.rate,
required this.timestamp,
});
}) {
if (timestamp < 0) {
throw ArgumentError('Timestamp cannot be negative');
}
}

factory ConversionResult.fromJson(Map<String, dynamic> json) {
if (json['request'] == null) {
throw FormatException('Missing required field: request');
}
return ConversionResult(
request: ConversionRequest.fromJson(json['request']),
result: (json['result'] as num).toDouble(),
rate: (json['rate'] as num).toDouble(),
timestamp: json['timestamp'],
result: (json['result'] as num?)?.toDouble() ?? 0.0,
rate: (json['rate'] as num?)?.toDouble() ?? 0.0,
timestamp: json['timestamp'] ? json['timestamp'] as int :
throw FormatException('Missing or invalid timestamp'),
);
}

Map<String, dynamic> toJson() => {
'request': request.toJson(),
'result': result,
'rate': rate,
'timestamp': timestamp,
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConversionResult &&
request == other.request &&
result == other.result &&
rate == other.rate &&
timestamp == other.timestamp;

@override
int get hashCode => Object.hash(request, result, rate, timestamp);

@override
String toString() => 'ConversionResult('
'request: $request, '
'result: $result, '
'rate: $rate, '
'timestamp: $timestamp)';
}

/// Represents a request to convert between currencies.
class ConversionRequest {
/// The amount to convert in the smallest unit of the currency
final int amount;
/// The currency code to convert from (ISO 4217)
final String from;
/// The currency code to convert to (ISO 4217)
final String to;

ConversionRequest({
required this.amount,
required this.from,
required this.to,
});
}) {
if (amount < 0) {
throw ArgumentError('Amount cannot be negative');
}
if (from.length != 3 || to.length != 3) {
throw ArgumentError('Currency codes must be 3 characters (ISO 4217)');
}
}

factory ConversionRequest.fromJson(Map<String, dynamic> json) {
final amount = json['amount'] as int?;
final from = json['from'] as String?;
final to = json['to'] as String?;

if (amount == null || from == null || to == null) {
throw FormatException('Missing required fields');
}

return ConversionRequest(
amount: json['amount'],
from: json['from'],
to: json['to'],
amount: amount,
from: from.toUpperCase(),
to: to.toUpperCase(),
);
}
}

Map<String, dynamic> toJson() => {
'amount': amount,
'from': from,
'to': to,
};

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ConversionRequest &&
amount == other.amount &&
from == other.from &&
to == other.to;

@override
int get hashCode => Object.hash(amount, from, to);

@override
String toString() => 'ConversionRequest('
'amount: $amount, '
'from: $from, '
'to: $to)';
}
19 changes: 13 additions & 6 deletions lib/data/models/enums/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,19 @@ enum Action {

const Action(this.value);


static Action fromString(String value) {
return Action.values.firstWhere(
(k) => k.value == value,
orElse: () => throw ArgumentError('Invalid Kind: $value'),
);
/// Converts a string value to its corresponding Action enum value.
///
/// Throws an ArgumentError if the string doesn't match any Action value.
static final _valueMap = {
for (var action in Action.values) action.value: action
};

static Action fromString(String value) {
final action = _valueMap[value];
if (action == null) {
throw ArgumentError('Invalid Action: $value');
}
return action;
}

}
Loading

0 comments on commit 09f44ee

Please sign in to comment.