From 0a1000d8488e01e0b049fef13be122e418095356 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Thu, 31 Oct 2024 16:35:21 -0700 Subject: [PATCH 1/9] Merged commits Add ExchangeService and OrderFilter widget added MostroAction enum type to match Rust implementation Added content, message, status and small order types Exchange service Add Content types for Mostro Orders Add session and session manager Events and orders Add test files from protocol docs Started test suites added integration tests add more test case data from docs Updated Order Display Filter ui add providers and session storage Added currency exchange lookup and custom currency fields Added input check for Add Order screen Update order screen to use App theme extracted Exchange Service providers to own file Order Details revision Refactor to StreamProvider for events Combined Kind and OrderType enums Add NIP-44 implementation nip44 impl Working Nip44 implementation nip44 complete implementation Nip44 clean up nip44 final mods android build updates build for Android --- .gitignore | 3 +- android/app/build.gradle | 2 +- integration_test/app_test.dart | 27 + lib/core/config.dart | 13 +- lib/core/utils/auth_utils.dart | 29 +- lib/core/utils/biometrics_helper.dart | 1 - lib/core/utils/nostr_utils.dart | 139 +++--- lib/data/models/content.dart | 4 + lib/data/models/conversion_result.dart | 42 ++ lib/data/models/enums/action.dart | 61 +++ lib/data/models/enums/order_type.dart | 15 + lib/data/models/enums/status.dart | 29 ++ lib/data/models/mostro_message.dart | 31 ++ lib/data/models/nostr_event.dart | 148 +----- lib/data/models/order.dart | 117 +++++ lib/data/models/order_model.dart | 3 +- lib/data/models/payment_request.dart | 27 + lib/data/models/peer.dart | 25 + lib/data/models/session.dart | 40 ++ lib/data/models/text_message.dart | 17 + lib/data/models/user_model.dart | 17 - .../repositories/mostro_order_repository.dart | 44 -- lib/data/repositories/mostro_repository.dart | 39 ++ .../repositories/open_orders_repository.dart | 29 ++ .../order_repository_interface.dart | 14 - .../repositories/secure_storage_manager.dart | 72 +++ lib/main.dart | 28 +- .../add_order/bloc/add_order_bloc.dart | 15 +- .../add_order/bloc/add_order_event.dart | 17 +- .../add_order/bloc/add_order_state.dart | 10 +- .../add_order/screens/add_order_screen.dart | 231 ++++++--- .../chat_list/screens/chat_list_screen.dart | 6 +- lib/presentation/home/bloc/home_bloc.dart | 21 +- lib/presentation/home/bloc/home_event.dart | 2 +- lib/presentation/home/bloc/home_state.dart | 8 +- .../home/screens/home_screen.dart | 77 +-- .../order/bloc/order_details_bloc.dart | 25 +- .../order/bloc/order_details_event.dart | 13 +- .../order/bloc/order_details_state.dart | 8 +- .../order/screens/order_details_screen.dart | 292 ++++++----- .../payment_qr/screens/payment_qr_screen.dart | 7 - .../widgets/currency_dropdown.dart | 61 +++ .../widgets/currency_text_field.dart | 42 ++ lib/presentation/widgets/custom_app_bar.dart | 1 + .../widgets/exchange_rate_widget.dart | 69 +++ lib/presentation/widgets/group_box.dart | 53 ++ lib/presentation/widgets/order_filter.dart | 107 ++++ lib/presentation/widgets/order_list.dart | 4 +- lib/presentation/widgets/order_list_item.dart | 24 +- lib/providers/event_store_providers.dart | 26 + lib/providers/exchange_service_provider.dart | 21 + lib/providers/riverpod_providers.dart | 23 + lib/services/currency_input_formatter.dart | 18 + lib/services/exchange_service.dart | 44 ++ lib/services/mostro_service.dart | 166 +++++-- lib/services/nostr_service.dart | 18 +- lib/services/yadio_exchange_service.dart | 29 ++ linux/flutter/generated_plugin_registrant.cc | 4 - linux/flutter/generated_plugins.cmake | 1 - macos/Flutter/GeneratedPluginRegistrant.swift | 2 - pubspec.lock | 467 ++++++++++++++---- pubspec.yaml | 24 +- test/examples/admin_add_solver.json | 25 + test/examples/admin_cancel_order.json | 67 +++ test/examples/admin_settle_order.json | 93 ++++ test/examples/buyer_sends_ln.json | 48 ++ test/examples/cancel_order.json | 121 +++++ test/examples/dispute.json | 108 ++++ test/examples/fiat_sent.json | 62 +++ test/examples/list_disputes.json | 23 + test/examples/list_orders.json | 27 + test/examples/new_buy_order.json | 70 +++ test/examples/new_buy_order_ln.json | 71 +++ test/examples/new_sell_order.json | 69 +++ test/examples/new_sell_range_order.json | 71 +++ test/examples/overview.json | 8 + test/examples/rate_user.json | 61 +++ test/examples/release.json | 94 ++++ test/examples/seller_pays_hold_invoive.json | 163 ++++++ test/examples/take_buy_order.json | 167 +++++++ test/examples/take_buy_range_order.json | 12 + test/examples/take_sell_order.json | 56 +++ test/examples/take_sell_order_ln.json | 47 ++ test/examples/take_sell_range_order.json | 73 +++ test/models/order_test.dart | 51 ++ .../flutter/generated_plugin_registrant.cc | 3 - windows/flutter/generated_plugins.cmake | 1 - 87 files changed, 3740 insertions(+), 803 deletions(-) create mode 100644 integration_test/app_test.dart create mode 100644 lib/data/models/content.dart create mode 100644 lib/data/models/conversion_result.dart create mode 100644 lib/data/models/enums/action.dart create mode 100644 lib/data/models/enums/order_type.dart create mode 100644 lib/data/models/enums/status.dart create mode 100644 lib/data/models/mostro_message.dart create mode 100644 lib/data/models/order.dart create mode 100644 lib/data/models/payment_request.dart create mode 100644 lib/data/models/peer.dart create mode 100644 lib/data/models/session.dart create mode 100644 lib/data/models/text_message.dart delete mode 100644 lib/data/models/user_model.dart delete mode 100644 lib/data/repositories/mostro_order_repository.dart create mode 100644 lib/data/repositories/mostro_repository.dart create mode 100644 lib/data/repositories/open_orders_repository.dart create mode 100644 lib/data/repositories/secure_storage_manager.dart create mode 100644 lib/presentation/widgets/currency_dropdown.dart create mode 100644 lib/presentation/widgets/currency_text_field.dart create mode 100644 lib/presentation/widgets/exchange_rate_widget.dart create mode 100644 lib/presentation/widgets/group_box.dart create mode 100644 lib/presentation/widgets/order_filter.dart create mode 100644 lib/providers/event_store_providers.dart create mode 100644 lib/providers/exchange_service_provider.dart create mode 100644 lib/providers/riverpod_providers.dart create mode 100644 lib/services/currency_input_formatter.dart create mode 100644 lib/services/exchange_service.dart create mode 100644 lib/services/yadio_exchange_service.dart create mode 100644 test/examples/admin_add_solver.json create mode 100644 test/examples/admin_cancel_order.json create mode 100644 test/examples/admin_settle_order.json create mode 100644 test/examples/buyer_sends_ln.json create mode 100644 test/examples/cancel_order.json create mode 100644 test/examples/dispute.json create mode 100644 test/examples/fiat_sent.json create mode 100644 test/examples/list_disputes.json create mode 100644 test/examples/list_orders.json create mode 100644 test/examples/new_buy_order.json create mode 100644 test/examples/new_buy_order_ln.json create mode 100644 test/examples/new_sell_order.json create mode 100644 test/examples/new_sell_range_order.json create mode 100644 test/examples/overview.json create mode 100644 test/examples/rate_user.json create mode 100644 test/examples/release.json create mode 100644 test/examples/seller_pays_hold_invoive.json create mode 100644 test/examples/take_buy_order.json create mode 100644 test/examples/take_buy_range_order.json create mode 100644 test/examples/take_sell_order.json create mode 100644 test/examples/take_sell_order_ln.json create mode 100644 test/examples/take_sell_range_order.json create mode 100644 test/models/order_test.dart diff --git a/.gitignore b/.gitignore index 612c050b..59ffe6fe 100644 --- a/.gitignore +++ b/.gitignore @@ -70,4 +70,5 @@ chrome/.packages # Misc *.log *.lock -.pdm \ No newline at end of file +.pdm +bfg-1.14.0.jar diff --git a/android/app/build.gradle b/android/app/build.gradle index 933cf738..fba96afe 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ plugins { android { namespace = "com.example.mostro_mobile" compileSdk = flutter.compileSdkVersion - ndkVersion = "25.1.8937393" // flutter.ndkVersion + ndkVersion = "25.1.8937393" //flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 00000000..6140ad54 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mostro_mobile/main.dart' as app; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Create and send a new sell order', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + // Navigate to Create Order Screen + final createOrderButton = find.byKey(Key('createOrderButton')); + await tester.tap(createOrderButton); + await tester.pumpAndSettle(); + + // Input order details + await tester.enterText(find.byKey(Key('fiatAmountField')), '100'); + await tester.tap(find.byKey(Key('submitOrderButton'))); + await tester.pumpAndSettle(); + + // Verify that order confirmation appears on UI + final confirmationMessage = find.text('Order Created'); + expect(confirmationMessage, findsOneWidget); + }); +} diff --git a/lib/core/config.dart b/lib/core/config.dart index 4fff4843..7e054592 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -1,20 +1,19 @@ -// lib/core/config.dart - import 'package:flutter/foundation.dart'; class Config { // Configuración de Nostr static const List nostrRelays = [ - //'ws://10.0.2.2:7000', - 'wss://relay.damus.io', - 'wss://relay.mostro.network', - 'wss://relay.nostr.net', + 'ws://127.0.0.1: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 static const String mostroPubKey = - 'npub1n5yrh6lkvc0l3lcmcfwake4r3ex7jrm0e6lumsc22d8ylf7jwk0qack9tql;'; + '9d9d0455a96871f2dc4289b8312429db2e925f167b37c77bf7b28014be235980'; // Tiempo de espera para conexiones a relays static const Duration nostrConnectionTimeout = Duration(seconds: 30); diff --git a/lib/core/utils/auth_utils.dart b/lib/core/utils/auth_utils.dart index b1db6816..b4b27bd4 100644 --- a/lib/core/utils/auth_utils.dart +++ b/lib/core/utils/auth_utils.dart @@ -1,33 +1,20 @@ -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - class AuthUtils { - static const _storage = FlutterSecureStorage(); - - static Future savePrivateKeyAndPin(String privateKey, String pin) async { - await _storage.write(key: 'user_private_key', value: privateKey); - await _storage.write(key: 'user_pin', value: pin); - } + static Future savePrivateKeyAndPin( + String privateKey, String pin) async {} static Future getPrivateKey() async { - return await _storage.read(key: 'user_private_key'); + return null; } static Future verifyPin(String inputPin) async { - final storedPin = await _storage.read(key: 'user_pin'); - return storedPin == inputPin; + return true; } - static Future deleteCredentials() async { - await _storage.delete(key: 'user_private_key'); - await _storage.delete(key: 'user_pin'); - await _storage.delete(key: 'use_biometrics'); - } + static Future deleteCredentials() async {} - static Future enableBiometrics() async { - await _storage.write(key: 'use_biometrics', value: 'true'); - } + static Future enableBiometrics() async {} static Future isBiometricsEnabled() async { - return await _storage.read(key: 'use_biometrics') == 'true'; + return true; } -} \ No newline at end of file +} diff --git a/lib/core/utils/biometrics_helper.dart b/lib/core/utils/biometrics_helper.dart index da814e07..ff06e547 100644 --- a/lib/core/utils/biometrics_helper.dart +++ b/lib/core/utils/biometrics_helper.dart @@ -19,7 +19,6 @@ class BiometricsHelper { ), ); } catch (e) { - print(e); return false; } } diff --git a/lib/core/utils/nostr_utils.dart b/lib/core/utils/nostr_utils.dart index b4c60a6d..476e8b46 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/core/utils/nostr_utils.dart @@ -2,14 +2,18 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:dart_nostr/dart_nostr.dart'; -import 'package:encrypt/encrypt.dart' as encrypt; +import 'package:elliptic/elliptic.dart'; +import 'package:nip44/nip44.dart'; class NostrUtils { static final Nostr _instance = Nostr.instance; // Generación de claves static NostrKeyPairs generateKeyPair() { - return _instance.keysService.generateKeyPair(); + var ec = getS256(); + var priv = ec.generatePrivateKey(); + + return NostrKeyPairs(private: priv.toHex()); } static NostrKeyPairs generateKeyPairFromPrivateKey(String privateKey) { @@ -118,73 +122,93 @@ class NostrUtils { } // NIP-59 y NIP-44 funciones - static NostrEvent createNIP59Event( - String content, String recipientPubKey, String senderPrivateKey) { + static Future createNIP59Event( + String content, String recipientPubKey, String senderPrivateKey) async { final senderKeyPair = generateKeyPairFromPrivateKey(senderPrivateKey); - final sharedSecret = - _calculateSharedSecret(senderPrivateKey, recipientPubKey); - - final encryptedContent = _encryptNIP44(content, sharedSecret); final createdAt = DateTime.now(); - final rumorEvent = NostrEvent( - kind: 1059, - pubkey: senderKeyPair.public, - content: encryptedContent, + final rumorEvent = NostrEvent.fromPartialData( + kind: 1, + keyPairs: senderKeyPair, + content: content, + createdAt: createdAt, tags: [ ["p", recipientPubKey] ], - createdAt: createdAt, - id: '', // Se generará después - sig: '', // Se generará después ); - // Generar ID y firma - final id = generateId({ - 'pubkey': rumorEvent.pubkey, - 'created_at': rumorEvent.createdAt!.millisecondsSinceEpoch ~/ 1000, - 'kind': rumorEvent.kind, - 'tags': rumorEvent.tags, - 'content': rumorEvent.content, - }); - signMessage(id, senderPrivateKey); + randomNow() => DateTime(createdAt.millisecondsSinceEpoch ~/ 1000); + + final encryptedContent = _encryptNIP44( + jsonEncode(rumorEvent.toMap()), senderPrivateKey, '02$recipientPubKey'); + + final sealEvent = NostrEvent.fromPartialData( + kind: 13, + keyPairs: senderKeyPair, + content: await encryptedContent, + createdAt: randomNow(), + ); final wrapperKeyPair = generateKeyPair(); - final wrappedContent = _encryptNIP44(jsonEncode(rumorEvent.toMap()), - _calculateSharedSecret(wrapperKeyPair.private, recipientPubKey)); - return NostrEvent( + final pk = wrapperKeyPair.private; + + final sealedContent = + _encryptNIP44(jsonEncode(sealEvent.toMap()), pk, '02$recipientPubKey'); + + final wrapEvent = NostrEvent.fromPartialData( kind: 1059, - pubkey: wrapperKeyPair.public, - content: wrappedContent, + content: await sealedContent, + keyPairs: wrapperKeyPair, tags: [ ["p", recipientPubKey] ], - createdAt: DateTime.now(), - id: generateId({ - 'pubkey': wrapperKeyPair.public, - 'created_at': DateTime.now().millisecondsSinceEpoch ~/ 1000, - 'kind': 1059, - 'tags': [ - ["p", recipientPubKey] - ], - 'content': wrappedContent, - }), - sig: '', // Se generará automáticamente al publicar el evento + createdAt: createdAt, ); + + return wrapEvent; } - static String decryptNIP59Event(NostrEvent event, String privateKey) { - final sharedSecret = _calculateSharedSecret(privateKey, event.pubkey); - final decryptedContent = _decryptNIP44(event.content ?? '', sharedSecret); + static Future decryptNIP59Event( + NostrEvent event, String privateKey) async { + final decryptedContent = + await _decryptNIP44(event.content ?? '', privateKey, event.pubkey); + + final rumorEvent = + NostrEvent.deserialized('["EVENT", "", $decryptedContent]'); - final rumorEvent = NostrEvent.deserialized(decryptedContent); - final rumorSharedSecret = - _calculateSharedSecret(privateKey, rumorEvent.pubkey); - final finalDecryptedContent = - _decryptNIP44(rumorEvent.content ?? '', rumorSharedSecret); + final finalDecryptedContent = await _decryptNIP44( + rumorEvent.content ?? '', privateKey, rumorEvent.pubkey); - return finalDecryptedContent; + print(finalDecryptedContent); + print( + NostrEvent.canBeDeserialized('["EVENT", "", $finalDecryptedContent]')); + + final wrap = jsonDecode(finalDecryptedContent) as Map; + + 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>.from( + (wrap['tags'] as List) + .map( + (nestedElem) => (nestedElem as List) + .map( + (nestedElemContent) => nestedElemContent.toString(), + ) + .toList(), + ) + .toList(), + ), + subscriptionId: '', + + ); } static Uint8List _calculateSharedSecret(String privateKey, String publicKey) { @@ -195,18 +219,13 @@ class NostrUtils { return Uint8List.fromList(sha256.convert(utf8.encode(sharedPoint)).bytes); } - static String _encryptNIP44(String content, Uint8List key) { - final iv = encrypt.IV.fromSecureRandom(16); - final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key(key))); - final encrypted = encrypter.encrypt(content, iv: iv); - return base64Encode(iv.bytes + encrypted.bytes); + static Future _encryptNIP44( + String content, String privkey, String pubkey) async { + return await Nip44.encryptMessage(content, privkey, pubkey); } - static String _decryptNIP44(String encryptedContent, Uint8List key) { - final decoded = base64Decode(encryptedContent); - final iv = encrypt.IV(decoded.sublist(0, 16)); - final encryptedBytes = decoded.sublist(16); - final encrypter = encrypt.Encrypter(encrypt.AES(encrypt.Key(key))); - return encrypter.decrypt64(base64Encode(encryptedBytes), iv: iv); + static Future _decryptNIP44( + String encryptedContent, String privkey, String pubkey) async { + return await Nip44.decryptMessage(encryptedContent, privkey, pubkey); } } diff --git a/lib/data/models/content.dart b/lib/data/models/content.dart new file mode 100644 index 00000000..ae33d3a3 --- /dev/null +++ b/lib/data/models/content.dart @@ -0,0 +1,4 @@ +abstract class Content { + String get type; + Map toJson(); +} diff --git a/lib/data/models/conversion_result.dart b/lib/data/models/conversion_result.dart new file mode 100644 index 00000000..cdd2e561 --- /dev/null +++ b/lib/data/models/conversion_result.dart @@ -0,0 +1,42 @@ +class ConversionResult { + final ConversionRequest request; + final double result; + final double rate; + final int timestamp; + + ConversionResult({ + required this.request, + required this.result, + required this.rate, + required this.timestamp, + }); + + factory ConversionResult.fromJson(Map json) { + return ConversionResult( + request: ConversionRequest.fromJson(json['request']), + result: (json['result'] as num).toDouble(), + rate: (json['rate'] as num).toDouble(), + timestamp: json['timestamp'], + ); + } +} + +class ConversionRequest { + final int amount; + final String from; + final String to; + + ConversionRequest({ + required this.amount, + required this.from, + required this.to, + }); + + factory ConversionRequest.fromJson(Map json) { + return ConversionRequest( + amount: json['amount'], + from: json['from'], + to: json['to'], + ); + } +} diff --git a/lib/data/models/enums/action.dart b/lib/data/models/enums/action.dart new file mode 100644 index 00000000..14ab1d57 --- /dev/null +++ b/lib/data/models/enums/action.dart @@ -0,0 +1,61 @@ +enum Action { + newOrder('new-order'), + takeSell('take-sell'), + takeBuy('take-buy'), + payInvoice('pay-invoice'), + fiatSent('fiat-sent'), + fiatSentOk('fiat-sent-ok'), + release('release'), + released('released'), + cancel('cancel'), + canceled('canceled'), + cooperativeCancelInitiatedByYou('cooperative-cancel-initiated-by-you'), + cooperativeCancelInitiatedByPeer('cooperative-cancel-initiated-by-peer'), + disputeInitiatedByYou('dispute-initiated-by-you'), + disputeInitiatedByPeer('dispute-initiated-by-peer'), + cooperativeCancelAccepted('cooperative-cancel-accepted'), + buyerInvoiceAccepted('buyer-invoice-accepted'), + purchaseCompleted('purchase-completed'), + holdInvoicePaymentAccepted('hold-invoice-payment-accepted'), + holdInvoicePaymentSettled('hold-invoice-payment-settled'), + holdInvoicePaymentCanceled('hold-invoice-payment-canceled'), + waitingSellerToPay('waiting-seller-to-pay'), + waitingBuyerInvoice('waiting-buyer-invoice'), + addInvoice('add-invoice'), + buyerTookOrder('buyer-took-order'), + rate('rate'), + rateUser('rate-user'), + rateReceived('rate-received'), + cantDo('cant-do'), + dispute('dispute'), + adminCancel('admin-cancel'), + adminCanceled('admin-canceled'), + adminSettle('admin-settle'), + adminSettled('admin-settled'), + adminAddSolver('admin-add-solver'), + adminTakeDispute('admin-take-dispute'), + adminTookDispute('admin-took-dispute'), + isNotYourOrder('is-not-your-order'), + notAllowedByStatus('not-allowed-by-status'), + outOfRangeFiatAmount('out-of-range-fiat-amount'), + isNotYourDispute('is-not-your-dispute'), + notFound('not-found'), + incorrectInvoiceAmount('incorrect-invoice-amount'), + invalidSatsAmount('invalid-sats-amount'), + outOfRangeSatsAmount('out-of-range-sats-amount'), + paymentFailed('payment-failed'), + invoiceUpdated('invoice-updated'); + + final String value; + + const Action(this.value); + + + static Action fromString(String value) { + return Action.values.firstWhere( + (k) => k.value == value, + orElse: () => throw ArgumentError('Invalid Kind: $value'), + ); + } + +} diff --git a/lib/data/models/enums/order_type.dart b/lib/data/models/enums/order_type.dart new file mode 100644 index 00000000..f282ac41 --- /dev/null +++ b/lib/data/models/enums/order_type.dart @@ -0,0 +1,15 @@ +enum OrderType { + buy('buy'), + sell('sell'); + + final String value; + + const OrderType(this.value); + + static OrderType fromString(String value) { + return OrderType.values.firstWhere( + (k) => k.value == value, + orElse: () => throw ArgumentError('Invalid Kind: $value'), + ); + } +} diff --git a/lib/data/models/enums/status.dart b/lib/data/models/enums/status.dart new file mode 100644 index 00000000..8d0b6c48 --- /dev/null +++ b/lib/data/models/enums/status.dart @@ -0,0 +1,29 @@ +enum Status { + active('active'), + canceled('canceled'), + canceledByAdmin('canceled-by-admin'), + settledByAdmin('settled-by-admin'), + completedByAdmin('completed-by-admin'), + dispute('dispute'), + expired('expired'), + fiatSent('fiat-sent'), + settledHoldInvoice('settled-hold-invoice'), + pending('pending'), + success('success'), + waitingBuyerInvoice('waiting-buyer-invoice'), + waitingPayment('waiting-payment'), + cooperativelyCanceled('cooperatively-canceled'), + inProgress('in-progress'); + + final String value; + + const Status(this.value); + + static Status fromString(String value) { + return Status.values.firstWhere( + (k) => k.value == value, + orElse: () => throw ArgumentError('Invalid Status: $value'), + ); + } + +} \ No newline at end of file diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart new file mode 100644 index 00000000..b2b5ba6b --- /dev/null +++ b/lib/data/models/mostro_message.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; + +class MostroMessage { + final int version = mostroVersion; + final String? requestId; + final Action action; + T? content; + + MostroMessage({required this.action, required this.requestId, this.content}); + + Map toJson() { + return { + 'order': { + 'version': mostroVersion, + 'id': requestId, + 'action': action.value, + 'content': content?.toJson(), + }, + }; + } + + factory MostroMessage.deserialized(String data) { + final decoded = jsonDecode(data); + final event = decoded as Map; + return MostroMessage(action: Action.fromString(event['order']['action']), requestId: event['order']['id']); + } +} diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index c7f56780..b0d1a524 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -1,26 +1,11 @@ import 'dart:convert'; +import 'package:timeago/timeago.dart' as timeago; import 'package:dart_nostr/dart_nostr.dart'; -import 'package:mostro_mobile/core/utils/nostr_utils.dart'; - -class P2POrderEvent { - String id; - final String pubkey; - final int createdAt; - final int kind = 38383; // Evento P2P con tipo 38383 - final List> tags; - final String content; - String sig; - - P2POrderEvent({ - required this.id, - required this.pubkey, - required this.createdAt, - required this.tags, - required this.content, - required this.sig, - }); +import 'package:mostro_mobile/data/models/order.dart'; +extension NostrEventExtensions on NostrEvent { // Getters para acceder fácilmente a los tags específicos + String? get recipient => _getTagValue('p'); String? get orderId => _getTagValue('d'); String? get orderType => _getTagValue('k'); String? get currency => _getTagValue('f'); @@ -29,121 +14,28 @@ class P2POrderEvent { String? get fiatAmount => _getTagValue('fa'); List get paymentMethods => _getTagValue('pm')?.split(',') ?? []; String? get premium => _getTagValue('premium'); + String? get source => _getTagValue('source'); + String? get rating => _getTagValue('rating') ?? "0"; String? get network => _getTagValue('network'); String? get layer => _getTagValue('layer'); - String? get expiration => _getTagValue('expiration'); + String? get name => _getTagValue('name') ?? 'Anon'; + String? get geohash => _getTagValue('g'); + String? get bond => _getTagValue('bond'); + String? get expiration => _timeAgo(_getTagValue('expiration')); String? get platform => _getTagValue('y'); + Order? get document => Order.fromJson(jsonDecode(_getTagValue('z')!)); String? _getTagValue(String key) { - final tag = tags.firstWhere((t) => t[0] == key, orElse: () => []); - return tag.length > 1 ? tag[1] : null; - } - - // Convertir el evento a un mapa JSON - Map toJson() { - return { - "id": id, - "pubkey": pubkey, - "created_at": createdAt, - "kind": kind, - "tags": tags, - "content": content, - "sig": sig, - }; - } - - // Convertir el evento a una cadena JSON - String toJsonString() => jsonEncode(toJson()); - - // Factory para crear un evento a partir de un JSON - factory P2POrderEvent.fromJson(Map json) { - return P2POrderEvent( - id: json['id'], - pubkey: json['pubkey'], - createdAt: json['created_at'], - tags: List>.from( - json['tags'].map((tag) => List.from(tag))), - content: json['content'], - sig: json['sig'], - ); - } - - // Método para añadir o actualizar un tag - void setTag(String key, String value) { - final index = tags.indexWhere((t) => t[0] == key); - if (index != -1) { - tags[index] = [key, value]; - } else { - tags.add([key, value]); - } - } - - // Método para validar el evento - bool isValid() { - // Implementa la lógica de validación aquí - // Por ejemplo, verifica que todos los tags obligatorios estén presentes - return orderId != null && - orderType != null && - currency != null && - status != null; - } - - // Método para firmar el evento - void sign(String privateKey) { - final eventData = { - 'pubkey': pubkey, - 'created_at': createdAt, - 'kind': kind, - 'tags': tags, - 'content': content, - }; - - id = NostrUtils.generateId(eventData); - sig = Nostr.instance.keysService.sign(privateKey: privateKey, message: id); - } - - // Método para verificar la firma del evento - bool verifySignature() { - final eventData = { - 'pubkey': pubkey, - 'created_at': createdAt, - 'kind': kind, - 'tags': tags, - 'content': content, - }; - final calculatedId = NostrUtils.generateId(eventData); - - if (calculatedId != id) { - return false; - } - - return Nostr.instance.keysService.verify( - publicKey: pubkey, - message: id, - signature: sig, - ); + final tag = tags?.firstWhere((t) => t[0] == key, orElse: () => []); + return tag!.length > 1 ? tag[1] : null; } - // Factory para crear y firmar un nuevo evento - factory P2POrderEvent.create({ - required String privateKey, - required int createdAt, - required List> tags, - required String content, - }) { - final keyPair = Nostr.instance.keysService - .generateKeyPairFromExistingPrivateKey(privateKey); - - final event = P2POrderEvent( - id: '', // Se generará al firmar - pubkey: keyPair.public, - createdAt: createdAt, - tags: tags, - content: content, - sig: '', // Se generará al firmar - ); - - event.sign(privateKey); - return event; + String _timeAgo(String? ts) { + if (ts == null) return "invalid date"; + final timestamp = int.tryParse(ts) ?? 0; + final DateTime eventTime = + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) + .subtract(Duration(hours: 36)); + return timeago.format(eventTime, allowFromNow: true); } } diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart new file mode 100644 index 00000000..4b3d4081 --- /dev/null +++ b/lib/data/models/order.dart @@ -0,0 +1,117 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; + +class Order implements Content { + final String? id; + final OrderType kind; + final Status status; + final int amount; + final String fiatCode; + final int? minAmount; + final int? maxAmount; + final int fiatAmount; + final String paymentMethod; + final int premium; + final String? masterBuyerPubkey; + final String? masterSellerPubkey; + final String? buyerInvoice; + final int? createdAt; + final int? expiresAt; + final int? buyerToken; + final int? sellerToken; + + Order({ + this.id, + required this.kind, + this.status = Status.pending, + this.amount = 0, + required this.fiatCode, + this.minAmount, + this.maxAmount, + required this.fiatAmount, + required this.paymentMethod, + required this.premium, + this.masterBuyerPubkey, + this.masterSellerPubkey, + this.buyerInvoice, + this.createdAt, + this.expiresAt, + this.buyerToken, + this.sellerToken, + }); + + @override + Map toJson() { + final data = { + type: { + 'kind': kind.value, + 'status': status.value, + 'amount': amount, + 'fiat_code': fiatCode, + 'fiat_amount': fiatAmount, + 'payment_method': paymentMethod, + 'premium': premium, + } + }; + + if (id != null) data[type]['id'] = id; + if (minAmount != null) data[type]['min_amount'] = minAmount; + if (maxAmount != null) data[type]['max_amount'] = maxAmount; + if (masterBuyerPubkey != null) { + data[type]['master_buyer_pubkey'] = masterBuyerPubkey; + } + if (masterSellerPubkey != null) { + data[type]['master_seller_pubkey'] = masterSellerPubkey; + } + if (buyerInvoice != null) data[type]['buyer_invoice'] = buyerInvoice; + if (createdAt != null) data[type]['created_at'] = createdAt; + if (expiresAt != null) data[type]['expires_at'] = expiresAt; + if (buyerToken != null) data[type]['buyer_token'] = buyerToken; + if (sellerToken != null) data[type]['seller_token'] = sellerToken; + + return data; + } + + factory Order.fromJson(Map json) { + return Order( + id: json['id'] as String?, + kind: OrderType.fromString(json['kind'] as String), + status: Status.fromString(json['status'] as String), + amount: json['amount'] as int, + fiatCode: json['fiat_code'] as String, + minAmount: json['min_amount'] as int?, + maxAmount: json['max_amount'] as int?, + fiatAmount: json['fiat_amount'] as int, + paymentMethod: json['payment_method'] as String, + premium: json['premium'] as int, + masterBuyerPubkey: json['master_buyer_pubkey'] as String?, + masterSellerPubkey: json['master_seller_pubkey'] as String?, + buyerInvoice: json['buyer_invoice'] as String?, + createdAt: json['created_at'] as int?, + expiresAt: json['expires_at'] as int?, + buyerToken: json['buyer_token'] as int?, + sellerToken: json['seller_token'] as int?, + ); + } + + factory Order.fromEvent(NostrEvent event) { + return Order( + id: event.orderId, + kind: OrderType.fromString(event.orderType!), + status: Status.fromString(event.status!), + amount: event.amount as int, + fiatCode: event.currency!, + fiatAmount: int.parse(event.fiatAmount!), + paymentMethod: event.paymentMethods.join(','), + premium: event.premium as int, + createdAt: event.createdAt as int, + expiresAt: event.expiration as int?, + ); + } + + @override + String get type => 'order'; +} diff --git a/lib/data/models/order_model.dart b/lib/data/models/order_model.dart index bc192115..89aa32af 100644 --- a/lib/data/models/order_model.dart +++ b/lib/data/models/order_model.dart @@ -161,7 +161,7 @@ class OrderModel { id: getString('d'), type: getString('k'), user: name, - rating: parseRating(getString('rating')), + rating: 0, ratingCount: getInt('rating_count'), amount: getInt('amt'), currency: getString('f'), @@ -181,7 +181,6 @@ class OrderModel { buyerFiatAmount: getDouble('buyer_fiat_amount'), ); } catch (e) { - print('Error creating OrderModel from tags: $e'); throw const FormatException('Invalid tags format for OrderModel'); } } diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart new file mode 100644 index 00000000..4f8d677f --- /dev/null +++ b/lib/data/models/payment_request.dart @@ -0,0 +1,27 @@ +import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/order.dart'; + +class PaymentRequest implements Content { + final Order? order; + final String? lnInvoice; + final int? amount; + + PaymentRequest( + {required this.order, required this.lnInvoice, required this.amount}); + + @override + Map toJson() { + final result = { + type: [order?.toJson(), lnInvoice] + }; + + if (amount != null) { + result[type]!.add(amount); + } + + return result; + } + + @override + String get type => 'payment_request'; +} diff --git a/lib/data/models/peer.dart b/lib/data/models/peer.dart new file mode 100644 index 00000000..ab167c5a --- /dev/null +++ b/lib/data/models/peer.dart @@ -0,0 +1,25 @@ +import 'package:mostro_mobile/data/models/content.dart'; + +class Peer implements Content { + final String publicKey; + + Peer({required this.publicKey}); + + factory Peer.fromJson(Map json) { + return Peer( + publicKey: json['pubkey'] as String, + ); + } + + @override + Map toJson() { + return { + type: { + 'pubkey': publicKey, + } + }; + } + + @override + String get type => 'Peer'; +} diff --git a/lib/data/models/session.dart b/lib/data/models/session.dart new file mode 100644 index 00000000..ed004033 --- /dev/null +++ b/lib/data/models/session.dart @@ -0,0 +1,40 @@ +import 'package:dart_nostr/dart_nostr.dart'; + +/// Represents a User session +/// +/// This class is used to store details of a user session +/// which is connected to a single Buy/Sell order and is +/// persisted for a maximum of 48hrs or until the order is +/// completed +class Session { + final String sessionId; + final DateTime startTime; + final NostrKeyPairs keyPair; + String? eventId; + + Session({ + required this.sessionId, + required this.startTime, + required this.keyPair, + this.eventId, + }); + + Map toJson() => { + 'sessionId': sessionId, + 'startTime': startTime.toIso8601String(), + 'privateKey': keyPair.private, + 'eventId' : eventId, + }; + + factory Session.fromJson(Map json) { + return Session( + sessionId: json['sessionId'], + startTime: DateTime.parse(json['startTime']), + keyPair: NostrKeyPairs(private: json['privateKey']), + eventId: json['eventId'], + ); + } + + String get privateKey => keyPair.private; + String get publicKey => keyPair.public; +} diff --git a/lib/data/models/text_message.dart b/lib/data/models/text_message.dart new file mode 100644 index 00000000..8ce7ae0a --- /dev/null +++ b/lib/data/models/text_message.dart @@ -0,0 +1,17 @@ +import 'package:mostro_mobile/data/models/content.dart'; + +class TextMessage implements Content { + final String message; + + TextMessage({required this.message}); + + @override + Map toJson() { + return { + type: message, + }; + } + + @override + String get type => 'text_message'; +} diff --git a/lib/data/models/user_model.dart b/lib/data/models/user_model.dart deleted file mode 100644 index bc59cc33..00000000 --- a/lib/data/models/user_model.dart +++ /dev/null @@ -1,17 +0,0 @@ -class User { - final String publicKey; - - User({required this.publicKey}); - - factory User.fromJson(Map json) { - return User( - publicKey: json['publicKey'] as String, - ); - } - - Map toJson() { - return { - 'publicKey': publicKey, - }; - } -} diff --git a/lib/data/repositories/mostro_order_repository.dart b/lib/data/repositories/mostro_order_repository.dart deleted file mode 100644 index bd3f4006..00000000 --- a/lib/data/repositories/mostro_order_repository.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:mostro_mobile/data/models/order_model.dart'; -import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; - -class MostroOrderRepository implements OrderRepository { - final MostroService _mostroService; - - MostroOrderRepository(this._mostroService); - - @override - Future createOrder(OrderModel order) async { - await _mostroService.publishOrder(order); - } - - @override - Future cancelOrder(String orderId) async { - await _mostroService.cancelOrder(orderId); - } - - @override - Future takeSellOrder(String orderId, {int? amount}) async { - await _mostroService.takeSellOrder(orderId, amount: amount); - } - - @override - Future takeBuyOrder(String orderId, {int? amount}) async { - await _mostroService.takeBuyOrder(orderId, amount: amount); - } - - @override - Stream getPendingOrders() { - return _mostroService.subscribeToOrders().where((order) => order.status == 'pending'); - } - - @override - Future sendFiatSent(String orderId) async { - await _mostroService.sendFiatSent(orderId); - } - - @override - Future releaseOrder(String orderId) async { - await _mostroService.releaseOrder(orderId); - } -} \ No newline at end of file diff --git a/lib/data/repositories/mostro_repository.dart b/lib/data/repositories/mostro_repository.dart new file mode 100644 index 00000000..b9fb4e1c --- /dev/null +++ b/lib/data/repositories/mostro_repository.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'package:dart_nostr/nostr/model/request/filter.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; + +class MostroRepository implements OrderRepository { + + // final NostrService _nostrService; + + final StreamController> _eventStreamController = + StreamController.broadcast(); + + final Map _orders = {}; + //final SecureStorageManager _secureStorageManager; + + MostroRepository(); + + void subscribe(NostrFilter filter) { + //_nostrService.subscribeToEvents(filter).listen((event) async { + + // final recipient = event.recipient; + + // final session = await _secureStorageManager.loadSession(recipient!); + + // final form = await decryptMessage( + // event.content!, session!.privateKey, event.pubkey); + + //final message = MostroMessage.deserialized(form); + //_orders[message.requestId!] = message; + //_eventStreamController.add(_orders.values.toList()); + //}); + } + + MostroMessage? getOrder(String orderId) { + return _orders[orderId]; + } + + +} diff --git a/lib/data/repositories/open_orders_repository.dart b/lib/data/repositories/open_orders_repository.dart new file mode 100644 index 00000000..ceb3cb28 --- /dev/null +++ b/lib/data/repositories/open_orders_repository.dart @@ -0,0 +1,29 @@ +import 'dart:async'; +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:dart_nostr/nostr/model/request/filter.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; + +class OpenOrdersRepository implements OrderRepository { + final NostrService _nostrService; + final StreamController> _eventStreamController = + StreamController.broadcast(); + final Map _events = {}; + + Stream> get eventsStream => _eventStreamController.stream; + + OpenOrdersRepository(this._nostrService); + + void subscribe(NostrFilter filter) { + _nostrService.subscribeToEvents(filter).listen((event) { + final key = '${event.kind}-${event.pubkey}-${event.orderId}'; + _events[key] = event; + _eventStreamController.add(_events.values.toList()); + }); + } + + void dispose() { + _eventStreamController.close(); + } +} diff --git a/lib/data/repositories/order_repository_interface.dart b/lib/data/repositories/order_repository_interface.dart index 9ccf4a5f..cb38e3b5 100644 --- a/lib/data/repositories/order_repository_interface.dart +++ b/lib/data/repositories/order_repository_interface.dart @@ -1,17 +1,3 @@ -import 'package:mostro_mobile/data/models/order_model.dart'; - abstract class OrderRepository { - Future createOrder(OrderModel order); - - Future cancelOrder(String orderId); - - Future takeSellOrder(String orderId, {int? amount}); - - Future takeBuyOrder(String orderId, {int? amount}); - - Stream getPendingOrders(); - - Future sendFiatSent(String orderId); - Future releaseOrder(String orderId); } diff --git a/lib/data/repositories/secure_storage_manager.dart b/lib/data/repositories/secure_storage_manager.dart new file mode 100644 index 00000000..17c69903 --- /dev/null +++ b/lib/data/repositories/secure_storage_manager.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:mostro_mobile/data/models/session.dart'; +import 'package:mostro_mobile/core/utils/nostr_utils.dart'; + +class SecureStorageManager { + Timer? _cleanupTimer; + final int sessionExpirationHours = 48; + + SecureStorageManager() { + _initializeCleanup(); + } + + Future newSession() async { + final keys = NostrUtils.generateKeyPair(); + final session = Session( + sessionId: keys.public, + startTime: DateTime.now(), + keyPair: keys, + ); + await saveSession(session); + return session; + } + + Future saveSession(Session session) async { + final prefs = await SharedPreferences.getInstance(); + String sessionJson = jsonEncode(session.toJson()); + await prefs.setString(session.sessionId, sessionJson); + } + + Future loadSession(String sessionId) async { + final prefs = await SharedPreferences.getInstance(); + String? sessionJson = prefs.getString(sessionId); + if (sessionJson != null) { + return Session.fromJson(jsonDecode(sessionJson)); + } + return null; + } + + Future deleteSession(String sessionId) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(sessionId); + } + + Future clearExpiredSessions() async { + final prefs = await SharedPreferences.getInstance(); + final now = DateTime.now(); + final allKeys = prefs.getKeys(); + + for (var key in allKeys) { + final sessionJson = prefs.getString(key); + if (sessionJson != null) { + final session = Session.fromJson(jsonDecode(sessionJson)); + if (now.difference(session.startTime).inHours >= sessionExpirationHours) { + await prefs.remove(key); + } + } + } + } + + void _initializeCleanup() { + clearExpiredSessions(); + _cleanupTimer = Timer.periodic(Duration(minutes: 30), (timer) { + clearExpiredSessions(); + }); + } + + void dispose() { + _cleanupTimer?.cancel(); + } +} diff --git a/lib/main.dart b/lib/main.dart index 721137cb..cd52c269 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mostro_mobile/data/repositories/mostro_order_repository.dart'; -import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; import 'package:mostro_mobile/presentation/auth/bloc/auth_state.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/providers/riverpod_providers.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:mostro_mobile/core/routes/app_routes.dart'; @@ -20,35 +20,29 @@ void main() async { final nostrService = NostrService(); await nostrService.init(); - final mostroService = MostroService(nostrService); - - final orderRepository = MostroOrderRepository(mostroService); - final prefs = await SharedPreferences.getInstance(); final isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true; final biometricsHelper = BiometricsHelper(); - runApp(MyApp( - isFirstLaunch: isFirstLaunch, - orderRepository: orderRepository, - biometricsHelper: biometricsHelper)); + runApp(ProviderScope( + child: MyApp( + isFirstLaunch: isFirstLaunch, biometricsHelper: biometricsHelper))); } -class MyApp extends StatelessWidget { +class MyApp extends ConsumerWidget { final bool isFirstLaunch; final BiometricsHelper biometricsHelper; - final OrderRepository orderRepository; const MyApp({ super.key, required this.isFirstLaunch, - required this.orderRepository, required this.biometricsHelper, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final homeBloc = ref.watch(homeBlocProvider); return MultiBlocProvider( providers: [ BlocProvider( @@ -59,7 +53,7 @@ class MyApp extends StatelessWidget { ), ), BlocProvider( - create: (context) => HomeBloc(orderRepository), + create: (context) => homeBloc, ), BlocProvider( create: (context) => ChatListBloc(), @@ -81,7 +75,7 @@ class MyApp extends StatelessWidget { title: 'Mostro', theme: ThemeData( primarySwatch: Colors.blue, - scaffoldBackgroundColor: const Color(0xFF1D212C), + scaffoldBackgroundColor: AppTheme.dark1, ), initialRoute: isFirstLaunch ? AppRoutes.welcome : AppRoutes.home, routes: AppRoutes.routes, diff --git a/lib/presentation/add_order/bloc/add_order_bloc.dart b/lib/presentation/add_order/bloc/add_order_bloc.dart index 3a8489cb..94bd80db 100644 --- a/lib/presentation/add_order/bloc/add_order_bloc.dart +++ b/lib/presentation/add_order/bloc/add_order_bloc.dart @@ -1,9 +1,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; import 'add_order_event.dart'; import 'add_order_state.dart'; class AddOrderBloc extends Bloc { - AddOrderBloc() : super(const AddOrderState()) { + final MostroService mostroService; + + AddOrderBloc(this.mostroService) : super(const AddOrderState()) { on(_onChangeOrderType); on(_onSubmitOrder); } @@ -12,8 +15,12 @@ class AddOrderBloc extends Bloc { emit(state.copyWith(currentType: event.orderType)); } - void _onSubmitOrder(SubmitOrder event, Emitter emit) { - // For now, just emit a success state - emit(state.copyWith(status: AddOrderStatus.success)); + void _onSubmitOrder(SubmitOrder event, Emitter emit) async { + + emit(state.copyWith(status: AddOrderStatus.submitting)); + + await mostroService.publishOrder(event.order); + + emit(state.copyWith(status: AddOrderStatus.submitted)); } } diff --git a/lib/presentation/add_order/bloc/add_order_event.dart b/lib/presentation/add_order/bloc/add_order_event.dart index 0a5cc3f6..87d00023 100644 --- a/lib/presentation/add_order/bloc/add_order_event.dart +++ b/lib/presentation/add_order/bloc/add_order_event.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/order.dart'; abstract class AddOrderEvent extends Equatable { const AddOrderEvent(); @@ -19,7 +20,7 @@ class ChangeOrderType extends AddOrderEvent { class SubmitOrder extends AddOrderEvent { final String fiatCode; - final double fiatAmount; + final int fiatAmount; final int satsAmount; final String paymentMethod; final OrderType orderType; @@ -32,6 +33,14 @@ class SubmitOrder extends AddOrderEvent { required this.orderType, }); + Order get order => Order( + kind: orderType, + fiatCode: fiatCode, + fiatAmount: fiatAmount, + paymentMethod: paymentMethod, + premium: 0); + @override - List get props => [fiatCode, fiatAmount, satsAmount, paymentMethod, orderType]; -} \ No newline at end of file + List get props => + [fiatCode, fiatAmount, satsAmount, paymentMethod, orderType]; +} diff --git a/lib/presentation/add_order/bloc/add_order_state.dart b/lib/presentation/add_order/bloc/add_order_state.dart index ea8def45..c761042a 100644 --- a/lib/presentation/add_order/bloc/add_order_state.dart +++ b/lib/presentation/add_order/bloc/add_order_state.dart @@ -1,17 +1,19 @@ import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; -enum AddOrderStatus { initial, loading, success, failure } +enum AddOrderStatus { initial, loading, success, submitting, submitted, failure } class AddOrderState extends Equatable { final OrderType currentType; final AddOrderStatus status; final String? errorMessage; + final String? currency; const AddOrderState({ this.currentType = OrderType.sell, this.status = AddOrderStatus.initial, this.errorMessage, + this.currency, }); AddOrderState copyWith({ @@ -27,5 +29,5 @@ class AddOrderState extends Equatable { } @override - List get props => [currentType, status, errorMessage]; -} \ No newline at end of file + List get props => [currentType, status, errorMessage, currency]; +} diff --git a/lib/presentation/add_order/screens/add_order_screen.dart b/lib/presentation/add_order/screens/add_order_screen.dart index cf92c76e..0b826801 100644 --- a/lib/presentation/add_order/screens/add_order_screen.dart +++ b/lib/presentation/add_order/screens/add_order_screen.dart @@ -1,52 +1,67 @@ +import 'package:bitcoin_icons/bitcoin_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_bloc.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_event.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_state.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; -import 'package:mostro_mobile/presentation/widgets/bottom_nav_bar.dart'; -import 'package:mostro_mobile/presentation/widgets/custom_app_bar.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_dropdown.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; +import 'package:mostro_mobile/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/providers/riverpod_providers.dart'; + +class AddOrderScreen extends ConsumerWidget { + final _formKey = GlobalKey(); -class AddOrderScreen extends StatelessWidget { AddOrderScreen({super.key}); - final _fiatCodeController = TextEditingController(); final _fiatAmountController = TextEditingController(); final _satsAmountController = TextEditingController(); final _paymentMethodController = TextEditingController(); final _lightningInvoiceController = TextEditingController(); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final orderRepo = ref.watch(mostroServiceProvider); + return BlocProvider( - create: (context) => AddOrderBloc(), + create: (context) => AddOrderBloc(orderRepo), child: BlocBuilder( builder: (context, state) { return Scaffold( - backgroundColor: const Color(0xFF1D212C), - appBar: const CustomAppBar(), + backgroundColor: AppTheme.dark1, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: + const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.cream1), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + 'NEW ORDER', + style: TextStyle( + color: AppTheme.cream1, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, + ), + ), + ), body: Column( children: [ Expanded( child: Container( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 16), + margin: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF303544), + color: AppTheme.dark2, borderRadius: BorderRadius.circular(20), ), - child: Column( - children: [ - _buildTabs(context, state), - Expanded( - child: state.currentType == OrderType.sell - ? _buildSellForm(context) - : _buildBuyForm(context), - ), - ], - ), + child: _buildContent(context, state, ref), ), ), - const BottomNavBar(), ], ), ); @@ -55,10 +70,71 @@ class AddOrderScreen extends StatelessWidget { ); } + Widget _buildContent( + BuildContext context, AddOrderState state, WidgetRef ref) { + if (state.status == AddOrderStatus.submitting) { + return Center(child: CircularProgressIndicator()); + } else if (state.status == AddOrderStatus.submitted) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Your offer has been published! Please wait until another user picks your order. It will be available for expiration_hours hours. You can cancel this order before another user picks it up by executing: cancel.', + style: TextStyle(fontSize: 18, color: AppTheme.cream1), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => + Navigator.of(context).pop(), + child: const Text('Back to Home'), + ), + ], + ), + ); + } else if (state.status == AddOrderStatus.failure) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Order failed: ${state.errorMessage}', + style: const TextStyle(fontSize: 18, color: Colors.redAccent), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => + Navigator.of(context).pop(), + child: const Text('Back to Home'), + ), + ], + ), + ); + } else { + return Form( + key: _formKey, + child: Column( + children: [ + _buildTabs(context, state), + Expanded( + child: state.currentType == OrderType.sell + ? _buildSellForm(context, ref) + : _buildBuyForm(context, ref), + ), + ], + ), + ); + } + } + Widget _buildTabs(BuildContext context, AddOrderState state) { return Container( decoration: const BoxDecoration( - color: Color(0xFF1D212C), + color: AppTheme.dark1, borderRadius: BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -88,7 +164,7 @@ class AddOrderScreen extends StatelessWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( - color: isActive ? const Color(0xFF303544) : const Color(0xFF1D212C), + color: isActive ? AppTheme.dark2 : AppTheme.dark1, borderRadius: BorderRadius.only( topLeft: Radius.circular(isActive ? 20 : 0), topRight: Radius.circular(isActive ? 20 : 0), @@ -98,7 +174,7 @@ class AddOrderScreen extends StatelessWidget { text, textAlign: TextAlign.center, style: TextStyle( - color: isActive ? Colors.white : Colors.grey, + color: isActive ? AppTheme.cream1 : AppTheme.grey2, fontWeight: FontWeight.bold, ), ), @@ -106,99 +182,87 @@ class AddOrderScreen extends StatelessWidget { ); } - Widget _buildSellForm(BuildContext context) { + Widget _buildSellForm(BuildContext context, WidgetRef ref) { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Make sure your order is below 20K sats', - style: TextStyle(color: Colors.grey)), + style: TextStyle(color: AppTheme.grey2)), const SizedBox(height: 16), - _buildDropdownField('Fiat code'), + CurrencyDropdown(label: 'Fiat code'), const SizedBox(height: 16), - _buildTextField('Fiat amount', _fiatAmountController), + CurrencyTextField( + controller: _fiatAmountController, label: 'Fiat amount'), const SizedBox(height: 16), _buildFixedToggle(), const SizedBox(height: 16), _buildTextField('Sats amount', _satsAmountController, - suffix: Icons.menu), + suffix: Icon(BitcoinIcons.satoshi_v1_outline).icon), const SizedBox(height: 16), _buildTextField('Payment method', _paymentMethodController), const SizedBox(height: 32), - _buildActionButtons(context), + _buildActionButtons(context, ref, OrderType.sell), ], ), ); } - Widget _buildBuyForm(BuildContext context) { + Widget _buildBuyForm(BuildContext context, WidgetRef ref) { return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Make sure your order is below 20K sats', - style: TextStyle(color: Colors.grey)), + style: TextStyle(color: AppTheme.grey2)), const SizedBox(height: 16), - _buildDropdownField('Fiat code'), + CurrencyDropdown(label: 'Fiat code'), const SizedBox(height: 16), - _buildTextField('Fiat amount', _fiatAmountController), + CurrencyTextField( + controller: _fiatAmountController, label: 'Fiat amount'), const SizedBox(height: 16), _buildFixedToggle(), const SizedBox(height: 16), _buildTextField('Sats amount', _satsAmountController, - suffix: Icons.menu), + suffix: Icon(BitcoinIcons.satoshi_v1_outline).icon), const SizedBox(height: 16), _buildTextField('Lightning Invoice without an amount', _lightningInvoiceController), const SizedBox(height: 16), _buildTextField('Payment method', _paymentMethodController), const SizedBox(height: 32), - _buildActionButtons(context), + _buildActionButtons(context, ref, OrderType.buy), ], ), ); } - Widget _buildDropdownField(String label) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: const Color(0xFF1D212C), - borderRadius: BorderRadius.circular(8), - ), - child: DropdownButtonFormField( - decoration: InputDecoration( - border: InputBorder.none, - labelText: label, - labelStyle: const TextStyle(color: Colors.grey), - ), - dropdownColor: const Color(0xFF1D212C), - style: const TextStyle(color: Colors.white), - items: const [], // Add your fiat code options here - onChanged: (value) {}, - ), - ); - } - Widget _buildTextField(String label, TextEditingController controller, {IconData? suffix}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFF1D212C), + color: AppTheme.dark1, borderRadius: BorderRadius.circular(8), ), - child: TextField( + child: TextFormField( controller: controller, - style: const TextStyle(color: Colors.white), + style: const TextStyle(color: AppTheme.cream1), decoration: InputDecoration( border: InputBorder.none, labelText: label, - labelStyle: const TextStyle(color: Colors.grey), - suffixIcon: suffix != null ? Icon(suffix, color: Colors.grey) : null, + labelStyle: const TextStyle(color: AppTheme.grey2), + suffixIcon: + suffix != null ? Icon(suffix, color: AppTheme.grey2) : null, ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, ), ); } @@ -206,47 +270,54 @@ class AddOrderScreen extends StatelessWidget { Widget _buildFixedToggle() { return Row( children: [ - const Text('Fixed', style: TextStyle(color: Colors.white)), + const Text('Fixed', style: TextStyle(color: AppTheme.cream1)), const SizedBox(width: 8), Switch( - value: false, // You should manage this state in the bloc + value: false, onChanged: (value) { - // Update the state in the bloc + // Update the state in the bloc if necessary }, ), ], ); } - Widget _buildActionButtons(BuildContext context) { + Widget _buildActionButtons( + BuildContext context, WidgetRef ref, OrderType orderType) { return Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('CANCEL', style: TextStyle(color: Colors.orange)), + onPressed: () => Navigator.of(context).pop(), + child: const Text('CANCEL', style: TextStyle(color: AppTheme.red2)), ), const SizedBox(width: 16), ElevatedButton( onPressed: () { - // For now, just print the values and close the screen - print('Fiat Code: ${_fiatCodeController.text}'); - print('Fiat Amount: ${_fiatAmountController.text}'); - print('Sats Amount: ${_satsAmountController.text}'); - print('Payment Method: ${_paymentMethodController.text}'); - if (_lightningInvoiceController.text.isNotEmpty) { - print('Lightning Invoice: ${_lightningInvoiceController.text}'); + if (_formKey.currentState?.validate() ?? false) { + _submitOrder(context, ref, orderType); } - Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF8CC541), + backgroundColor: AppTheme.mostroGreen, ), child: const Text('SUBMIT'), ), ], ); } + + void _submitOrder(BuildContext context, WidgetRef ref, OrderType orderType) { + final selectedFiatCode = ref.read(selectedFiatCodeProvider); + + if (_formKey.currentState?.validate() ?? false) { + context.read().add(SubmitOrder( + fiatCode: selectedFiatCode ?? '', // Use selected fiat code + fiatAmount: int.tryParse(_fiatAmountController.text) ?? 0, + satsAmount: int.tryParse(_satsAmountController.text) ?? 0, + paymentMethod: _paymentMethodController.text, + orderType: orderType, + )); + } + } } diff --git a/lib/presentation/chat_list/screens/chat_list_screen.dart b/lib/presentation/chat_list/screens/chat_list_screen.dart index 5f721266..0d11ecdb 100644 --- a/lib/presentation/chat_list/screens/chat_list_screen.dart +++ b/lib/presentation/chat_list/screens/chat_list_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:mostro_mobile/data/models/chat_model.dart'; import 'package:mostro_mobile/presentation/chat_list/bloc/chat_list_bloc.dart'; import 'package:mostro_mobile/presentation/chat_list/bloc/chat_list_event.dart'; @@ -25,14 +26,15 @@ class ChatListScreen extends StatelessWidget { ), child: Column( children: [ - const Padding( - padding: EdgeInsets.all(16.0), + Padding( + padding: const EdgeInsets.all(16.0), child: Text( 'Chats', style: TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, ), ), ), diff --git a/lib/presentation/home/bloc/home_bloc.dart b/lib/presentation/home/bloc/home_bloc.dart index 2a29198d..23cadca9 100644 --- a/lib/presentation/home/bloc/home_bloc.dart +++ b/lib/presentation/home/bloc/home_bloc.dart @@ -1,16 +1,14 @@ import 'dart:async'; - import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/order_model.dart'; -import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; import 'package:mostro_mobile/presentation/home/bloc/home_event.dart'; import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; class HomeBloc extends Bloc { - final OrderRepository orderRepository; StreamSubscription? ordersSubscription; - HomeBloc(this.orderRepository) : super(HomeState.initial()) { + HomeBloc() : super(HomeState.initial()) { on(_onLoadOrders); on(_onChangeOrderType); on(_onOrderReceived); @@ -18,21 +16,6 @@ class HomeBloc extends Bloc { } Future _onLoadOrders(LoadOrders event, Emitter emit) async { - emit(state.copyWith(status: HomeStatus.loading)); - - await ordersSubscription?.cancel(); - - ordersSubscription = orderRepository.getPendingOrders().listen( - (order) { - add(OrderReceived(order)); - }, - onError: (error) { - add(OrdersError(error.toString())); - }, - onDone: () { - // EOSE - }, - ); } void _onOrderReceived(OrderReceived event, Emitter emit) { diff --git a/lib/presentation/home/bloc/home_event.dart b/lib/presentation/home/bloc/home_event.dart index d6a45d75..e4bd90cf 100644 --- a/lib/presentation/home/bloc/home_event.dart +++ b/lib/presentation/home/bloc/home_event.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/order_model.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; abstract class HomeEvent extends Equatable { const HomeEvent(); diff --git a/lib/presentation/home/bloc/home_state.dart b/lib/presentation/home/bloc/home_state.dart index 81792c85..18916a94 100644 --- a/lib/presentation/home/bloc/home_state.dart +++ b/lib/presentation/home/bloc/home_state.dart @@ -1,10 +1,9 @@ import 'package:equatable/equatable.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/order_model.dart'; enum HomeStatus { initial, loading, loaded, error } -enum OrderType { buy, sell } - class HomeState extends Equatable { final HomeStatus status; final List allOrders; @@ -47,5 +46,6 @@ class HomeState extends Equatable { } @override - List get props => [status, allOrders, filteredOrders, orderType, errorMessage]; -} \ No newline at end of file + List get props => + [status, allOrders, filteredOrders, orderType, errorMessage]; +} diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index 1a0d8f7f..dd99797f 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -1,21 +1,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/presentation/home/bloc/home_bloc.dart'; import 'package:mostro_mobile/presentation/home/bloc/home_event.dart'; import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; import 'package:mostro_mobile/presentation/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/presentation/widgets/custom_app_bar.dart'; +import 'package:mostro_mobile/presentation/widgets/order_filter.dart'; import 'package:mostro_mobile/presentation/widgets/order_list.dart'; -import '../bloc/home_bloc.dart'; +import 'package:mostro_mobile/providers/event_store_providers.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends ConsumerWidget { const HomeScreen({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { // Cargar las órdenes cuando se inicia la pantalla context.read().add(LoadOrders()); - return Scaffold( backgroundColor: const Color(0xFF1D212C), appBar: const CustomAppBar(), @@ -33,10 +37,10 @@ class HomeScreen extends StatelessWidget { child: Column( children: [ _buildTabs(), - _buildFilterButton(), + _buildFilterButton(context), const SizedBox(height: 6.0), Expanded( - child: _buildOrderList(), + child: _buildOrderList(ref), ), const BottomNavBar(), ], @@ -101,13 +105,24 @@ class HomeScreen extends StatelessWidget { ); } - Widget _buildFilterButton() { + Widget _buildFilterButton(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( children: [ OutlinedButton.icon( - onPressed: () {}, + onPressed: () { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: OrderFilter(), + ); + }, + ); + }, icon: const HeroIcon(HeroIcons.funnel, style: HeroIconStyle.outline, color: Colors.white), label: const Text("FILTER", style: TextStyle(color: Colors.white)), @@ -132,27 +147,29 @@ class HomeScreen extends StatelessWidget { ); } - Widget _buildOrderList() { - return BlocBuilder( - builder: (context, state) { - if (state.status == HomeStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } else if (state.status == HomeStatus.loaded) { - if (state.filteredOrders.isEmpty) { - return const Center( - child: Text( - 'No orders available for this type', - style: TextStyle(color: Colors.white), - ), - ); - } - return OrderList(orders: state.filteredOrders); - } else { - return const Center( - child: Text('Error loading orders', - style: TextStyle(color: Colors.white))); - } - }, - ); + Widget _buildOrderList(WidgetRef ref) { + final orderEventsAsync = ref.watch(orderEventsProvider); + + return orderEventsAsync.when( + data: (events) { + return BlocBuilder( + builder: (context, state) { + if (events.isEmpty) { + return const Center( + child: Text( + 'No orders available for this type', + style: TextStyle(color: Colors.white), + ), + ); + } + return OrderList( + orders: events + .where((evt) => evt.orderType == state.orderType.value) + .toList()); + }, + ); + }, + loading: () => Center(child: CircularProgressIndicator()), + error: (error, stack) => Center(child: Text('Error: $error'))); } } diff --git a/lib/presentation/order/bloc/order_details_bloc.dart b/lib/presentation/order/bloc/order_details_bloc.dart index dbef6bbc..c71a4075 100644 --- a/lib/presentation/order/bloc/order_details_bloc.dart +++ b/lib/presentation/order/bloc/order_details_bloc.dart @@ -1,9 +1,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_state.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; class OrderDetailsBloc extends Bloc { - OrderDetailsBloc() : super(const OrderDetailsState()) { + final MostroService mostroService; + + OrderDetailsBloc(this.mostroService) : super(const OrderDetailsState()) { on(_onLoadOrderDetails); on(_onCancelOrder); on(_onContinueOrder); @@ -12,17 +17,23 @@ class OrderDetailsBloc extends Bloc { void _onLoadOrderDetails( LoadOrderDetails event, Emitter emit) { emit(state.copyWith(status: OrderDetailsStatus.loading)); - // Aquí podrías cargar información adicional si fuera necesario emit(state.copyWith(status: OrderDetailsStatus.loaded, order: event.order)); } void _onCancelOrder(CancelOrder event, Emitter emit) { - // Implementar lógica para cancelar la orden - print('Cancelling order'); + emit(state.copyWith(status: OrderDetailsStatus.cancelled)); } - void _onContinueOrder(ContinueOrder event, Emitter emit) { - // Implementar lógica para continuar con la orden - print('Continuing with order'); + void _onContinueOrder( + ContinueOrder event, Emitter emit) async { + emit(state.copyWith(status: OrderDetailsStatus.loading)); + + if (event.order.orderType == OrderType.buy.value) { + await mostroService.takeBuyOrder(event.order.orderId!); + } else { + await mostroService.takeSellOrder(event.order.orderId!); + } + + emit(state.copyWith(status: OrderDetailsStatus.done)); } } diff --git a/lib/presentation/order/bloc/order_details_event.dart b/lib/presentation/order/bloc/order_details_event.dart index fad0ff68..81b343d3 100644 --- a/lib/presentation/order/bloc/order_details_event.dart +++ b/lib/presentation/order/bloc/order_details_event.dart @@ -1,5 +1,5 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; abstract class OrderDetailsEvent extends Equatable { const OrderDetailsEvent(); @@ -9,7 +9,7 @@ abstract class OrderDetailsEvent extends Equatable { } class LoadOrderDetails extends OrderDetailsEvent { - final OrderModel order; + final NostrEvent order; const LoadOrderDetails(this.order); @@ -19,4 +19,11 @@ class LoadOrderDetails extends OrderDetailsEvent { class CancelOrder extends OrderDetailsEvent {} -class ContinueOrder extends OrderDetailsEvent {} +class ContinueOrder extends OrderDetailsEvent { + final NostrEvent order; + + const ContinueOrder(this.order); + + @override + List get props => [order]; +} diff --git a/lib/presentation/order/bloc/order_details_state.dart b/lib/presentation/order/bloc/order_details_state.dart index 059faab4..9f11534f 100644 --- a/lib/presentation/order/bloc/order_details_state.dart +++ b/lib/presentation/order/bloc/order_details_state.dart @@ -1,11 +1,11 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; -enum OrderDetailsStatus { initial, loading, loaded, error } +enum OrderDetailsStatus { initial, loading, loaded, error, cancelled, done } class OrderDetailsState extends Equatable { final OrderDetailsStatus status; - final OrderModel? order; + final NostrEvent? order; final String? errorMessage; const OrderDetailsState({ @@ -16,7 +16,7 @@ class OrderDetailsState extends Equatable { OrderDetailsState copyWith({ OrderDetailsStatus? status, - OrderModel? order, + NostrEvent? order, String? errorMessage, }) { return OrderDetailsState( diff --git a/lib/presentation/order/screens/order_details_screen.dart b/lib/presentation/order/screens/order_details_screen.dart index 4292ba3f..fb272e25 100644 --- a/lib/presentation/order/screens/order_details_screen.dart +++ b/lib/presentation/order/screens/order_details_screen.dart @@ -1,41 +1,55 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_bloc.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_state.dart'; -import 'package:mostro_mobile/presentation/widgets/bottom_nav_bar.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; +import 'package:mostro_mobile/presentation/widgets/exchange_rate_widget.dart'; +import 'package:mostro_mobile/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/providers/riverpod_providers.dart'; -class OrderDetailsScreen extends StatelessWidget { - final OrderModel initialOrder; +class OrderDetailsScreen extends ConsumerWidget { + final NostrEvent initialOrder; - const OrderDetailsScreen({super.key, required this.initialOrder}); + final _satsAmountController = TextEditingController(); + + OrderDetailsScreen({super.key, required this.initialOrder}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final mostroService = ref.watch(mostroServiceProvider); return BlocProvider( create: (context) => - OrderDetailsBloc()..add(LoadOrderDetails(initialOrder)), + OrderDetailsBloc(mostroService)..add(LoadOrderDetails(initialOrder)), child: BlocBuilder( builder: (context, state) { if (state.status == OrderDetailsStatus.loading) { return const Center(child: CircularProgressIndicator()); - } - if (state.status == OrderDetailsStatus.error) { + } else if (state.status == OrderDetailsStatus.error) { return Center( child: Text(state.errorMessage ?? 'An error occurred')); + } else if (state.status == OrderDetailsStatus.loaded) { + return _buildContent(context, ref, state.order!); + } else if (state.status == OrderDetailsStatus.cancelled || + state.status == OrderDetailsStatus.done) { + return _buildCompletionMessage(context, state); } - if (state.order == null) { - return const Center(child: Text('Order not found')); - } - return _buildContent(context, state.order!); + return const Center(child: Text('Order not found')); }, ), ); } - Widget _buildContent(BuildContext context, OrderModel order) { + Widget _buildCompletionMessage( + BuildContext context, OrderDetailsState state) { + final message = state.status == OrderDetailsStatus.cancelled + ? 'Order has been cancelled.' + : 'Order has been completed!'; return Scaffold( backgroundColor: const Color(0xFF1D212C), appBar: AppBar( @@ -45,55 +59,105 @@ class OrderDetailsScreen extends StatelessWidget { icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), - title: - const Text('ORDER DETAILS', style: TextStyle(color: Colors.white)), - actions: [ - IconButton( - icon: const HeroIcon(HeroIcons.plus, color: Colors.white), - onPressed: () { - // Implementar lógica para añadir - }, + title: Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 20, ), - IconButton( - icon: const HeroIcon(HeroIcons.bolt, - style: HeroIconStyle.solid, color: Color(0xFF8CC541)), - onPressed: () { - // Implementar lógica para acción de rayo - }, - ), - ], + ), ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - _buildSellerInfo(order), - const SizedBox(height: 16), - _buildSellerAmount(order), - const SizedBox(height: 16), - _buildExchangeRate(order), - const SizedBox(height: 16), - _buildBuyerInfo(order), - const SizedBox(height: 16), - _buildBuyerAmount(order), - const SizedBox(height: 24), - _buildActionButtons(context), - ], + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, ), + textAlign: TextAlign.center, ), - ), + const SizedBox(height: 16), + const Text( + 'Thank you for using our service!', + style: TextStyle(color: Colors.grey), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF8CC541), + ), + child: const Text('Return to Main Screen'), + ), + ], ), - const BottomNavBar(), - ], + ), ), ); } - Widget _buildSellerInfo(OrderModel order) { + Widget _buildContent(BuildContext context, WidgetRef ref, NostrEvent order) { + return Scaffold( + backgroundColor: const Color(0xFF1D212C), + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text('${order.orderType?.toUpperCase()} BITCOIN', + style: TextStyle( + color: Colors.white, + fontFamily: GoogleFonts.robotoCondensed().fontFamily)), + ), + body: BlocConsumer( + listener: (context, state) { + if (state.status == OrderDetailsStatus.cancelled || + state.status == OrderDetailsStatus.done) { + Navigator.of(context).pop(); + } + }, builder: (context, state) { + return Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildSellerInfo(order), + const SizedBox(height: 16), + _buildSellerAmount(order, ref), + const SizedBox(height: 16), + ExchangeRateWidget(currency: order.currency!), + const SizedBox(height: 16), + _buildBuyerInfo(order), + const SizedBox(height: 16), + _buildBuyerAmount(order), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ), + ), + ), + ), + ], + ); + }), + ); + } + + Widget _buildSellerInfo(NostrEvent order) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -103,17 +167,18 @@ class OrderDetailsScreen extends StatelessWidget { child: Row( children: [ CircleAvatar( - backgroundImage: NetworkImage(order.sellerAvatar), + backgroundColor: Colors.grey, + child: Text('S', style: TextStyle(color: Colors.white)), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(order.sellerName, + Text(order.name!, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold)), - Text('${order.sellerRating}/5 (${order.sellerReviewCount})', + Text('${order.rating}/5 (_)', style: const TextStyle(color: Color(0xFF8CC541))), ], ), @@ -130,73 +195,55 @@ class OrderDetailsScreen extends StatelessWidget { ); } - Widget _buildSellerAmount(OrderModel order) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${order.fiatAmount} ${order.fiatCurrency} (${order.premium})', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold)), - Text('${order.satsAmount} sats', - style: const TextStyle(color: Colors.grey)), - const SizedBox(height: 8), - Row( - children: [ - const HeroIcon(HeroIcons.creditCard, - style: HeroIconStyle.outline, color: Colors.white, size: 16), - const SizedBox(width: 8), - Text(order.paymentMethod, - style: const TextStyle(color: Colors.white)), - ], + Widget _buildSellerAmount(NostrEvent order, WidgetRef ref) { + final exchangeRateAsyncValue = + ref.watch(exchangeRateProvider(order.currency!)); + return exchangeRateAsyncValue.when( + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (exchangeRate) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF303544), + borderRadius: BorderRadius.circular(12), ), - ], - ), - ); - } - - Widget _buildExchangeRate(OrderModel order) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text('1 BTC = \$ ${order.exchangeRate}', - style: const TextStyle(color: Colors.white)), - Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('price yado.io', style: TextStyle(color: Colors.grey)), - const SizedBox(width: 4), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: Colors.grey.withOpacity(0.3), - shape: BoxShape.circle, - ), - child: const HeroIcon(HeroIcons.arrowsUpDown, - style: HeroIconStyle.outline, - color: Colors.white, - size: 12), + Text('${order.fiatAmount} ${order.currency} (${order.premium}%)', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold)), + Text('${order.amount} sats', + style: const TextStyle(color: Colors.grey)), + const SizedBox(height: 8), + Row( + children: [ + const HeroIcon(HeroIcons.creditCard, + style: HeroIconStyle.outline, + color: Colors.white, + size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + order.paymentMethods[0], + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.visible, + softWrap: true, + ), + ), + ], ), ], ), - ], - ), + ); + }, ); } - Widget _buildBuyerInfo(OrderModel order) { + Widget _buildBuyerInfo(NostrEvent order) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -228,7 +275,7 @@ class OrderDetailsScreen extends StatelessWidget { ); } - Widget _buildBuyerAmount(OrderModel order) { + Widget _buildBuyerAmount(NostrEvent order) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -238,12 +285,9 @@ class OrderDetailsScreen extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('${order.buyerSatsAmount} sats', - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold)), - Text('\$ ${order.buyerFiatAmount}', + CurrencyTextField(controller: _satsAmountController, label: 'sats'), + const SizedBox(height: 8), + Text('\$ ${order.amount}', style: const TextStyle(color: Colors.grey)), const SizedBox(height: 8), const Row( @@ -280,7 +324,7 @@ class OrderDetailsScreen extends StatelessWidget { Expanded( child: ElevatedButton( onPressed: () { - context.read().add(ContinueOrder()); + context.read().add(ContinueOrder(initialOrder)); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF8CC541), diff --git a/lib/presentation/payment_qr/screens/payment_qr_screen.dart b/lib/presentation/payment_qr/screens/payment_qr_screen.dart index ea749b65..0da0c2e8 100644 --- a/lib/presentation/payment_qr/screens/payment_qr_screen.dart +++ b/lib/presentation/payment_qr/screens/payment_qr_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:qr_flutter/qr_flutter.dart'; import '../bloc/payment_qr_bloc.dart'; import '../bloc/payment_qr_event.dart'; import '../bloc/payment_qr_state.dart'; @@ -37,12 +36,6 @@ class PaymentQrScreen extends StatelessWidget { style: TextStyle(color: Colors.white), ), const SizedBox(height: 20), - QrImage( - data: state.qrData, - version: QrVersions.auto, - size: 200.0, - foregroundColor: Colors.white, - ), const SizedBox(height: 20), Text( 'Expires in: ${state.expiresIn}', diff --git a/lib/presentation/widgets/currency_dropdown.dart b/lib/presentation/widgets/currency_dropdown.dart new file mode 100644 index 00000000..86dbd0f8 --- /dev/null +++ b/lib/presentation/widgets/currency_dropdown.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/providers/exchange_service_provider.dart'; + +class CurrencyDropdown extends ConsumerWidget { + final String label; + + const CurrencyDropdown({ + super.key, + required this.label, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final currencyCodesAsync = ref.watch(currencyCodesProvider); + final selectedFiatCode = ref.watch(selectedFiatCodeProvider); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFF1D212C), + borderRadius: BorderRadius.circular(8), + ), + child: currencyCodesAsync.when( + loading: () => const Center( + child: SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator(), + ), + ), + error: (error, stackTrace) => Text('Error: $error'), + data: (currencyCodes) { + final items = currencyCodes.keys.map((code) { + return DropdownMenuItem( + value: code, + child: Text( + '$code - ${currencyCodes[code]}', + style: const TextStyle(color: Colors.white), + ), + ); + }).toList(); + + return DropdownButtonFormField( + decoration: InputDecoration( + border: InputBorder.none, + labelText: label, + labelStyle: const TextStyle(color: Colors.grey), + ), + dropdownColor: const Color(0xFF1D212C), + style: const TextStyle(color: Colors.white), + items: items, + value: selectedFiatCode, + onChanged: (value) => + ref.read(selectedFiatCodeProvider.notifier).state = value, + ); + }, + ), + ); + } +} diff --git a/lib/presentation/widgets/currency_text_field.dart b/lib/presentation/widgets/currency_text_field.dart new file mode 100644 index 00000000..488efd2e --- /dev/null +++ b/lib/presentation/widgets/currency_text_field.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:mostro_mobile/services/currency_input_formatter.dart'; + +class CurrencyTextField extends StatelessWidget { + final TextEditingController controller; + final String label; + + const CurrencyTextField( + {super.key, required this.controller, required this.label}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFF1D212C), + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + controller: controller, + keyboardType: TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + CurrencyInputFormatter(), + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')), + ], + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + border: InputBorder.none, + labelText: label, + labelStyle: const TextStyle(color: Colors.grey), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + ), + ); + } +} diff --git a/lib/presentation/widgets/custom_app_bar.dart b/lib/presentation/widgets/custom_app_bar.dart index 8944c74a..de620783 100644 --- a/lib/presentation/widgets/custom_app_bar.dart +++ b/lib/presentation/widgets/custom_app_bar.dart @@ -19,6 +19,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { ), actions: [ IconButton( + key: Key('createOrderButton'), icon: const HeroIcon(HeroIcons.plus, style: HeroIconStyle.outline, color: Colors.white), onPressed: () { diff --git a/lib/presentation/widgets/exchange_rate_widget.dart b/lib/presentation/widgets/exchange_rate_widget.dart new file mode 100644 index 00000000..8689ba3e --- /dev/null +++ b/lib/presentation/widgets/exchange_rate_widget.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/providers/exchange_service_provider.dart'; + +class ExchangeRateWidget extends ConsumerWidget { + final String currency; + + const ExchangeRateWidget({ + super.key, + required this.currency, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Watch the provider for the specific currency + final exchangeRateAsyncValue = ref.watch(exchangeRateProvider(currency)); + + return exchangeRateAsyncValue.when( + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (exchangeRate) { + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFF303544), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '1 BTC = ${exchangeRate.toStringAsFixed(2)} $currency', + style: const TextStyle(color: Colors.white), + ), + Row( + children: [ + Text('price in $currency', + style: const TextStyle(color: Colors.grey)), + const SizedBox(width: 4), + GestureDetector( + onTap: () { + // Trigger refresh for this specific currency + ref + .read(exchangeRateProvider(currency).notifier) + .fetchExchangeRate(currency); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.3), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.sync, + color: Colors.white, + size: 12, + ), + ), + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/presentation/widgets/group_box.dart b/lib/presentation/widgets/group_box.dart new file mode 100644 index 00000000..b2b8c473 --- /dev/null +++ b/lib/presentation/widgets/group_box.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; + +class GroupBox extends StatelessWidget { + final String title; + final Widget child; + + const GroupBox({ + super.key, + required this.title, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, // Allow overflow of title + children: [ + // The group box with content + Container( + padding: const EdgeInsets.all(12.0), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey, width: 1.5), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + ], + ), + ), + // The title widget overlapping the border + Positioned( + top: -10, // Adjust this value for how much you want to overlap + left: 10, // Adjust horizontal alignment if needed + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + color: Colors.white, // This covers the border underneath the title + child: Text( + title, + style: TextStyle( + color: AppTheme.mostroGreen, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/order_filter.dart b/lib/presentation/widgets/order_filter.dart new file mode 100644 index 00000000..15e8d16d --- /dev/null +++ b/lib/presentation/widgets/order_filter.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/presentation/widgets/group_box.dart'; + +class OrderFilter extends StatelessWidget { + const OrderFilter({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 300, + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.cream1, + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 5, + offset: Offset(0, 3), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const HeroIcon(HeroIcons.funnel, + style: HeroIconStyle.outline, color: AppTheme.dark2), + SizedBox(width: 8), + Text( + 'FILTER', + style: AppTheme.theme.textTheme.headlineSmall!.copyWith( + color: AppTheme.dark2, + ), + ), + ], + ), + IconButton( + icon: Icon(Icons.close, color: AppTheme.dark2, size: 20), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + SizedBox(height: 20), + buildDropdownSection(context, 'Fiat currencies', ''), + buildDropdownSection(context, 'Payment methods', ''), + buildDropdownSection(context, 'Countries', ''), + buildDropdownSection(context, 'Rating', ''), + ], + ), + ); + } + + Widget buildDropdownSection( + BuildContext context, String title, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 4), + Container( + padding: EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppTheme.grey), + ), + child: GroupBox( + title: title, + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + onChanged: (String? newValue) {}, + items: [value] + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle( + color: AppTheme.dark1, + fontSize: 14, + ), + ), + ); + }).toList(), + icon: Icon( + Icons.arrow_drop_down, + color: AppTheme.dark1, + ), + ), + )), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/order_list.dart b/lib/presentation/widgets/order_list.dart index 3e75e46a..4a165b81 100644 --- a/lib/presentation/widgets/order_list.dart +++ b/lib/presentation/widgets/order_list.dart @@ -1,9 +1,9 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; -import '../../data/models/order_model.dart'; import 'order_list_item.dart'; class OrderList extends StatelessWidget { - final List orders; + final List orders; const OrderList({super.key, required this.orders}); diff --git a/lib/presentation/widgets/order_list_item.dart b/lib/presentation/widgets/order_list_item.dart index 137c7670..255fd0ec 100644 --- a/lib/presentation/widgets/order_list_item.dart +++ b/lib/presentation/widgets/order_list_item.dart @@ -1,11 +1,13 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/screens/order_details_screen.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; import 'package:google_fonts/google_fonts.dart'; class OrderListItem extends StatelessWidget { - final OrderModel order; + final NostrEvent order; const OrderListItem({super.key, required this.order}); @@ -34,11 +36,11 @@ class OrderListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${order.user} ${order.rating}/5 (${order.ratingCount})', + '${order.name} ${order.rating}/5 (_)', style: const TextStyle(color: Colors.white), ), Text( - 'Time: ${order.timeAgo}', + 'Time: ${order.expiration}', style: const TextStyle(color: Colors.white), ), ], @@ -56,7 +58,7 @@ class OrderListItem extends StatelessWidget { Text.rich( TextSpan( children: [ - buildStyledTextSpan( + _buildStyledTextSpan( 'offering ', '${order.amount}', isValue: true, @@ -77,14 +79,14 @@ class OrderListItem extends StatelessWidget { Text.rich( TextSpan( children: [ - buildStyledTextSpan( + _buildStyledTextSpan( 'for ', '${order.fiatAmount}', isValue: true, isBold: true, ), TextSpan( - text: '${order.fiatCurrency} ', + text: '${order.currency} ', style: const TextStyle( color: Colors.white, fontSize: 16.0, @@ -109,7 +111,7 @@ class OrderListItem extends StatelessWidget { child: Row( children: [ HeroIcon( - _getPaymentMethodIcon(order.paymentMethod), + _getPaymentMethodIcon(order.paymentMethods[0]), style: HeroIconStyle.outline, color: Colors.white, size: 16, @@ -117,7 +119,7 @@ class OrderListItem extends StatelessWidget { const SizedBox(width: 4), Flexible( child: Text( - order.paymentMethod, + order.paymentMethods[0], style: const TextStyle(color: Colors.grey), overflow: TextOverflow.visible, softWrap: true, @@ -150,12 +152,12 @@ class OrderListItem extends StatelessWidget { } } - TextSpan buildStyledTextSpan(String label, String value, + TextSpan _buildStyledTextSpan(String label, String value, {bool isValue = false, bool isBold = false}) { return TextSpan( text: label, style: TextStyle( - color: Colors.white, + color: AppTheme.cream1, fontWeight: FontWeight.normal, fontSize: isValue ? 16.0 : 24.0, fontFamily: GoogleFonts.robotoCondensed().fontFamily, diff --git a/lib/providers/event_store_providers.dart b/lib/providers/event_store_providers.dart new file mode 100644 index 00000000..ee877f33 --- /dev/null +++ b/lib/providers/event_store_providers.dart @@ -0,0 +1,26 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:dart_nostr/nostr/model/request/filter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; + +final nostrServicerProvider = Provider((ref) { + return NostrService()..init(); +}); + +final orderRepositoryProvider = Provider((ref) { + final nostrService = ref.read(nostrServicerProvider); + return OpenOrdersRepository(nostrService); +}); + +final orderEventsProvider = StreamProvider>((ref) { + final orderRepository = ref.watch(orderRepositoryProvider); + DateTime filterTime = DateTime.now().subtract(Duration(hours: 24)); + var filter = NostrFilter( + kinds: const [38383], + since: filterTime, + ); + orderRepository.subscribe(filter); + + return orderRepository.eventsStream; +}); diff --git a/lib/providers/exchange_service_provider.dart b/lib/providers/exchange_service_provider.dart new file mode 100644 index 00000000..47b67643 --- /dev/null +++ b/lib/providers/exchange_service_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/services/exchange_service.dart'; +import 'package:mostro_mobile/services/yadio_exchange_service.dart'; + +final exchangeServiceProvider = Provider((ref) { + return YadioExchangeService(); +}); + +final exchangeRateProvider = StateNotifierProvider.family, String>((ref, currency) { + final exchangeService = ref.read(exchangeServiceProvider); + final notifier = ExchangeRateNotifier(exchangeService); + notifier.fetchExchangeRate(currency); + return notifier; +}); + +final currencyCodesProvider = FutureProvider>((ref) async { + final exchangeService = ref.read(exchangeServiceProvider); + return await exchangeService.getCurrencyCodes(); +}); + +final selectedFiatCodeProvider = StateProvider((ref) => null); diff --git a/lib/providers/riverpod_providers.dart b/lib/providers/riverpod_providers.dart new file mode 100644 index 00000000..7f78b2b0 --- /dev/null +++ b/lib/providers/riverpod_providers.dart @@ -0,0 +1,23 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/secure_storage_manager.dart'; +import 'package:mostro_mobile/presentation/home/bloc/home_bloc.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; + +final nostrServicerProvider = Provider((ref) { + return NostrService()..init(); +}); + +final homeBlocProvider = Provider((ref) { + return HomeBloc(); +}); + +final sessionManagerProvider = Provider((ref) { + return SecureStorageManager(); +}); + +final mostroServiceProvider = Provider((ref) { + final sessionStorage = ref.read(sessionManagerProvider); + final nostrService = ref.read(nostrServicerProvider); + return MostroService(nostrService, sessionStorage); +}); diff --git a/lib/services/currency_input_formatter.dart b/lib/services/currency_input_formatter.dart new file mode 100644 index 00000000..699e508b --- /dev/null +++ b/lib/services/currency_input_formatter.dart @@ -0,0 +1,18 @@ +import 'package:flutter/services.dart'; + +class CurrencyInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final text = newValue.text; + final regex = RegExp(r'^\d*\.?\d{0,2}$'); + + if (regex.hasMatch(text)) { + return newValue; + } else { + return oldValue; + } + } +} diff --git a/lib/services/exchange_service.dart b/lib/services/exchange_service.dart new file mode 100644 index 00000000..bbeeac5f --- /dev/null +++ b/lib/services/exchange_service.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; + +class ExchangeRateNotifier extends StateNotifier> { + final ExchangeService exchangeService; + + ExchangeRateNotifier(this.exchangeService) + : super(const AsyncValue.loading()); + + Future fetchExchangeRate(String currency) async { + try { + state = const AsyncValue.loading(); + final rate = await exchangeService.getExchangeRate(currency, 'BTC'); + state = AsyncValue.data(rate); + } catch (error) { + state = AsyncValue.error(error, StackTrace.current); + } + } +} + +abstract class ExchangeService { + final String baseUrl; + + ExchangeService(this.baseUrl); + + Future getExchangeRate( + String fromCurrency, + String toCurrency, + ); + + Future> getRequest(String endpoint) async { + final url = Uri.parse('$baseUrl$endpoint'); + final response = await http.get(url); + + if (response.statusCode == 200) { + return json.decode(response.body) as Map; + } else { + throw Exception('Failed to load data: ${response.statusCode}'); + } + } + + Future> getCurrencyCodes(); +} diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 68ccf7c5..d0711b49 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,104 +1,182 @@ +import 'dart:collection'; import 'dart:convert'; +import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:mostro_mobile/core/config.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/session.dart'; +import 'package:mostro_mobile/data/repositories/mostro_repository.dart'; +import 'package:mostro_mobile/data/repositories/secure_storage_manager.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; +const int mostroVersion = 1; + class MostroService { final NostrService _nostrService; + final SecureStorageManager _secureStorageManager; + final _mostroRepository = MostroRepository(); - MostroService(this._nostrService); + final _orders = HashMap(); + final _sessions = HashMap(); - Future publishOrder(OrderModel order) async { - final content = jsonEncode({ - 'order': { - 'version': 1, - 'action': 'new-order', - 'content': { - 'order': order.toJson(), - }, - }, - }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); - await _nostrService.publishEvent(event); + MostroService(this._nostrService, this._secureStorageManager); + + Stream subscribeToOrders(NostrFilter filter) { + return _nostrService.subscribeToEvents(filter); } - Future cancelOrder(String orderId) async { + Future publishOrder(Order order) async { + final session = await _secureStorageManager.newSession(); + final content = jsonEncode({ 'order': { - 'version': 1, - 'id': orderId, - 'action': 'cancel', - 'content': null, + 'version': mostroVersion, + 'action': Action.newOrder.value, + 'content': order.toJson(), }, }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); + + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); + await _nostrService.publishEvent(event); + + final filter = NostrFilter(p: [session.publicKey]); + + subscribeToOrders(filter).listen((event) async { + final response = + await _nostrService.decryptNIP59Event(event, session.privateKey); + + final orderResponse = MostroMessage.deserialized(response.content!); + + if (orderResponse.requestId != null) { + _orders[orderResponse.requestId!] = orderResponse; + _sessions[orderResponse.requestId!] = session; + session.eventId = orderResponse.requestId; + } + }); } Future takeSellOrder(String orderId, {int? amount}) async { + final session = await _secureStorageManager.newSession(); + session.eventId = orderId; + final content = jsonEncode({ 'order': { - 'version': 1, + 'version': mostroVersion, 'id': orderId, - 'action': 'take-sell', + 'action': Action.takeSell.value, 'content': amount != null ? {'amount': amount} : null, }, }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); + await _nostrService.publishEvent(event); + + final filter = NostrFilter(p: [session.publicKey]); + + subscribeToOrders(filter).listen((event) async { + final response = + await _nostrService.decryptNIP59Event(event, session.privateKey); + + final orderResponse = MostroMessage.deserialized(response.content!); + + print(response); + }); } Future takeBuyOrder(String orderId, {int? amount}) async { + final session = await _secureStorageManager.newSession(); + session.eventId = orderId; + final content = jsonEncode({ 'order': { - 'version': 1, + 'version': mostroVersion, 'id': orderId, - 'action': 'take-buy', + 'action': Action.takeBuy.value, 'content': amount != null ? {'amount': amount} : null, }, }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); await _nostrService.publishEvent(event); + final filter = NostrFilter(p: [session.publicKey]); + + subscribeToOrders(filter).listen((event) async { + final response = + await _nostrService.decryptNIP59Event(event, session.privateKey); + + final orderResponse = MostroMessage.deserialized(response.content!); + + print(response); + }); } - Stream subscribeToOrders() { - DateTime filterTime = DateTime.now().subtract(Duration(hours: 24)); - - var filter = NostrFilter( - kinds: const [38383], - since: filterTime, - ); - return _nostrService.subscribeToEvents(filter).map((event) { - // Convertir el evento Nostr a OrderModel - // Implementar la lógica de conversión aquí - return OrderModel.fromEventTags(event.tags!); + Future cancelOrder(String orderId) async { + final order = _mostroRepository.getOrder(orderId); + + final session = await _secureStorageManager.loadSession(order!.requestId!); + + if (session == null) { + // TODO: throw error + return; + } + + final content = jsonEncode({ + 'order': { + 'version': mostroVersion, + 'id': orderId, + 'action': Action.cancel, + 'content': null, + }, }); + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); + await _nostrService.publishEvent(event); } Future sendFiatSent(String orderId) async { + final session = await _secureStorageManager.loadSession(orderId); + + if (session == null) { + // TODO: throw error + return; + } + final content = jsonEncode({ 'order': { - 'version': 1, + 'version': mostroVersion, 'id': orderId, - 'action': 'fiat-sent', + 'action': Action.fiatSent.value, 'content': null, }, }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); await _nostrService.publishEvent(event); } Future releaseOrder(String orderId) async { + final session = await _secureStorageManager.loadSession(orderId); + + if (session == null) { + // TODO: throw error + return; + } + final content = jsonEncode({ 'order': { - 'version': 1, + 'version': mostroVersion, 'id': orderId, - 'action': 'release', + 'action': Action.release.value, 'content': null, }, }); - final event = await _nostrService.createNIP59Event(content, Config.mostroPubKey); + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); await _nostrService.publishEvent(event); } -} \ No newline at end of file +} diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index 83765ede..225b20ae 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -80,39 +80,25 @@ class NostrService { return keyPair; } - Future getPrivateKey() async { - return await AuthUtils.getPrivateKey(); - } - String getMostroPubKey() { return Config.mostroPubKey; } Future createNIP59Event( - String content, String recipientPubKey) async { + String content, String recipientPubKey, String senderPrivateKey) async { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); } - final senderPrivateKey = await getPrivateKey(); - if (senderPrivateKey == null) { - throw Exception('No private key found. Generate a key pair first.'); - } - return NostrUtils.createNIP59Event( content, recipientPubKey, senderPrivateKey); } - Future decryptNIP59Event(NostrEvent event) async { + Future decryptNIP59Event(NostrEvent event, String privateKey) async { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); } - final privateKey = await getPrivateKey(); - if (privateKey == null) { - throw Exception('No private key found. Generate a key pair first.'); - } - return NostrUtils.decryptNIP59Event(event, privateKey); } } diff --git a/lib/services/yadio_exchange_service.dart b/lib/services/yadio_exchange_service.dart new file mode 100644 index 00000000..83697660 --- /dev/null +++ b/lib/services/yadio_exchange_service.dart @@ -0,0 +1,29 @@ +import 'exchange_service.dart'; + +class YadioExchangeService extends ExchangeService { + YadioExchangeService() : super('https://api.yadio.io/'); + + @override + Future getExchangeRate( + String fromCurrency, + String toCurrency, + ) async { + final endpoint = 'rate/$fromCurrency/$toCurrency'; + final data = await getRequest(endpoint); + + if (data.containsKey('rate')) { + return (data['rate'] as num).toDouble(); + } else { + throw Exception('Exchange rate not found in response'); + } + } + + @override + Future> getCurrencyCodes() async { + final endpoint = 'currencies'; + final data = await getRequest(endpoint); + + return data.map((key, value) => MapEntry(key, value.toString())); + + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f797..e71a16d2 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,6 @@ #include "generated_plugin_registrant.h" -#include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); - flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba0..2e1de87a 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 86135f19..307f9946 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,13 +5,11 @@ import FlutterMacOS import Foundation -import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 192a3802..cf117bab 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,22 +1,35 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - args: + _fe_analyzer_shared: dependency: transitive description: - name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "2.6.0" - asn1lib: + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: dependency: transitive description: - name: asn1lib - sha256: "6b151826fcc95ff246cd219a0bf4c753ea14f4081ad71c61939becf3aba27f70" + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "1.5.5" + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" async: dependency: transitive description: @@ -65,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + bitcoin_icons: + dependency: "direct main" + description: + name: bitcoin_icons + sha256: "6ee9e69d78683035c089ebc91c3c9de95f7694d7bdcfa7edbbd51d832876f9b3" + url: "https://pub.dev" + source: hosted + version: "0.0.4" bloc: dependency: "direct main" description: @@ -81,6 +102,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" characters: dependency: transitive description: @@ -97,8 +142,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - collection: + code_builder: dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: "direct main" description: name: collection sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a @@ -113,6 +166,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" + url: "https://pub.dev" + source: hosted + version: "1.11.0" crypto: dependency: "direct main" description: @@ -121,14 +182,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - cryptography: - dependency: "direct main" + cryptography_plus: + dependency: transitive description: - name: cryptography - sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + name: cryptography_plus + sha256: "34db787df4f4740a39474b6fb0a610aa6dc13a5b5b68754b4787a79939ac0454" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" cupertino_icons: dependency: "direct main" description: @@ -145,14 +206,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.2.0" - encrypt: + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + elliptic: dependency: "direct main" description: - name: encrypt - sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + name: elliptic + sha256: "0c303d810603953a65dc39c4c542fb7538defd9e212403c54c266140819523b6" url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "0.3.11" equatable: dependency: "direct main" description: @@ -181,10 +250,18 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -198,6 +275,11 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_lints: dependency: "direct dev" description: @@ -214,62 +296,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.23" - flutter_secure_storage: + flutter_riverpod: dependency: "direct main" description: - name: flutter_secure_storage - sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "9.2.2" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted - version: "3.1.2" + version: "2.6.1" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: "578bd8c508144fdaffd4f77b8ef2d8c523602275cd697cc3db284dbd762ef4ce" url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.14" flutter_test: dependency: "direct dev" description: flutter @@ -280,6 +322,27 @@ packages: description: flutter source: sdk version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" google_fonts: dependency: "direct main" description: @@ -304,22 +367,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - hive: - dependency: "direct main" - description: - name: hive - sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" - url: "https://pub.dev" - source: hosted - version: "2.2.3" - hive_flutter: - dependency: "direct main" - description: - name: hive_flutter - sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc - url: "https://pub.dev" - source: hosted - version: "1.1.0" http: dependency: "direct main" description: @@ -328,6 +375,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: @@ -336,6 +391,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: transitive description: @@ -344,14 +404,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.1" leak_tracker: dependency: transitive description: @@ -432,6 +500,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: @@ -456,6 +532,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mockito: + dependency: "direct dev" + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" nested: dependency: transitive description: @@ -464,6 +556,31 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nip44: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "73379b8b1332f1ee78a250055494e6b73e673ea2" + url: "https://github.com/chebizarro/dart-nip44.git" + source: git + version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: @@ -476,10 +593,10 @@ packages: dependency: transitive description: name: path_parsing - sha256: "45f7d6bba1128761de5540f39d5ca000ea8a1f22f06b76b61094a60a2997bd0e" + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" path_provider: dependency: transitive description: @@ -540,10 +657,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -553,13 +670,29 @@ packages: source: hosted version: "2.1.8" pointycastle: - dependency: "direct main" + dependency: transitive description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" url: "https://pub.dev" source: hosted version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" provider: dependency: transitive description: @@ -568,6 +701,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" qr: dependency: transitive description: @@ -584,14 +725,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" shared_preferences_android: dependency: transitive description: @@ -640,11 +789,67 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -661,6 +866,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" stream_channel: dependency: transitive description: @@ -677,6 +890,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -685,6 +906,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" test_api: dependency: transitive description: @@ -693,6 +922,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" timeago: dependency: "direct main" description: @@ -713,26 +950,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.14" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "2430b973a4ca3c4dbc9999b62b8c719a160100dcbae5c819bae0cacce32c9cdb" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.12" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.15" vector_math: dependency: transitive description: @@ -749,6 +986,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" web: dependency: transitive description: @@ -758,21 +1003,29 @@ packages: source: hosted version: "1.1.0" web_socket_channel: - dependency: "direct main" + dependency: transitive description: name: web_socket_channel sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted version: "2.4.0" - win32: + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webkit_inspection_protocol: dependency: transitive description: - name: win32 - sha256: "10169d3934549017f0ae278ccb07f828f9d6ea21573bab0fb77b0e1ef0fce454" + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "5.7.2" + version: "1.2.1" xdg_directories: dependency: transitive description: @@ -789,6 +1042,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: - dart: ">=3.5.3 <=3.9.9" + dart: ">=3.5.4 <=3.9.9" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 70af18e4..c7abee27 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,31 +31,31 @@ dependencies: flutter: sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 http: ^1.2.2 dart_nostr: ^8.2.0 bloc: ^8.1.4 flutter_bloc: ^8.1.6 - flutter_secure_storage: ^9.2.2 qr_flutter: ^4.0.0 heroicons: ^0.10.0 - cryptography: ^2.7.0 - hive: ^2.2.3 crypto: ^3.0.5 - hive_flutter: ^1.1.0 - pointycastle: ^3.9.1 convert: ^3.1.1 shared_preferences: ^2.3.2 - web_socket_channel: ^2.4.0 equatable: ^2.0.5 - encrypt: ^5.0.3 logging: ^1.2.0 local_auth: ^2.3.0 google_fonts: ^6.2.1 timeago: ^3.7.0 + flutter_riverpod: ^2.6.1 + bitcoin_icons: ^0.0.4 + collection: ^1.18.0 + elliptic: ^0.3.11 + + nip44: + git: + url: https://github.com/chebizarro/dart-nip44.git + ref: master + dev_dependencies: flutter_test: @@ -67,6 +67,10 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^5.0.0 + test: ^1.25.7 + mockito: ^5.4.4 + integration_test: + sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/test/examples/admin_add_solver.json b/test/examples/admin_add_solver.json new file mode 100644 index 00000000..891e72fe --- /dev/null +++ b/test/examples/admin_add_solver.json @@ -0,0 +1,25 @@ +[ + "Admin add solver", + "Solvers are users appointed by the Mostro administrator and are responsible for resolving disputes.", + "The administrator can add or remove them at any time.", + "The administrator can also solve disputes.", + "To add a solver the admin will need to send an `order` message to Mostro with action `admin-add-solver`:", + { + "order": { + "version": 1, + "action": "admin-add-solver", + "content": { + "text_message": "npub1qqq884wtp2jn96lqhqlnarl4kk3rmvrc9z2nmrvqujx3m4l2ea5qd5d0fq" + } + } + }, + "Mostro response", + "Mostro will send this message to the admin:", + { + "order": { + "version": 1, + "action": "admin-add-solver", + "content": null + } + } +] diff --git a/test/examples/admin_cancel_order.json b/test/examples/admin_cancel_order.json new file mode 100644 index 00000000..67afc3ae --- /dev/null +++ b/test/examples/admin_cancel_order.json @@ -0,0 +1,67 @@ +[ + "Cancel order", + "An admin can cancel an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an `order` message to Mostro with action `admin-cancel` with the `id` of the order like this:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "admin-cancel", + "content": null + } + }, + "Mostro response", + "Mostro will send this message to the both parties buyer/seller and to the admin:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "admin-canceled", + "content": null + } + }, + "Mostro updates parameterized replaceable events", + "Mostro will publish two parameterized replaceable events, one for the order to update the status to `canceled-by-admin`, this means that the hold invoice was canceled and the seller's funds were returned:", + [ + "EVENT", + "RAND", + { + "id": "3d74ce3f10096d163603aa82beb5778bd1686226fdfcfba5d4c3a2c3137929ea", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703260182, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "canceled-by-admin"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "76bfc5e9ce089757dd4074472e1df421da700ce133c874f40b1136607121eca8acfdd2b8b4b374adaa83fa0c7d99672eb21a1068b6b6b774742d5de5bfc932ba" + } + ], + "And updates parameterized replaceable dispute event with status `seller-refunded`:", + [ + "EVENT", + "RAND", + { + "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703274022, + "kind": 38383, + "tags": [ + ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], + ["s", "seller-refunded"], + ["y", "mostrop2p"], + ["z", "dispute"] + ], + "content": "", + "sig": "6d7ca7bef7b696f1f6f8cfc33b3fe1beb2fdc6b7647efc93be669c6c1a9d4bafc770d9b0d25432c204dd487d48b39e589dfd7b03bf0e808483921b8937bd5367" + } + ] +] diff --git a/test/examples/admin_settle_order.json b/test/examples/admin_settle_order.json new file mode 100644 index 00000000..a3643868 --- /dev/null +++ b/test/examples/admin_settle_order.json @@ -0,0 +1,93 @@ +[ + "Settle order", + "An admin can settle an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an `order` message to Mostro with action `admin-settle` with the `id` of the order like this:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "admin-settle", + "content": null + } + }, + "Mostro response", + "Mostro will send this message to the both parties buyer/seller and to the admin:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "admin-settled", + "content": null + } + }, + "Mostro updates parameterized replaceable events", + "Mostro will publish two parameterized replaceable messages, one for the order to update the status to `settled-by-admin`, this means that the hold invoice paid by the seller was settled:", + [ + "EVENT", + "RAND", + { + "id": "3d74ce3f10096d163603aa82beb5778bd1686226fdfcfba5d4c3a2c3137929ea", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703260182, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "settled-by-admin"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "76bfc5e9ce089757dd4074472e1df421da700ce133c874f40b1136607121eca8acfdd2b8b4b374adaa83fa0c7d99672eb21a1068b6b6b774742d5de5bfc932ba" + } + ], + "And updates parameterized replaceable dispute event with status `settled`:", + [ + "EVENT", + "RAND", + { + "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703274022, + "kind": 38383, + "tags": [ + ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], + ["s", "settled"], + ["y", "mostrop2p"], + ["z", "dispute"] + ], + "content": "", + "sig": "6d7ca7bef7b696f1f6f8cfc33b3fe1beb2fdc6b7647efc93be669c6c1a9d4bafc770d9b0d25432c204dd487d48b39e589dfd7b03bf0e808483921b8937bd5367" + } + ], + "Payment of the buyer's invoice", + "At this point Mostro is trying to pay the buyer's invoice, right after complete the payment Mostro will update the status of the order parameterized replaceable event to `success`:", + [ + "EVENT", + "RAND", + { + "id": "6170892aca6a73906142e58a9c29734d49b399a3811f6216ce553b4a77a8a11e", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703274032, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "success"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "1670a9e61f7bc99f7121a95a2d479456970fbd9bc84d663160e35d1a95d71a006c7986db050ea584d5040927879fd9dcc85dc0ab5c6367f679c9fd5fd33a3cfb" + } + ] +] diff --git a/test/examples/buyer_sends_ln.json b/test/examples/buyer_sends_ln.json new file mode 100644 index 00000000..a9708ca6 --- /dev/null +++ b/test/examples/buyer_sends_ln.json @@ -0,0 +1,48 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "add-invoice", + "content": { + "payment_request": [ + null, + "lnbcrt1pn9dvx0pp5935mskms2uf8wx90m8dlr60ytwn5vxy0e65ls42h7y7exweyvekqdqqcqzzsxqyz5vqsp5xjmllv4ta7jkuc5nfgqp8qjc3amzfewmlycpkkggr7q2y5mjfldq9qyyssqncpf3vm8hwujutqc99f0vy45zh8es54mn6u99q9t6rwm0q80dxszskzrp24y46lxqkc7ly9p80t6lalc8x8xhsn49yhy70a7wqyygugpv7chqs", + 3922 + ] + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "waiting-seller-to-pay", + "content": null + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-payment"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/examples/cancel_order.json b/test/examples/cancel_order.json new file mode 100644 index 00000000..76b99a28 --- /dev/null +++ b/test/examples/cancel_order.json @@ -0,0 +1,121 @@ +[ + "Cancel Order", + "A user can cancel an order created by himself and with status `pending` sending action `cancel`, the rumor's content of the message will look like this:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cancel", + "content": null + } + }, + "Mostro response", + "Mostro will send a message with action `cancel` confirming the order was canceled, here an example of rumor's content of the message:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "canceled", + "content": null + } + }, + "Mostro updates the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `canceled`:", + [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "canceled"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "Cancel cooperatively", + "A user can cancel an `active` order, but will need the counterparty to agree, let's look at an example where the seller initiates a cooperative cancellation:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cancel", + "content": null + } + }, + "Mostro will send this message to the seller:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cooperative-cancel-initiated-by-you", + "content": null + } + }, + "And this message to the buyer:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cooperative-cancel-initiated-by-peer", + "content": null + } + }, + "Mostro updates the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `cooperatively-canceled`:", + [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "cooperatively-canceled"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "The buyer can accept the cooperative cancellation sending this message:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cancel", + "content": null + } + }, + "And Mostro will send this message to both parties:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "cooperative-cancel-accepted", + "content": null + } + } +] diff --git a/test/examples/dispute.json b/test/examples/dispute.json new file mode 100644 index 00000000..aa5dc999 --- /dev/null +++ b/test/examples/dispute.json @@ -0,0 +1,108 @@ +[ + "Dispute", + "A use can start a dispute in an order with status `active` or `fiat-sent` sending action `dispute`, here is an example where the seller initiates a dispute:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "dispute", + "content": null + } + }, + "Mostro response", + "Mostro will send this message to the seller:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "dispute-initiated-by-you", + "content": { + "dispute": "efc75871-2568-40b9-a6ee-c382d4d6de01" + } + } + }, + "And here is the message to the buyer:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "dispute-initiated-by-peer", + "content": { + "dispute": "efc75871-2568-40b9-a6ee-c382d4d6de01" + } + } + }, + "Mostro will not update the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `dispute`, this is because the order is still active, the dispute is just a way to let the admins and the other party know that there is a problem with the order.", + "Mostro send a parameterized replaceable event to show the dispute", + "Here is an example of the event sent by Mostro:", + [ + "EVENT", + "RAND", + { + "id": "4a4d63698f8a27d7d44e5669224acf6af2516a9350ae5f07d3cb91e5601f7302", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703016565, + "kind": 38383, + "tags": [ + ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], + ["s", "initiated"], + ["y", "mostrop2p"], + ["z", "dispute"] + ], + "content": "", + "sig": "00a1da45c00684c5af18cf292ca11697c9e70f2a691e6cd397211e717d2f54362dd401d7567da8184a5c596f48a09693479e67214c23e773523a63d0b1c3f537" + } + ], + "Mostro admin will see the dispute and can take it using the dispute `Id` from `d` tag, in this case `efc75871-2568-40b9-a6ee-c382d4d6de01`.", + { + "dispute": { + "version": 1, + "id": "efc75871-2568-40b9-a6ee-c382d4d6de01", + "action": "admin-take-dispute", + "content": null + } + }, + "Mostro will send a confirmation message to the admin with the order details:", + { + "dispute": { + "version": 1, + "id": "efc75871-2568-40b9-a6ee-c382d4d6de01", + "action": "admin-took-dispute", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "active", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", + "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", + "buyer_invoice": "lnbcrt11020n1pjcypj3pp58m3d9gcu4cc8l3jgkpfn7zhqv2jfw7p3t6z3tq2nmk9cjqam2c3sdqqcqzzsxqyz5vqsp5mew44wzjs0a58d9sfpkrdpyrytswna6gftlfrv8xghkc6fexu6sq9qyyssqnwfkqdxm66lxjv8z68ysaf0fmm50ztvv773jzuyf8a5tat3lnhks6468ngpv3lk5m7yr7vsg97jh6artva5qhd95vafqhxupyuawmrcqnthl9y", + "created_at": 1698870173 + } + } + } + }, + "Also Mostro will broadcast a new parameterized replaceable dispute event to update the dispute `status` to `in-progress`:", + [ + "EVENT", + "RAND", + { + "id": "2bb3f5a045bcc1eb057fd1e22c0cece7c58428a6ab5153299ef4e1e89633fde9", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703020540, + "kind": 38383, + "tags": [ + ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], + ["s", "in-progress"], + ["y", "mostrop2p"], + ["z", "dispute"] + ], + "content": "", + "sig": "20d454a0704cfac1d4a6660d234ce407deb56db8f08598741af5d38c0698a96234fd326a34e7efb2ac20c1c0ed0a921fd50513aab8f5c4b83e2509f2d32794d2" + } + ] +] diff --git a/test/examples/fiat_sent.json b/test/examples/fiat_sent.json new file mode 100644 index 00000000..5793cb4f --- /dev/null +++ b/test/examples/fiat_sent.json @@ -0,0 +1,62 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "fiat-sent", + "content": null + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "fiat-sent-ok", + "content": { + "Peer": { + "pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78" + } + } + } + }, + "seller": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", + "action": "fiat-sent-ok", + "content": { + "Peer": { + "pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427" + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "fiat-sent"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/examples/list_disputes.json b/test/examples/list_disputes.json new file mode 100644 index 00000000..1ef383f4 --- /dev/null +++ b/test/examples/list_disputes.json @@ -0,0 +1,23 @@ +[ + "Listing Disputes", + "Mostro publishes new disputes with event kind `38383` and status `initiated`:", + [ + "EVENT", + "RAND", + { + "id": "4a4d63698f8a27d7d44e5669224acf6af2516a9350ae5f07d3cb91e5601f7302", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1703016565, + "kind": 38383, + "tags": [ + ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], + ["s", "initiated"], + ["y", "mostrop2p"], + ["z", "dispute"] + ], + "content": "", + "sig": "00a1da45c00684c5af18cf292ca11697c9e70f2a691e6cd397211e717d2f54362dd401d7567da8184a5c596f48a09693479e67214c23e773523a63d0b1c3f537" + } + ], + "Clients can query this events by nostr event kind `38383`, nostr event author, dispute status (`s`), type (`z`)" +] diff --git a/test/examples/list_orders.json b/test/examples/list_orders.json new file mode 100644 index 00000000..cfe5468f --- /dev/null +++ b/test/examples/list_orders.json @@ -0,0 +1,27 @@ +[ + "EVENT", + "RAND", + { + "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702548701, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "pending"], + ["amt", "0"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" + } +] diff --git a/test/examples/new_buy_order.json b/test/examples/new_buy_order.json new file mode 100644 index 00000000..7f9aee44 --- /dev/null +++ b/test/examples/new_buy_order.json @@ -0,0 +1,70 @@ +{ + "order": { + "order": { + "version": 1, + "action": "new-order", + "content": { + "order": { + "kind": "buy", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 0 + } + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "new-order", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "buy", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": null, + "master_seller_pubkey": null, + "buyer_invoice": null, + "created_at": 1698870173 + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702548701, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "buy"], + ["f", "VES"], + ["s", "pending"], + ["amt", "0"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" + } + ] +} diff --git a/test/examples/new_buy_order_ln.json b/test/examples/new_buy_order_ln.json new file mode 100644 index 00000000..41f3bfe2 --- /dev/null +++ b/test/examples/new_buy_order_ln.json @@ -0,0 +1,71 @@ +{ + "order": { + "order": { + "version": 1, + "action": "new-order", + "content": { + "order": { + "kind": "buy", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "buyer_invoice": "mostro_p2p@ln.tips", + "created_at": 0 + } + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "new-order", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "buy", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": null, + "master_seller_pubkey": null, + "buyer_invoice": "mostro_p2p@ln.tips", + "created_at": 1698870173 + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702548701, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "buy"], + ["f", "VES"], + ["s", "pending"], + ["amt", "0"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" + } + ] +} diff --git a/test/examples/new_sell_order.json b/test/examples/new_sell_order.json new file mode 100644 index 00000000..9ba0f72a --- /dev/null +++ b/test/examples/new_sell_order.json @@ -0,0 +1,69 @@ +{ + "order": { + "order": { + "version": 1, + "action": "new-order", + "content": { + "order": { + "kind": "sell", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "min_amount": null, + "max_amount": null, + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 0 + } + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "new-order", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698870173 + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702548701, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "pending"], + ["amt", "0"], + ["fa", "100"], + ["pm", "face to face", "bank transfer"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" + } + ] +} diff --git a/test/examples/new_sell_range_order.json b/test/examples/new_sell_range_order.json new file mode 100644 index 00000000..5b24fa8c --- /dev/null +++ b/test/examples/new_sell_range_order.json @@ -0,0 +1,71 @@ +{ + "order": { + "order": { + "version": 1, + "action": "new-order", + "content": { + "order": { + "kind": "sell", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "min_amount": 10, + "max_amount": 20, + "fiat_amount": 0, + "payment_method": "face to face", + "premium": 1, + "created_at": 0 + } + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "new-order", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "pending", + "amount": 0, + "fiat_code": "VES", + "min_amount": 10, + "max_amount": 20, + "fiat_amount": 0, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698870173 + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702548701, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "pending"], + ["amt", "0"], + ["fa", "10", "20"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" + } + ] +} diff --git a/test/examples/overview.json b/test/examples/overview.json new file mode 100644 index 00000000..d1ae81f7 --- /dev/null +++ b/test/examples/overview.json @@ -0,0 +1,8 @@ +{ + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "fiat-sent", + "content": null + } +} \ No newline at end of file diff --git a/test/examples/rate_user.json b/test/examples/rate_user.json new file mode 100644 index 00000000..951ffae7 --- /dev/null +++ b/test/examples/rate_user.json @@ -0,0 +1,61 @@ +[ + "User rating", + "After a successful trade Mostro send a Gift wrap Nostr event to both parties to let them know they can rate each other, here an example how the message look like:", + { + "order": { + "version": 1, + "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", + "action": "rate", + "content": null + } + }, + "After a Mostro client receive this message, the user can rate the other party, the rating is a number between 1 and 5, to rate the client must receive user's input and create a new Gift wrap Nostr event to send to Mostro with this content:", + { + "order": { + "version": 1, + "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", + "action": "rate-user", + "content": { + "rating_user": 5 + } + } + }, + "Confirmation message", + "If Mostro received the correct message, it will send back a confirmation message to the user with the action `rate-received`:", + { + "order": { + "version": 1, + "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", + "pubkey": null, + "action": "rate-received", + "content": { + "rating_user": 5 + } + } + }, + "Mostro updates the parameterized replaceable rating event, in this event the `d` tag will be the user pubkey `00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78` and looks like this:", + [ + "EVENT", + "RAND", + { + "id": "80909a120d17632f99995f92caff4801f25e9e523d7643bf8acb0166bd0932a6", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702637077, + "kind": 38383, + "tags": [ + [ + "d", + "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78" + ], + ["total_reviews", "1"], + ["total_rating", "2"], + ["last_rating", "1"], + ["max_rate", "2"], + ["min_rate", "5"], + ["data_label", "rating"] + ], + "content": "", + "sig": "456fdc0589a5ffe1b55d5474cef2826bf01f458d63cf409490def9c5af31052e0461d38aed4f386f5dcea999e9fe6001d27d592dbba54a0420687dce0652322f" + } + ] +] diff --git a/test/examples/release.json b/test/examples/release.json new file mode 100644 index 00000000..d9373c46 --- /dev/null +++ b/test/examples/release.json @@ -0,0 +1,94 @@ +[ + "Release", + "After confirming the buyer sent the fiat money, the seller should send a message to Mostro indicating that sats should be delivered to the buyer, the rumor's content of the message will look like this:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "release", + "content": null + } + }, + "Mostro response", + "Here an example of the Mostro response to the seller:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "hold-invoice-payment-settled", + "content": null + } + }, + "And a message to the buyer to let him know that the sats were released:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "released", + "content": null + } + }, + "Buyer receives sats", + "Right after seller release sats Mostro will try to pay the buyer's lightning invoice, if the payment is successful Mostro will send a message to the buyer indicating that the purchase was completed:", + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "purchase-completed", + "content": null + } + }, + "Mostro updates the parameterized replaceable event with d tag ede61c96-4c13-4519-bf3a-dcf7f1e9d842 to change the status to settled-hold-invoice:", + [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "settled-hold-invoice"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "Mostro will then attempt to pay the buyer's invoice, if the payment successds Mostro updates the parameterized replaceable event with d tag ede61c96-4c13-4519-bf3a-dcf7f1e9d842 to change the status to success:", + [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "success"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +] diff --git a/test/examples/seller_pays_hold_invoive.json b/test/examples/seller_pays_hold_invoive.json new file mode 100644 index 00000000..1d8b0cc2 --- /dev/null +++ b/test/examples/seller_pays_hold_invoive.json @@ -0,0 +1,163 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "pay-invoice", + "content": { + "payment_request": [ + { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "waiting-payment", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698937797 + }, + "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" + ] + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "buyer-took-order", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "active", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", + "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", + "buyer_invoice": null, + "created_at": 1698937797 + } + } + } + }, + "buyer_response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "hold-invoice-payment-accepted", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "active", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", + "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", + "buyer_invoice": null, + "created_at": 1698937797 + } + } + } + }, "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "active"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], "waiting_invoice": + { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "waiting-buyer-invoice", + "content": null + } + }, + "buyer":{ + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "add-invoice", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "sell", + "status": "waiting-buyer-invoice", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698937797 + } + } + } + }, "update_event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-buyer-invoice"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "buyer_sends_invoice": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "add-invoice", + "content": { + "payment_request": [ + null, + "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" + ] + } + } + } +} diff --git a/test/examples/take_buy_order.json b/test/examples/take_buy_order.json new file mode 100644 index 00000000..a176dcf3 --- /dev/null +++ b/test/examples/take_buy_order.json @@ -0,0 +1,167 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-buy", + "content": null + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "pay-invoice", + "content": { + "payment_request": [ + { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "kind": "buy", + "status": "waiting-payment", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": 1698957793 + }, + "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" + ] + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-payment"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "buyer": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "waiting-seller-to-pay", + "content": null + } + }, + "seller": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "waiting-buyer-invoice", + "content": null + } + }, + "update_event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-buyer-invoice"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ], + "seller_update": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "add-invoice", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "status": "waiting-buyer-invoice", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "created_at": null + } + } + } + }, + "buyer_sends_ln": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "pubkey": null, + "action": "add-invoice", + "content": { + "payment_request": [ + null, + "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" + ] + } + } + }, + "final_event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "active"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/examples/take_buy_range_order.json b/test/examples/take_buy_range_order.json new file mode 100644 index 00000000..d3b46d56 --- /dev/null +++ b/test/examples/take_buy_range_order.json @@ -0,0 +1,12 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-buy", + "content": { + "amount": 15 + } + } + } +} diff --git a/test/examples/take_sell_order.json b/test/examples/take_sell_order.json new file mode 100644 index 00000000..2d63886f --- /dev/null +++ b/test/examples/take_sell_order.json @@ -0,0 +1,56 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-sell", + "content": null + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "add-invoice", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "amount": 7851, + "fiat_code": "VES", + "fiat_amount": 100, + "payment_method": "face to face", + "premium": 1, + "buyer_pubkey": null, + "seller_pubkey": null + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-buyer-invoice"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/examples/take_sell_order_ln.json b/test/examples/take_sell_order_ln.json new file mode 100644 index 00000000..5a44c90a --- /dev/null +++ b/test/examples/take_sell_order_ln.json @@ -0,0 +1,47 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-sell", + "content": { + "payment_request": [null, "mostro_p2p@ln.tips"] + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "waiting-seller-to-pay", + "content": null + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-payment"], + ["amt", "7851"], + ["fa", "100"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/examples/take_sell_range_order.json b/test/examples/take_sell_range_order.json new file mode 100644 index 00000000..dea23339 --- /dev/null +++ b/test/examples/take_sell_range_order.json @@ -0,0 +1,73 @@ +{ + "order": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-sell", + "content": { + "amount": 15 + } + } + }, + "order_with_ln": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "take-sell", + "content": { + "payment_request": [null, "mostro_p2p@ln.tips", 15] + } + } + }, + "response": { + "order": { + "version": 1, + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "action": "add-invoice", + "content": { + "order": { + "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", + "amount": 7851, + "fiat_code": "VES", + "min_amount": 10, + "max_amount": 20, + "fiat_amount": 15, + "payment_method": "face to face", + "premium": 1, + "master_buyer_pubkey": null, + "master_seller_pubkey": null, + "buyer_invoice": null, + "created_at": null, + "expires_at": null + } + } + } + }, + "event": [ + "EVENT", + "RAND", + { + "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", + "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", + "created_at": 1702549437, + "kind": 38383, + "tags": [ + ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], + ["k", "sell"], + ["f", "VES"], + ["s", "waiting-buyer-invoice"], + ["amt", "7851"], + ["fa", "15"], + ["pm", "face to face"], + ["premium", "1"], + ["network", "mainnet"], + ["layer", "lightning"], + ["expiration", "1719391096"], + ["y", "mostrop2p"], + ["z", "order"] + ], + "content": "", + "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" + } + ] +} diff --git a/test/models/order_test.dart b/test/models/order_test.dart new file mode 100644 index 00000000..ee7a5432 --- /dev/null +++ b/test/models/order_test.dart @@ -0,0 +1,51 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:test/test.dart'; + +Future> loadJson(String path) async { + final file = File(path); + final contents = await file.readAsString(); + return jsonDecode(contents); +} + +void main() { + group('Order Tests', () { + test('Create new Order with default values', () { + final order = Order( + kind: OrderType.sell, + fiatCode: 'VES', + fiatAmount: 100, + paymentMethod: 'face to face', + premium: 1, + ); + + expect(order.status, equals(Status.pending)); + expect(order.amount, equals(0)); + expect(order.fiatAmount, equals(100)); + }); + }); + + group('Order Tests with JSON', () { + test('Parse new sell order from JSON file', () async { + // Load JSON data + final jsonData = await loadJson('test/examples/new_sell_order.json'); + + // Parse JSON to model + final orderData = jsonData['order']['order']['content']['order']; + final order = Order.fromJson(orderData); + + // Validate model properties + expect(order.kind, equals(OrderType.sell)); + expect(order.status, equals(Status.pending)); + expect(order.amount, equals(0)); + expect(order.fiatCode, equals('VES')); + expect(order.fiatAmount, equals(100)); + expect(order.paymentMethod, equals('face to face')); + expect(order.premium, equals(1)); + }); + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 011734da..7407dddd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,9 @@ #include "generated_plugin_registrant.h" -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - FlutterSecureStorageWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 11485fce..ef187dca 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,7 +3,6 @@ # list(APPEND FLUTTER_PLUGIN_LIST - flutter_secure_storage_windows local_auth_windows ) From df31c0e4b559fc4b05594817cb8591d759db4a28 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Fri, 15 Nov 2024 09:35:35 -0800 Subject: [PATCH 2/9] clean up for merge --- integration_test/app_test.dart | 27 ---- test/examples/admin_add_solver.json | 25 --- test/examples/admin_cancel_order.json | 67 -------- test/examples/admin_settle_order.json | 93 ----------- test/examples/buyer_sends_ln.json | 48 ------ test/examples/cancel_order.json | 121 -------------- test/examples/dispute.json | 108 ------------- test/examples/fiat_sent.json | 62 -------- test/examples/list_disputes.json | 23 --- test/examples/list_orders.json | 27 ---- test/examples/new_buy_order.json | 70 -------- test/examples/new_buy_order_ln.json | 71 --------- test/examples/new_sell_order.json | 69 -------- test/examples/new_sell_range_order.json | 71 --------- test/examples/overview.json | 8 - test/examples/rate_user.json | 61 ------- test/examples/release.json | 94 ----------- test/examples/seller_pays_hold_invoive.json | 163 ------------------- test/examples/take_buy_order.json | 167 -------------------- test/examples/take_buy_range_order.json | 12 -- test/examples/take_sell_order.json | 56 ------- test/examples/take_sell_order_ln.json | 47 ------ test/examples/take_sell_range_order.json | 73 --------- 23 files changed, 1563 deletions(-) delete mode 100644 integration_test/app_test.dart delete mode 100644 test/examples/admin_add_solver.json delete mode 100644 test/examples/admin_cancel_order.json delete mode 100644 test/examples/admin_settle_order.json delete mode 100644 test/examples/buyer_sends_ln.json delete mode 100644 test/examples/cancel_order.json delete mode 100644 test/examples/dispute.json delete mode 100644 test/examples/fiat_sent.json delete mode 100644 test/examples/list_disputes.json delete mode 100644 test/examples/list_orders.json delete mode 100644 test/examples/new_buy_order.json delete mode 100644 test/examples/new_buy_order_ln.json delete mode 100644 test/examples/new_sell_order.json delete mode 100644 test/examples/new_sell_range_order.json delete mode 100644 test/examples/overview.json delete mode 100644 test/examples/rate_user.json delete mode 100644 test/examples/release.json delete mode 100644 test/examples/seller_pays_hold_invoive.json delete mode 100644 test/examples/take_buy_order.json delete mode 100644 test/examples/take_buy_range_order.json delete mode 100644 test/examples/take_sell_order.json delete mode 100644 test/examples/take_sell_order_ln.json delete mode 100644 test/examples/take_sell_range_order.json diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart deleted file mode 100644 index 6140ad54..00000000 --- a/integration_test/app_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:mostro_mobile/main.dart' as app; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Create and send a new sell order', (WidgetTester tester) async { - app.main(); - await tester.pumpAndSettle(); - - // Navigate to Create Order Screen - final createOrderButton = find.byKey(Key('createOrderButton')); - await tester.tap(createOrderButton); - await tester.pumpAndSettle(); - - // Input order details - await tester.enterText(find.byKey(Key('fiatAmountField')), '100'); - await tester.tap(find.byKey(Key('submitOrderButton'))); - await tester.pumpAndSettle(); - - // Verify that order confirmation appears on UI - final confirmationMessage = find.text('Order Created'); - expect(confirmationMessage, findsOneWidget); - }); -} diff --git a/test/examples/admin_add_solver.json b/test/examples/admin_add_solver.json deleted file mode 100644 index 891e72fe..00000000 --- a/test/examples/admin_add_solver.json +++ /dev/null @@ -1,25 +0,0 @@ -[ - "Admin add solver", - "Solvers are users appointed by the Mostro administrator and are responsible for resolving disputes.", - "The administrator can add or remove them at any time.", - "The administrator can also solve disputes.", - "To add a solver the admin will need to send an `order` message to Mostro with action `admin-add-solver`:", - { - "order": { - "version": 1, - "action": "admin-add-solver", - "content": { - "text_message": "npub1qqq884wtp2jn96lqhqlnarl4kk3rmvrc9z2nmrvqujx3m4l2ea5qd5d0fq" - } - } - }, - "Mostro response", - "Mostro will send this message to the admin:", - { - "order": { - "version": 1, - "action": "admin-add-solver", - "content": null - } - } -] diff --git a/test/examples/admin_cancel_order.json b/test/examples/admin_cancel_order.json deleted file mode 100644 index 67afc3ae..00000000 --- a/test/examples/admin_cancel_order.json +++ /dev/null @@ -1,67 +0,0 @@ -[ - "Cancel order", - "An admin can cancel an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an `order` message to Mostro with action `admin-cancel` with the `id` of the order like this:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "admin-cancel", - "content": null - } - }, - "Mostro response", - "Mostro will send this message to the both parties buyer/seller and to the admin:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "admin-canceled", - "content": null - } - }, - "Mostro updates parameterized replaceable events", - "Mostro will publish two parameterized replaceable events, one for the order to update the status to `canceled-by-admin`, this means that the hold invoice was canceled and the seller's funds were returned:", - [ - "EVENT", - "RAND", - { - "id": "3d74ce3f10096d163603aa82beb5778bd1686226fdfcfba5d4c3a2c3137929ea", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703260182, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "canceled-by-admin"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "76bfc5e9ce089757dd4074472e1df421da700ce133c874f40b1136607121eca8acfdd2b8b4b374adaa83fa0c7d99672eb21a1068b6b6b774742d5de5bfc932ba" - } - ], - "And updates parameterized replaceable dispute event with status `seller-refunded`:", - [ - "EVENT", - "RAND", - { - "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703274022, - "kind": 38383, - "tags": [ - ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], - ["s", "seller-refunded"], - ["y", "mostrop2p"], - ["z", "dispute"] - ], - "content": "", - "sig": "6d7ca7bef7b696f1f6f8cfc33b3fe1beb2fdc6b7647efc93be669c6c1a9d4bafc770d9b0d25432c204dd487d48b39e589dfd7b03bf0e808483921b8937bd5367" - } - ] -] diff --git a/test/examples/admin_settle_order.json b/test/examples/admin_settle_order.json deleted file mode 100644 index a3643868..00000000 --- a/test/examples/admin_settle_order.json +++ /dev/null @@ -1,93 +0,0 @@ -[ - "Settle order", - "An admin can settle an order, most of the time this is done when admin is solving a dispute, for this the admin will need to send an `order` message to Mostro with action `admin-settle` with the `id` of the order like this:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "admin-settle", - "content": null - } - }, - "Mostro response", - "Mostro will send this message to the both parties buyer/seller and to the admin:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "admin-settled", - "content": null - } - }, - "Mostro updates parameterized replaceable events", - "Mostro will publish two parameterized replaceable messages, one for the order to update the status to `settled-by-admin`, this means that the hold invoice paid by the seller was settled:", - [ - "EVENT", - "RAND", - { - "id": "3d74ce3f10096d163603aa82beb5778bd1686226fdfcfba5d4c3a2c3137929ea", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703260182, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "settled-by-admin"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "76bfc5e9ce089757dd4074472e1df421da700ce133c874f40b1136607121eca8acfdd2b8b4b374adaa83fa0c7d99672eb21a1068b6b6b774742d5de5bfc932ba" - } - ], - "And updates parameterized replaceable dispute event with status `settled`:", - [ - "EVENT", - "RAND", - { - "id": "098e8622eae022a79bc793984fccbc5ea3f6641bdcdffaa031c00d3bd33ca5a0", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703274022, - "kind": 38383, - "tags": [ - ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], - ["s", "settled"], - ["y", "mostrop2p"], - ["z", "dispute"] - ], - "content": "", - "sig": "6d7ca7bef7b696f1f6f8cfc33b3fe1beb2fdc6b7647efc93be669c6c1a9d4bafc770d9b0d25432c204dd487d48b39e589dfd7b03bf0e808483921b8937bd5367" - } - ], - "Payment of the buyer's invoice", - "At this point Mostro is trying to pay the buyer's invoice, right after complete the payment Mostro will update the status of the order parameterized replaceable event to `success`:", - [ - "EVENT", - "RAND", - { - "id": "6170892aca6a73906142e58a9c29734d49b399a3811f6216ce553b4a77a8a11e", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703274032, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "success"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "1670a9e61f7bc99f7121a95a2d479456970fbd9bc84d663160e35d1a95d71a006c7986db050ea584d5040927879fd9dcc85dc0ab5c6367f679c9fd5fd33a3cfb" - } - ] -] diff --git a/test/examples/buyer_sends_ln.json b/test/examples/buyer_sends_ln.json deleted file mode 100644 index a9708ca6..00000000 --- a/test/examples/buyer_sends_ln.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "add-invoice", - "content": { - "payment_request": [ - null, - "lnbcrt1pn9dvx0pp5935mskms2uf8wx90m8dlr60ytwn5vxy0e65ls42h7y7exweyvekqdqqcqzzsxqyz5vqsp5xjmllv4ta7jkuc5nfgqp8qjc3amzfewmlycpkkggr7q2y5mjfldq9qyyssqncpf3vm8hwujutqc99f0vy45zh8es54mn6u99q9t6rwm0q80dxszskzrp24y46lxqkc7ly9p80t6lalc8x8xhsn49yhy70a7wqyygugpv7chqs", - 3922 - ] - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "waiting-seller-to-pay", - "content": null - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-payment"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} diff --git a/test/examples/cancel_order.json b/test/examples/cancel_order.json deleted file mode 100644 index 76b99a28..00000000 --- a/test/examples/cancel_order.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - "Cancel Order", - "A user can cancel an order created by himself and with status `pending` sending action `cancel`, the rumor's content of the message will look like this:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cancel", - "content": null - } - }, - "Mostro response", - "Mostro will send a message with action `cancel` confirming the order was canceled, here an example of rumor's content of the message:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "canceled", - "content": null - } - }, - "Mostro updates the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `canceled`:", - [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "canceled"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "Cancel cooperatively", - "A user can cancel an `active` order, but will need the counterparty to agree, let's look at an example where the seller initiates a cooperative cancellation:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cancel", - "content": null - } - }, - "Mostro will send this message to the seller:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cooperative-cancel-initiated-by-you", - "content": null - } - }, - "And this message to the buyer:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cooperative-cancel-initiated-by-peer", - "content": null - } - }, - "Mostro updates the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `cooperatively-canceled`:", - [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "cooperatively-canceled"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "The buyer can accept the cooperative cancellation sending this message:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cancel", - "content": null - } - }, - "And Mostro will send this message to both parties:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "cooperative-cancel-accepted", - "content": null - } - } -] diff --git a/test/examples/dispute.json b/test/examples/dispute.json deleted file mode 100644 index aa5dc999..00000000 --- a/test/examples/dispute.json +++ /dev/null @@ -1,108 +0,0 @@ -[ - "Dispute", - "A use can start a dispute in an order with status `active` or `fiat-sent` sending action `dispute`, here is an example where the seller initiates a dispute:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "dispute", - "content": null - } - }, - "Mostro response", - "Mostro will send this message to the seller:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "dispute-initiated-by-you", - "content": { - "dispute": "efc75871-2568-40b9-a6ee-c382d4d6de01" - } - } - }, - "And here is the message to the buyer:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "dispute-initiated-by-peer", - "content": { - "dispute": "efc75871-2568-40b9-a6ee-c382d4d6de01" - } - } - }, - "Mostro will not update the parameterized replaceable event with `d` tag `ede61c96-4c13-4519-bf3a-dcf7f1e9d842` to change the status to `dispute`, this is because the order is still active, the dispute is just a way to let the admins and the other party know that there is a problem with the order.", - "Mostro send a parameterized replaceable event to show the dispute", - "Here is an example of the event sent by Mostro:", - [ - "EVENT", - "RAND", - { - "id": "4a4d63698f8a27d7d44e5669224acf6af2516a9350ae5f07d3cb91e5601f7302", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703016565, - "kind": 38383, - "tags": [ - ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], - ["s", "initiated"], - ["y", "mostrop2p"], - ["z", "dispute"] - ], - "content": "", - "sig": "00a1da45c00684c5af18cf292ca11697c9e70f2a691e6cd397211e717d2f54362dd401d7567da8184a5c596f48a09693479e67214c23e773523a63d0b1c3f537" - } - ], - "Mostro admin will see the dispute and can take it using the dispute `Id` from `d` tag, in this case `efc75871-2568-40b9-a6ee-c382d4d6de01`.", - { - "dispute": { - "version": 1, - "id": "efc75871-2568-40b9-a6ee-c382d4d6de01", - "action": "admin-take-dispute", - "content": null - } - }, - "Mostro will send a confirmation message to the admin with the order details:", - { - "dispute": { - "version": 1, - "id": "efc75871-2568-40b9-a6ee-c382d4d6de01", - "action": "admin-took-dispute", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "active", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", - "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", - "buyer_invoice": "lnbcrt11020n1pjcypj3pp58m3d9gcu4cc8l3jgkpfn7zhqv2jfw7p3t6z3tq2nmk9cjqam2c3sdqqcqzzsxqyz5vqsp5mew44wzjs0a58d9sfpkrdpyrytswna6gftlfrv8xghkc6fexu6sq9qyyssqnwfkqdxm66lxjv8z68ysaf0fmm50ztvv773jzuyf8a5tat3lnhks6468ngpv3lk5m7yr7vsg97jh6artva5qhd95vafqhxupyuawmrcqnthl9y", - "created_at": 1698870173 - } - } - } - }, - "Also Mostro will broadcast a new parameterized replaceable dispute event to update the dispute `status` to `in-progress`:", - [ - "EVENT", - "RAND", - { - "id": "2bb3f5a045bcc1eb057fd1e22c0cece7c58428a6ab5153299ef4e1e89633fde9", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703020540, - "kind": 38383, - "tags": [ - ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], - ["s", "in-progress"], - ["y", "mostrop2p"], - ["z", "dispute"] - ], - "content": "", - "sig": "20d454a0704cfac1d4a6660d234ce407deb56db8f08598741af5d38c0698a96234fd326a34e7efb2ac20c1c0ed0a921fd50513aab8f5c4b83e2509f2d32794d2" - } - ] -] diff --git a/test/examples/fiat_sent.json b/test/examples/fiat_sent.json deleted file mode 100644 index 5793cb4f..00000000 --- a/test/examples/fiat_sent.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "fiat-sent", - "content": null - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "fiat-sent-ok", - "content": { - "Peer": { - "pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78" - } - } - } - }, - "seller": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", - "action": "fiat-sent-ok", - "content": { - "Peer": { - "pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427" - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "fiat-sent"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} diff --git a/test/examples/list_disputes.json b/test/examples/list_disputes.json deleted file mode 100644 index 1ef383f4..00000000 --- a/test/examples/list_disputes.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - "Listing Disputes", - "Mostro publishes new disputes with event kind `38383` and status `initiated`:", - [ - "EVENT", - "RAND", - { - "id": "4a4d63698f8a27d7d44e5669224acf6af2516a9350ae5f07d3cb91e5601f7302", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1703016565, - "kind": 38383, - "tags": [ - ["d", "efc75871-2568-40b9-a6ee-c382d4d6de01"], - ["s", "initiated"], - ["y", "mostrop2p"], - ["z", "dispute"] - ], - "content": "", - "sig": "00a1da45c00684c5af18cf292ca11697c9e70f2a691e6cd397211e717d2f54362dd401d7567da8184a5c596f48a09693479e67214c23e773523a63d0b1c3f537" - } - ], - "Clients can query this events by nostr event kind `38383`, nostr event author, dispute status (`s`), type (`z`)" -] diff --git a/test/examples/list_orders.json b/test/examples/list_orders.json deleted file mode 100644 index cfe5468f..00000000 --- a/test/examples/list_orders.json +++ /dev/null @@ -1,27 +0,0 @@ -[ - "EVENT", - "RAND", - { - "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702548701, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "pending"], - ["amt", "0"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" - } -] diff --git a/test/examples/new_buy_order.json b/test/examples/new_buy_order.json deleted file mode 100644 index 7f9aee44..00000000 --- a/test/examples/new_buy_order.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "action": "new-order", - "content": { - "order": { - "kind": "buy", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 0 - } - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "new-order", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "buy", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": null, - "master_seller_pubkey": null, - "buyer_invoice": null, - "created_at": 1698870173 - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702548701, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "buy"], - ["f", "VES"], - ["s", "pending"], - ["amt", "0"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" - } - ] -} diff --git a/test/examples/new_buy_order_ln.json b/test/examples/new_buy_order_ln.json deleted file mode 100644 index 41f3bfe2..00000000 --- a/test/examples/new_buy_order_ln.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "action": "new-order", - "content": { - "order": { - "kind": "buy", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "buyer_invoice": "mostro_p2p@ln.tips", - "created_at": 0 - } - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "new-order", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "buy", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": null, - "master_seller_pubkey": null, - "buyer_invoice": "mostro_p2p@ln.tips", - "created_at": 1698870173 - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702548701, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "buy"], - ["f", "VES"], - ["s", "pending"], - ["amt", "0"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" - } - ] -} diff --git a/test/examples/new_sell_order.json b/test/examples/new_sell_order.json deleted file mode 100644 index 9ba0f72a..00000000 --- a/test/examples/new_sell_order.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "action": "new-order", - "content": { - "order": { - "kind": "sell", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "min_amount": null, - "max_amount": null, - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 0 - } - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "new-order", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 1698870173 - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702548701, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "pending"], - ["amt", "0"], - ["fa", "100"], - ["pm", "face to face", "bank transfer"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" - } - ] -} diff --git a/test/examples/new_sell_range_order.json b/test/examples/new_sell_range_order.json deleted file mode 100644 index 5b24fa8c..00000000 --- a/test/examples/new_sell_range_order.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "action": "new-order", - "content": { - "order": { - "kind": "sell", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "min_amount": 10, - "max_amount": 20, - "fiat_amount": 0, - "payment_method": "face to face", - "premium": 1, - "created_at": 0 - } - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "new-order", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "pending", - "amount": 0, - "fiat_code": "VES", - "min_amount": 10, - "max_amount": 20, - "fiat_amount": 0, - "payment_method": "face to face", - "premium": 1, - "created_at": 1698870173 - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "84fad0d29cb3529d789faeff2033e88fe157a48e071c6a5d1619928289420e31", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702548701, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "pending"], - ["amt", "0"], - ["fa", "10", "20"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "7e8fe1eb644f33ff51d8805c02a0e1a6d034e6234eac50ef7a7e0dac68a0414f7910366204fa8217086f90eddaa37ded71e61f736d1838e37c0b73f6a16c4af2" - } - ] -} diff --git a/test/examples/overview.json b/test/examples/overview.json deleted file mode 100644 index d1ae81f7..00000000 --- a/test/examples/overview.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "fiat-sent", - "content": null - } -} \ No newline at end of file diff --git a/test/examples/rate_user.json b/test/examples/rate_user.json deleted file mode 100644 index 951ffae7..00000000 --- a/test/examples/rate_user.json +++ /dev/null @@ -1,61 +0,0 @@ -[ - "User rating", - "After a successful trade Mostro send a Gift wrap Nostr event to both parties to let them know they can rate each other, here an example how the message look like:", - { - "order": { - "version": 1, - "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", - "action": "rate", - "content": null - } - }, - "After a Mostro client receive this message, the user can rate the other party, the rating is a number between 1 and 5, to rate the client must receive user's input and create a new Gift wrap Nostr event to send to Mostro with this content:", - { - "order": { - "version": 1, - "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", - "action": "rate-user", - "content": { - "rating_user": 5 - } - } - }, - "Confirmation message", - "If Mostro received the correct message, it will send back a confirmation message to the user with the action `rate-received`:", - { - "order": { - "version": 1, - "id": "7e44aa5d-855a-4b17-865e-8ca3834a91a3", - "pubkey": null, - "action": "rate-received", - "content": { - "rating_user": 5 - } - } - }, - "Mostro updates the parameterized replaceable rating event, in this event the `d` tag will be the user pubkey `00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78` and looks like this:", - [ - "EVENT", - "RAND", - { - "id": "80909a120d17632f99995f92caff4801f25e9e523d7643bf8acb0166bd0932a6", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702637077, - "kind": 38383, - "tags": [ - [ - "d", - "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78" - ], - ["total_reviews", "1"], - ["total_rating", "2"], - ["last_rating", "1"], - ["max_rate", "2"], - ["min_rate", "5"], - ["data_label", "rating"] - ], - "content": "", - "sig": "456fdc0589a5ffe1b55d5474cef2826bf01f458d63cf409490def9c5af31052e0461d38aed4f386f5dcea999e9fe6001d27d592dbba54a0420687dce0652322f" - } - ] -] diff --git a/test/examples/release.json b/test/examples/release.json deleted file mode 100644 index d9373c46..00000000 --- a/test/examples/release.json +++ /dev/null @@ -1,94 +0,0 @@ -[ - "Release", - "After confirming the buyer sent the fiat money, the seller should send a message to Mostro indicating that sats should be delivered to the buyer, the rumor's content of the message will look like this:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "release", - "content": null - } - }, - "Mostro response", - "Here an example of the Mostro response to the seller:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "hold-invoice-payment-settled", - "content": null - } - }, - "And a message to the buyer to let him know that the sats were released:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "released", - "content": null - } - }, - "Buyer receives sats", - "Right after seller release sats Mostro will try to pay the buyer's lightning invoice, if the payment is successful Mostro will send a message to the buyer indicating that the purchase was completed:", - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "purchase-completed", - "content": null - } - }, - "Mostro updates the parameterized replaceable event with d tag ede61c96-4c13-4519-bf3a-dcf7f1e9d842 to change the status to settled-hold-invoice:", - [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "settled-hold-invoice"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "Mostro will then attempt to pay the buyer's invoice, if the payment successds Mostro updates the parameterized replaceable event with d tag ede61c96-4c13-4519-bf3a-dcf7f1e9d842 to change the status to success:", - [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "success"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -] diff --git a/test/examples/seller_pays_hold_invoive.json b/test/examples/seller_pays_hold_invoive.json deleted file mode 100644 index 1d8b0cc2..00000000 --- a/test/examples/seller_pays_hold_invoive.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "pay-invoice", - "content": { - "payment_request": [ - { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "waiting-payment", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 1698937797 - }, - "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" - ] - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "buyer-took-order", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "active", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", - "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", - "buyer_invoice": null, - "created_at": 1698937797 - } - } - } - }, - "buyer_response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "hold-invoice-payment-accepted", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "active", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": "0000147e939bef2b81c27af4c1b702c90c3843f7212a34934bff1e049b7f1427", - "master_seller_pubkey": "00000ba40c5795451705bb9c165b3af93c846894d3062a9cd7fcba090eb3bf78", - "buyer_invoice": null, - "created_at": 1698937797 - } - } - } - }, "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "active"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], "waiting_invoice": - { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "waiting-buyer-invoice", - "content": null - } - }, - "buyer":{ - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "add-invoice", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "sell", - "status": "waiting-buyer-invoice", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 1698937797 - } - } - } - }, "update_event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-buyer-invoice"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "buyer_sends_invoice": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "add-invoice", - "content": { - "payment_request": [ - null, - "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" - ] - } - } - } -} diff --git a/test/examples/take_buy_order.json b/test/examples/take_buy_order.json deleted file mode 100644 index a176dcf3..00000000 --- a/test/examples/take_buy_order.json +++ /dev/null @@ -1,167 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-buy", - "content": null - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "pay-invoice", - "content": { - "payment_request": [ - { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "kind": "buy", - "status": "waiting-payment", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": 1698957793 - }, - "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" - ] - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-payment"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "buyer": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "waiting-seller-to-pay", - "content": null - } - }, - "seller": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "waiting-buyer-invoice", - "content": null - } - }, - "update_event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-buyer-invoice"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ], - "seller_update": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "add-invoice", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "status": "waiting-buyer-invoice", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "created_at": null - } - } - } - }, - "buyer_sends_ln": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "pubkey": null, - "action": "add-invoice", - "content": { - "payment_request": [ - null, - "lnbcrt78510n1pj59wmepp50677g8tffdqa2p8882y0x6newny5vtz0hjuyngdwv226nanv4uzsdqqcqzzsxqyz5vqsp5skn973360gp4yhlpmefwvul5hs58lkkl3u3ujvt57elmp4zugp4q9qyyssqw4nzlr72w28k4waycf27qvgzc9sp79sqlw83j56txltz4va44j7jda23ydcujj9y5k6k0rn5ms84w8wmcmcyk5g3mhpqepf7envhdccp72nz6e" - ] - } - } - }, - "final_event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "active"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} diff --git a/test/examples/take_buy_range_order.json b/test/examples/take_buy_range_order.json deleted file mode 100644 index d3b46d56..00000000 --- a/test/examples/take_buy_range_order.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-buy", - "content": { - "amount": 15 - } - } - } -} diff --git a/test/examples/take_sell_order.json b/test/examples/take_sell_order.json deleted file mode 100644 index 2d63886f..00000000 --- a/test/examples/take_sell_order.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-sell", - "content": null - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "add-invoice", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "amount": 7851, - "fiat_code": "VES", - "fiat_amount": 100, - "payment_method": "face to face", - "premium": 1, - "buyer_pubkey": null, - "seller_pubkey": null - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-buyer-invoice"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} diff --git a/test/examples/take_sell_order_ln.json b/test/examples/take_sell_order_ln.json deleted file mode 100644 index 5a44c90a..00000000 --- a/test/examples/take_sell_order_ln.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-sell", - "content": { - "payment_request": [null, "mostro_p2p@ln.tips"] - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "waiting-seller-to-pay", - "content": null - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-payment"], - ["amt", "7851"], - ["fa", "100"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} diff --git a/test/examples/take_sell_range_order.json b/test/examples/take_sell_range_order.json deleted file mode 100644 index dea23339..00000000 --- a/test/examples/take_sell_range_order.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "order": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-sell", - "content": { - "amount": 15 - } - } - }, - "order_with_ln": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "take-sell", - "content": { - "payment_request": [null, "mostro_p2p@ln.tips", 15] - } - } - }, - "response": { - "order": { - "version": 1, - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "action": "add-invoice", - "content": { - "order": { - "id": "ede61c96-4c13-4519-bf3a-dcf7f1e9d842", - "amount": 7851, - "fiat_code": "VES", - "min_amount": 10, - "max_amount": 20, - "fiat_amount": 15, - "payment_method": "face to face", - "premium": 1, - "master_buyer_pubkey": null, - "master_seller_pubkey": null, - "buyer_invoice": null, - "created_at": null, - "expires_at": null - } - } - } - }, - "event": [ - "EVENT", - "RAND", - { - "id": "eb0582360ebd3836c90711f774fbecb27e600f4a5fedf4fc2d16fc852f8380b1", - "pubkey": "dbe0b1be7aafd3cfba92d7463edbd4e33b2969f61bd554d37ac56f032e13355a", - "created_at": 1702549437, - "kind": 38383, - "tags": [ - ["d", "ede61c96-4c13-4519-bf3a-dcf7f1e9d842"], - ["k", "sell"], - ["f", "VES"], - ["s", "waiting-buyer-invoice"], - ["amt", "7851"], - ["fa", "15"], - ["pm", "face to face"], - ["premium", "1"], - ["network", "mainnet"], - ["layer", "lightning"], - ["expiration", "1719391096"], - ["y", "mostrop2p"], - ["z", "order"] - ], - "content": "", - "sig": "a835f8620db3ebdd9fa142ae99c599a61da86321c60f7c9fed0cc57169950f4121757ff64a5e998baccf6b68272aa51819c3e688d8ad586c0177b3cd1ab09c0f" - } - ] -} From c3244d6e5120af2a973d7de7d9add7d17f482665 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Sun, 17 Nov 2024 23:18:31 -0800 Subject: [PATCH 3/9] fix random date error --- lib/core/utils/nostr_utils.dart | 34 +++++++----------- .../add_order/bloc/add_order_bloc.dart | 27 +++++++++++--- .../add_order/bloc/add_order_event.dart | 8 +++++ lib/services/mostro_service.dart | 36 ++++++++----------- 4 files changed, 57 insertions(+), 48 deletions(-) diff --git a/lib/core/utils/nostr_utils.dart b/lib/core/utils/nostr_utils.dart index 476e8b46..fa959fd0 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/core/utils/nostr_utils.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'dart:typed_data'; +import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:elliptic/elliptic.dart'; @@ -10,10 +10,7 @@ class NostrUtils { // Generación de claves static NostrKeyPairs generateKeyPair() { - var ec = getS256(); - var priv = ec.generatePrivateKey(); - - return NostrKeyPairs(private: priv.toHex()); + return NostrKeyPairs(private: generatePrivateKey()); } static NostrKeyPairs generateKeyPairFromPrivateKey(String privateKey) { @@ -22,7 +19,7 @@ class NostrUtils { } static String generatePrivateKey() { - return _instance.keysService.generatePrivateKey(); + return getS256().generatePrivateKey().toHex(); } // Codificación y decodificación de claves @@ -121,6 +118,13 @@ class NostrUtils { return digest.toString(); // Devuelve el ID como una cadena hex } + static DateTime randomNow() { + final now = DateTime.now(); + final randomSeconds = + Random().nextInt(2 * 24 * 60 * 60); + return now.subtract(Duration(seconds: randomSeconds)); + } + // NIP-59 y NIP-44 funciones static Future createNIP59Event( String content, String recipientPubKey, String senderPrivateKey) async { @@ -137,15 +141,13 @@ class NostrUtils { ], ); - randomNow() => DateTime(createdAt.millisecondsSinceEpoch ~/ 1000); - - final encryptedContent = _encryptNIP44( + final encryptedContent = await _encryptNIP44( jsonEncode(rumorEvent.toMap()), senderPrivateKey, '02$recipientPubKey'); final sealEvent = NostrEvent.fromPartialData( kind: 13, keyPairs: senderKeyPair, - content: await encryptedContent, + content: encryptedContent, createdAt: randomNow(), ); @@ -180,10 +182,6 @@ class NostrUtils { final finalDecryptedContent = await _decryptNIP44( rumorEvent.content ?? '', privateKey, rumorEvent.pubkey); - print(finalDecryptedContent); - print( - NostrEvent.canBeDeserialized('["EVENT", "", $finalDecryptedContent]')); - final wrap = jsonDecode(finalDecryptedContent) as Map; return NostrEvent( @@ -207,17 +205,9 @@ class NostrUtils { .toList(), ), subscriptionId: '', - ); } - static Uint8List _calculateSharedSecret(String privateKey, String publicKey) { - // Nota: Esta implementación puede necesitar ajustes dependiendo de cómo - // dart_nostr maneje la generación de secretos compartidos. - // Posiblemente necesites usar una biblioteca de criptografía adicional aquí. - final sharedPoint = generateKeyPairFromPrivateKey(privateKey).public; - return Uint8List.fromList(sha256.convert(utf8.encode(sharedPoint)).bytes); - } static Future _encryptNIP44( String content, String privkey, String pubkey) async { diff --git a/lib/presentation/add_order/bloc/add_order_bloc.dart b/lib/presentation/add_order/bloc/add_order_bloc.dart index 94bd80db..8dc506d9 100644 --- a/lib/presentation/add_order/bloc/add_order_bloc.dart +++ b/lib/presentation/add_order/bloc/add_order_bloc.dart @@ -1,4 +1,5 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'add_order_event.dart'; import 'add_order_state.dart'; @@ -9,18 +10,36 @@ class AddOrderBloc extends Bloc { AddOrderBloc(this.mostroService) : super(const AddOrderState()) { on(_onChangeOrderType); on(_onSubmitOrder); + on(_onOrderUpdateReceived); } void _onChangeOrderType(ChangeOrderType event, Emitter emit) { emit(state.copyWith(currentType: event.orderType)); } - void _onSubmitOrder(SubmitOrder event, Emitter emit) async { - + Future _onSubmitOrder( + SubmitOrder event, Emitter emit) async { emit(state.copyWith(status: AddOrderStatus.submitting)); - await mostroService.publishOrder(event.order); + final order = await mostroService.publishOrder(event.order); + add(OrderUpdateReceived(order)); + } - emit(state.copyWith(status: AddOrderStatus.submitted)); + void _onOrderUpdateReceived( + OrderUpdateReceived event, Emitter emit) { + switch (event.order.action) { + case Action.newOrder: + print(event.order.content!.toJson()); + emit(state.copyWith(status: AddOrderStatus.submitted)); + break; + case Action.outOfRangeSatsAmount: + case Action.outOfRangeFiatAmount: + print("Error! ${event.order.action.value}"); + emit(state.copyWith( + status: AddOrderStatus.failure, errorMessage: "Invalid amount")); + break; + default: + break; + } } } diff --git a/lib/presentation/add_order/bloc/add_order_event.dart b/lib/presentation/add_order/bloc/add_order_event.dart index 87d00023..f2aa0074 100644 --- a/lib/presentation/add_order/bloc/add_order_event.dart +++ b/lib/presentation/add_order/bloc/add_order_event.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; abstract class AddOrderEvent extends Equatable { @@ -44,3 +45,10 @@ class SubmitOrder extends AddOrderEvent { List get props => [fiatCode, fiatAmount, satsAmount, paymentMethod, orderType]; } + +class OrderUpdateReceived extends AddOrderEvent { + final MostroMessage order; + + const OrderUpdateReceived(this.order); +} + diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index d0711b49..7224d8c2 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:mostro_mobile/core/config.dart'; +import 'package:mostro_mobile/data/models/content.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; @@ -27,7 +28,8 @@ class MostroService { return _nostrService.subscribeToEvents(filter); } - Future publishOrder(Order order) async { + Future publishOrder( + Order order) async { final session = await _secureStorageManager.newSession(); final content = jsonEncode({ @@ -45,18 +47,11 @@ class MostroService { final filter = NostrFilter(p: [session.publicKey]); - subscribeToOrders(filter).listen((event) async { - final response = - await _nostrService.decryptNIP59Event(event, session.privateKey); - - final orderResponse = MostroMessage.deserialized(response.content!); - - if (orderResponse.requestId != null) { - _orders[orderResponse.requestId!] = orderResponse; - _sessions[orderResponse.requestId!] = session; - session.eventId = orderResponse.requestId; - } - }); + return await subscribeToOrders(filter).asyncMap((event) async { + return await _nostrService.decryptNIP59Event(event, session.privateKey); + }).map((event) { + return MostroMessage.deserialized(event.content!); + }).first; } Future takeSellOrder(String orderId, {int? amount}) async { @@ -88,7 +83,7 @@ class MostroService { }); } - Future takeBuyOrder(String orderId, {int? amount}) async { + Future> takeBuyOrder(String orderId, {int? amount}) async { final session = await _secureStorageManager.newSession(); session.eventId = orderId; @@ -105,14 +100,11 @@ class MostroService { await _nostrService.publishEvent(event); final filter = NostrFilter(p: [session.publicKey]); - subscribeToOrders(filter).listen((event) async { - final response = - await _nostrService.decryptNIP59Event(event, session.privateKey); - - final orderResponse = MostroMessage.deserialized(response.content!); - - print(response); - }); + return await subscribeToOrders(filter).asyncMap((event) async { + return await _nostrService.decryptNIP59Event(event, session.privateKey); + }).map((event) { + return MostroMessage.deserialized(event.content!); + }).first; } Future cancelOrder(String orderId) async { From 09f44ee40684dcea1467d0ddb9cf153a23e82482 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Tue, 19 Nov 2024 16:29:07 -0800 Subject: [PATCH 4/9] Rabbit suggestions + l10n --- l10n.yaml | 3 + lib/core/config.dart | 4 +- lib/core/utils/auth_utils.dart | 8 +- lib/core/utils/nostr_utils.dart | 146 ++++-- lib/data/models/content.dart | 12 + lib/data/models/conversion_result.dart | 99 +++- lib/data/models/enums/action.dart | 19 +- lib/data/models/enums/order_type.dart | 12 +- lib/data/models/mostro_message.dart | 28 +- lib/data/models/nostr_event.dart | 32 +- lib/data/models/order.dart | 53 +- lib/data/models/payment_request.dart | 37 +- lib/data/models/peer.dart | 8 +- lib/data/rating.dart | 34 ++ .../repositories/open_orders_repository.dart | 18 +- .../repositories/secure_storage_manager.dart | 38 +- lib/generated/intl/messages_all.dart | 63 +++ lib/generated/intl/messages_en.dart | 163 ++++++ lib/generated/l10n.dart | 494 ++++++++++++++++++ lib/l10n/intl_en.arb | 44 ++ lib/main.dart | 9 + .../add_order/bloc/add_order_bloc.dart | 13 +- .../add_order/screens/add_order_screen.dart | 4 +- .../chat_list/screens/chat_list_screen.dart | 2 +- .../home/screens/home_screen.dart | 2 +- .../order/bloc/order_details_bloc.dart | 28 +- .../order/bloc/order_details_event.dart | 10 + .../order/screens/order_details_screen.dart | 281 +++++----- .../widgets/currency_dropdown.dart | 27 +- .../widgets/exchange_rate_widget.dart | 13 +- lib/presentation/widgets/order_list_item.dart | 2 +- lib/providers/event_store_providers.dart | 10 +- lib/providers/exchange_service_provider.dart | 1 + lib/providers/riverpod_providers.dart | 5 +- lib/services/exchange_service.dart | 42 +- lib/services/mostro_service.dart | 46 +- lib/services/yadio_exchange_service.dart | 36 +- pubspec.lock | 15 +- pubspec.yaml | 13 +- 39 files changed, 1535 insertions(+), 339 deletions(-) create mode 100644 l10n.yaml create mode 100644 lib/data/rating.dart create mode 100644 lib/generated/intl/messages_all.dart create mode 100644 lib/generated/intl/messages_en.dart create mode 100644 lib/generated/l10n.dart create mode 100644 lib/l10n/intl_en.arb diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..c10580f6 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: intl_en.arb +output-dir: lib/generated diff --git a/lib/core/config.dart b/lib/core/config.dart index 7e054592..41610a77 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -4,14 +4,14 @@ class Config { // Configuración de Nostr static const List 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'; diff --git a/lib/core/utils/auth_utils.dart b/lib/core/utils/auth_utils.dart index b4b27bd4..42825eca 100644 --- a/lib/core/utils/auth_utils.dart +++ b/lib/core/utils/auth_utils.dart @@ -7,14 +7,16 @@ class AuthUtils { } static Future verifyPin(String inputPin) async { - return true; + throw UnimplementedError('verifyPin is not implemented yet'); } - static Future deleteCredentials() async {} + static Future deleteCredentials() async { + throw UnimplementedError('deleteCredentials is not implemented yet'); + } static Future enableBiometrics() async {} static Future isBiometricsEnabled() async { - return true; + throw UnimplementedError('isBiometricsEnabled is not implemented yet'); } } diff --git a/lib/core/utils/nostr_utils.dart b/lib/core/utils/nostr_utils.dart index fa959fd0..c2db4570 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/core/utils/nostr_utils.dart @@ -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 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(); @@ -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, @@ -173,49 +198,90 @@ class NostrUtils { static Future 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; - - 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>.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; + + // 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>.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 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 _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 _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'); + } } } diff --git a/lib/data/models/content.dart b/lib/data/models/content.dart index ae33d3a3..c5a80a11 100644 --- a/lib/data/models/content.dart +++ b/lib/data/models/content.dart @@ -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 toJson(); + + factory Content.fromJson(Map 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'); + } } diff --git a/lib/data/models/conversion_result.dart b/lib/data/models/conversion_result.dart index cdd2e561..48c2240f 100644 --- a/lib/data/models/conversion_result.dart +++ b/lib/data/models/conversion_result.dart @@ -1,7 +1,12 @@ +/// 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({ @@ -9,34 +14,110 @@ class ConversionResult { required this.result, required this.rate, required this.timestamp, - }); + }) { + if (timestamp < 0) { + throw ArgumentError('Timestamp cannot be negative'); + } + } factory ConversionResult.fromJson(Map 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 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 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 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)'; +} \ No newline at end of file diff --git a/lib/data/models/enums/action.dart b/lib/data/models/enums/action.dart index 14ab1d57..432d33b4 100644 --- a/lib/data/models/enums/action.dart +++ b/lib/data/models/enums/action.dart @@ -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; } } diff --git a/lib/data/models/enums/order_type.dart b/lib/data/models/enums/order_type.dart index f282ac41..939474f3 100644 --- a/lib/data/models/enums/order_type.dart +++ b/lib/data/models/enums/order_type.dart @@ -7,9 +7,13 @@ enum OrderType { const OrderType(this.value); static OrderType fromString(String value) { - return OrderType.values.firstWhere( - (k) => k.value == value, - orElse: () => throw ArgumentError('Invalid Kind: $value'), - ); + switch (value) { + case 'buy': + return OrderType.buy; + case 'sell': + return OrderType.sell; + default: + throw ArgumentError('Invalid OrderType: $value'); + } } } diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index b2b5ba6b..5ccf9c12 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -24,8 +24,30 @@ class MostroMessage { } factory MostroMessage.deserialized(String data) { - final decoded = jsonDecode(data); - final event = decoded as Map; - return MostroMessage(action: Action.fromString(event['order']['action']), requestId: event['order']['id']); + try { + final decoded = jsonDecode(data); + final event = decoded as Map; + final order = event['order'] != null + ? event['order'] as Map + : throw FormatException('Missing order object'); + + final action = order['action'] != null + ? Action.fromString(order['action']) + : throw FormatException('Missing action field'); + + final id = order['id'] != null + ? order['id'] as String? + : throw FormatException('Missing id field'); + + final content = order['content'] ?? Content.fromJson(event['order']['content']) as T; + + return MostroMessage( + action: action, + requestId: id, + content: content, + ); + } catch (e) { + throw FormatException('Failed to deserialize MostroMessage: $e'); + } } } diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index b0d1a524..9e3d05cb 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -1,4 +1,6 @@ import 'dart:convert'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/rating.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/data/models/order.dart'; @@ -7,7 +9,7 @@ extension NostrEventExtensions on NostrEvent { // Getters para acceder fácilmente a los tags específicos String? get recipient => _getTagValue('p'); String? get orderId => _getTagValue('d'); - String? get orderType => _getTagValue('k'); + OrderType? get orderType => OrderType.fromString(_getTagValue('k')!); String? get currency => _getTagValue('f'); String? get status => _getTagValue('s'); String? get amount => _getTagValue('amt'); @@ -15,7 +17,9 @@ extension NostrEventExtensions on NostrEvent { List get paymentMethods => _getTagValue('pm')?.split(',') ?? []; String? get premium => _getTagValue('premium'); String? get source => _getTagValue('source'); - String? get rating => _getTagValue('rating') ?? "0"; + Rating? get rating => _getTagValue('rating') != null + ? Rating.deserialized(_getTagValue('rating')!) + : null; String? get network => _getTagValue('network'); String? get layer => _getTagValue('layer'); String? get name => _getTagValue('name') ?? 'Anon'; @@ -23,19 +27,29 @@ extension NostrEventExtensions on NostrEvent { String? get bond => _getTagValue('bond'); String? get expiration => _timeAgo(_getTagValue('expiration')); String? get platform => _getTagValue('y'); - Order? get document => Order.fromJson(jsonDecode(_getTagValue('z')!)); + Order? get document { + final jsonString = _getTagValue('z'); + if (jsonString != null) { + return Order.fromJson(jsonDecode(jsonString)); + } + return null; + } String? _getTagValue(String key) { final tag = tags?.firstWhere((t) => t[0] == key, orElse: () => []); - return tag!.length > 1 ? tag[1] : null; + return (tag != null && tag.length > 1) ? tag[1] : null; } String _timeAgo(String? ts) { if (ts == null) return "invalid date"; - final timestamp = int.tryParse(ts) ?? 0; - final DateTime eventTime = - DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) - .subtract(Duration(hours: 36)); - return timeago.format(eventTime, allowFromNow: true); + final timestamp = int.tryParse(ts); + if (timestamp != null && timestamp > 0) { + final DateTime eventTime = + DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) + .subtract(Duration(hours: 36)); + return timeago.format(eventTime, allowFromNow: true); + } else { + return "invalid date"; + } } } diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 4b3d4081..afa5f28a 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -76,31 +76,48 @@ class Order implements Content { } factory Order.fromJson(Map json) { + // Validate required fields + void validateField(String field) { + if (!json.containsKey(field)) { + throw FormatException('Missing required field: $field'); + } + } + + // Validate required fields + ['kind', 'status', 'fiat_code', 'fiat_amount', 'payment_method', 'premium'] + .forEach(validateField); + + // Safe type casting + T? safeCast(String key, T Function(dynamic) converter) { + final value = json[key]; + return value == null ? null : converter(value); + } + return Order( - id: json['id'] as String?, - kind: OrderType.fromString(json['kind'] as String), - status: Status.fromString(json['status'] as String), - amount: json['amount'] as int, - fiatCode: json['fiat_code'] as String, - minAmount: json['min_amount'] as int?, - maxAmount: json['max_amount'] as int?, - fiatAmount: json['fiat_amount'] as int, - paymentMethod: json['payment_method'] as String, - premium: json['premium'] as int, - masterBuyerPubkey: json['master_buyer_pubkey'] as String?, - masterSellerPubkey: json['master_seller_pubkey'] as String?, - buyerInvoice: json['buyer_invoice'] as String?, - createdAt: json['created_at'] as int?, - expiresAt: json['expires_at'] as int?, - buyerToken: json['buyer_token'] as int?, - sellerToken: json['seller_token'] as int?, + id: safeCast('id', (v) => v.toString()), + kind: OrderType.fromString(json['kind'].toString()), + status: Status.fromString(json['status']), + amount: json['amount'], + fiatCode: json['fiat_code'], + minAmount: json['min_amount'], + maxAmount: json['max_amount'], + fiatAmount: json['fiat_amount'], + paymentMethod: json['payment_method'], + premium: json['premium'], + masterBuyerPubkey: json['master_buyer_pubkey'], + masterSellerPubkey: json['master_seller_pubkey'], + buyerInvoice: json['buyer_invoice'], + createdAt: json['created_at'], + expiresAt: json['expires_at'], + buyerToken: json['buyer_token'], + sellerToken: json['seller_token'], ); } factory Order.fromEvent(NostrEvent event) { return Order( id: event.orderId, - kind: OrderType.fromString(event.orderType!), + kind: event.orderType!, status: Status.fromString(event.status!), amount: event.amount as int, fiatCode: event.currency!, diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart index 4f8d677f..070ab9cf 100644 --- a/lib/data/models/payment_request.dart +++ b/lib/data/models/payment_request.dart @@ -6,22 +6,43 @@ class PaymentRequest implements Content { final String? lnInvoice; final int? amount; - PaymentRequest( - {required this.order, required this.lnInvoice, required this.amount}); - + PaymentRequest({ + this.order, + this.lnInvoice, + this.amount, + }) { + // At least one parameter should be non-null + if (order == null && lnInvoice == null && amount == null) { + throw ArgumentError('At least one parameter must be provided'); + } + } + @override Map toJson() { - final result = { - type: [order?.toJson(), lnInvoice] - }; - + final typeKey = type; + final List values = []; + + values.add(order?.toJson()); + values.add(lnInvoice); + if (amount != null) { - result[type]!.add(amount); + values.add(amount); } + + final result = { + typeKey: values + }; return result; } + factory PaymentRequest.fromJson(List json) { + return PaymentRequest( + order: json[0] ?? Order.fromJson(json[0]), + lnInvoice: json[1], + amount: json[2]); + } + @override String get type => 'payment_request'; } diff --git a/lib/data/models/peer.dart b/lib/data/models/peer.dart index ab167c5a..b39d01ae 100644 --- a/lib/data/models/peer.dart +++ b/lib/data/models/peer.dart @@ -6,11 +6,15 @@ class Peer implements Content { Peer({required this.publicKey}); factory Peer.fromJson(Map json) { + final pubkey = json['pubkey']; + if (pubkey == null || pubkey is! String) { + throw FormatException('Invalid or missing pubkey in JSON'); + } return Peer( - publicKey: json['pubkey'] as String, + publicKey: pubkey, ); } - + @override Map toJson() { return { diff --git a/lib/data/rating.dart b/lib/data/rating.dart new file mode 100644 index 00000000..96f7ea3a --- /dev/null +++ b/lib/data/rating.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +class Rating { + final int totalReviews; + final double totalRating; + final int lastRating; + final int maxRate; + final int minRate; + + Rating( + {required this.totalReviews, + required this.totalRating, + required this.lastRating, + required this.maxRate, + required this.minRate}); + + factory Rating.deserialized(String data) { + if (data == 'none') { + return Rating( + totalReviews: 0, + totalRating: 0.0, + lastRating: 0, + maxRate: 0, + minRate: 0); + } + final json = jsonDecode(data) as Map; + return Rating( + totalReviews: json['rating']['total_reviews'], + totalRating: json['rating']['total_rating'], + lastRating: json['rating']['last_rating'], + maxRate: json['rating']['max_rate'], + minRate: json['rating']['min_rate']); + } +} diff --git a/lib/data/repositories/open_orders_repository.dart b/lib/data/repositories/open_orders_repository.dart index ceb3cb28..866007d9 100644 --- a/lib/data/repositories/open_orders_repository.dart +++ b/lib/data/repositories/open_orders_repository.dart @@ -15,15 +15,31 @@ class OpenOrdersRepository implements OrderRepository { OpenOrdersRepository(this._nostrService); + StreamSubscription? _subscription; + + /// Subscribes to events matching the given filter. + /// + /// @param filter The filter criteria for events. + /// @throws ArgumentError if filter is null void subscribe(NostrFilter filter) { - _nostrService.subscribeToEvents(filter).listen((event) { + ArgumentError.checkNotNull(filter, 'filter'); + + // Cancel existing subscription if any + _subscription?.cancel(); + + _subscription = _nostrService.subscribeToEvents(filter).listen((event) { final key = '${event.kind}-${event.pubkey}-${event.orderId}'; _events[key] = event; _eventStreamController.add(_events.values.toList()); + }, onError: (error) { + // Log error and optionally notify listeners + print('Error in order subscription: $error'); }); } void dispose() { + _subscription?.cancel(); _eventStreamController.close(); + _events.clear(); } } diff --git a/lib/data/repositories/secure_storage_manager.dart b/lib/data/repositories/secure_storage_manager.dart index 17c69903..53101118 100644 --- a/lib/data/repositories/secure_storage_manager.dart +++ b/lib/data/repositories/secure_storage_manager.dart @@ -7,6 +7,8 @@ import 'package:mostro_mobile/core/utils/nostr_utils.dart'; class SecureStorageManager { Timer? _cleanupTimer; final int sessionExpirationHours = 48; + static const cleanupIntervalMinutes = 30; + static const maxBatchSize = 100; SecureStorageManager() { _initializeCleanup(); @@ -44,24 +46,40 @@ class SecureStorageManager { } Future clearExpiredSessions() async { - final prefs = await SharedPreferences.getInstance(); - final now = DateTime.now(); - final allKeys = prefs.getKeys(); + try { + final prefs = await SharedPreferences.getInstance(); + final now = DateTime.now(); + final allKeys = prefs.getKeys(); + int processedCount = 0; - for (var key in allKeys) { - final sessionJson = prefs.getString(key); - if (sessionJson != null) { - final session = Session.fromJson(jsonDecode(sessionJson)); - if (now.difference(session.startTime).inHours >= sessionExpirationHours) { - await prefs.remove(key); + for (var key in allKeys) { + if (processedCount >= maxBatchSize) { + // Schedule remaining cleanup for next run + break; + } + final sessionJson = prefs.getString(key); + if (sessionJson != null) { + try { + final session = Session.fromJson(jsonDecode(sessionJson)); + if (now.difference(session.startTime).inHours >= sessionExpirationHours) { + await prefs.remove(key); + processedCount++; + } + } catch (e) { + print('Error processing session $key: $e'); + await prefs.remove(key); + processedCount++; + } } } + } catch (e) { + print('Error during session cleanup: $e'); } } void _initializeCleanup() { clearExpiredSessions(); - _cleanupTimer = Timer.periodic(Duration(minutes: 30), (timer) { + _cleanupTimer = Timer.periodic(Duration(minutes: cleanupIntervalMinutes), (timer) { clearExpiredSessions(); }); } diff --git a/lib/generated/intl/messages_all.dart b/lib/generated/intl/messages_all.dart new file mode 100644 index 00000000..203415cc --- /dev/null +++ b/lib/generated/intl/messages_all.dart @@ -0,0 +1,63 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that looks up messages for specific locales by +// delegating to the appropriate library. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:implementation_imports, file_names, unnecessary_new +// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering +// ignore_for_file:argument_type_not_assignable, invalid_assignment +// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases +// ignore_for_file:comment_references + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; +import 'package:intl/src/intl_helpers.dart'; + +import 'messages_en.dart' as messages_en; + +typedef Future LibraryLoader(); +Map _deferredLibraries = { + 'en': () => new SynchronousFuture(null), +}; + +MessageLookupByLibrary? _findExact(String localeName) { + switch (localeName) { + case 'en': + return messages_en.messages; + default: + return null; + } +} + +/// User programs should call this before using [localeName] for messages. +Future initializeMessages(String localeName) { + var availableLocale = Intl.verifiedLocale( + localeName, (locale) => _deferredLibraries[locale] != null, + onFailure: (_) => null); + if (availableLocale == null) { + return new SynchronousFuture(false); + } + var lib = _deferredLibraries[availableLocale]; + lib == null ? new SynchronousFuture(false) : lib(); + initializeInternalMessageLookup(() => new CompositeMessageLookup()); + messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor); + return new SynchronousFuture(true); +} + +bool _messagesExistFor(String locale) { + try { + return _findExact(locale) != null; + } catch (e) { + return false; + } +} + +MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) { + var actualLocale = + Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null); + if (actualLocale == null) return null; + return _findExact(actualLocale); +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart new file mode 100644 index 00000000..2dda54e3 --- /dev/null +++ b/lib/generated/intl/messages_en.dart @@ -0,0 +1,163 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a en locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'en'; + + static String m0(amount, fiat_code, fiat_amount, expiration_seconds) => + "Please send me an invoice for ${amount} satoshis equivalent to ${fiat_code} ${fiat_amount}. This is where I\'ll send the funds upon completion of the trade. If you don\'t provide the invoice within ${expiration_seconds} this trade will be cancelled."; + + static String m1(npub) => "You have successfully added the solver ${npub}."; + + static String m2(id) => "You have cancelled the order ID: ${id}!"; + + static String m3(id) => "Admin has cancelled the order ID: ${id}!"; + + static String m4(id) => "You have completed the order ID: ${id}!"; + + static String m5(id) => "Admin has completed the order ID: ${id}!"; + + static String m6(details) => + "Here are the details of the dispute order you have taken: ${details}. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed."; + + static String m7(admin_npub) => + "The solver ${admin_npub} will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token."; + + static String m8(buyer_npub, fiat_code, fiat_amount, payment_method) => + "Get in touch with the buyer, this is their npub ${buyer_npub} to inform them how to send you ${fiat_code} ${fiat_amount} through ${payment_method}. I will notify you once the buyer indicates the fiat money has been sent. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first."; + + static String m9(id) => "You have cancelled the order ID: ${id}!"; + + static String m10(action) => + "You are not allowed to ${action} for this order!"; + + static String m11(id) => "Order ${id} has been successfully cancelled!"; + + static String m12(id) => + "Your counterparty wants to cancel order ID: ${id}. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message."; + + static String m13(id) => + "You have initiated the cancellation of the order ID: ${id}. Your counterparty must agree to the cancellation too. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first."; + + static String m14(id, user_token) => + "Your counterparty has initiated a dispute for order Id: ${id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: ${user_token}."; + + static String m15(id, user_token) => + "You have initiated a dispute for order Id: ${id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: ${user_token}."; + + static String m16(seller_npub) => + "I have informed ${seller_npub} that you have sent the fiat money. When the seller confirms they have received your fiat money, they should release the funds. If they refuse, you can open a dispute."; + + static String m17(buyer_npub) => + "${buyer_npub} has informed that they have sent you the fiat money. Once you confirm receipt, please release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, send me release-order-message."; + + static String m18(seller_npub, id, fiat_code, fiat_amount, payment_method) => + "Get in touch with the seller, this is their npub ${seller_npub} to get the details on how to send the fiat money for the order ${id}, you must send ${fiat_code} ${fiat_amount} using ${payment_method}. Once you send the fiat money, please let me know with fiat-sent."; + + static String m19(buyer_npub) => + "Your Sats sale has been completed after confirming the payment from ${buyer_npub}."; + + static String m20(amount) => + "The amount stated in the invoice is incorrect. Please send an invoice with an amount of ${amount} satoshis, an invoice without an amount, or a lightning address."; + + static String m21(action) => + "You did not create this order and are not authorized to ${action} it."; + + static String m22(expiration_hours) => + "Your offer has been published! Please wait until another user picks your order. It will be available for ${expiration_hours} hours. You can cancel this order before another user picks it up by executing: cancel."; + + static String m23(action, id, order_status) => + "You are not allowed to ${action} because order Id ${id} status is ${order_status}."; + + static String m24(min_amount, max_amount) => + "The requested amount is incorrect and may be outside the acceptable range. The minimum is ${min_amount} and the maximum is ${max_amount}."; + + static String m25(min_order_amount, max_order_amount) => + "The allowed Sats amount for this Mostro is between min ${min_order_amount} and max ${max_order_amount}. Please enter an amount within this range."; + + static String m26(amount, fiat_code, fiat_amount, expiration_seconds) => + "Please pay this hold invoice of ${amount} Sats for ${fiat_code} ${fiat_amount} to start the operation. If you do not pay it within ${expiration_seconds} the trade will be cancelled."; + + static String m27(payment_attempts, payment_retries_interval) => + "I tried to send you the Sats but the payment of your invoice failed. I will try ${payment_attempts} more times in ${payment_retries_interval} minutes window. Please ensure your node/wallet is online."; + + static String m28(seller_npub) => + "${seller_npub} has already released the Sats! Expect your invoice to be paid any time. Remember your wallet needs to be online to receive through the Lightning Network."; + + static String m29(expiration_seconds) => + "Payment received! Your Sats are now \'held\' in your own wallet. Please wait a bit. I\'ve requested the buyer to provide an invoice. Once they do, I\'ll connect you both. If they do not do so within ${expiration_seconds} your Sats will be available in your wallet again and the trade will be cancelled."; + + static String m30(id, expiration_seconds) => + "Please wait a bit. I\'ve sent a payment request to the seller to send the Sats for the order ID ${id}. Once the payment is made, I\'ll connect you both. If the seller doesn\'t complete the payment within ${expiration_seconds} minutes the trade will be cancelled."; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "add_invoice": m0, + "admin_add_solver": m1, + "admin_canceled_admin": m2, + "admin_canceled_users": m3, + "admin_settled_admin": m4, + "admin_settled_users": m5, + "admin_took_dispute_admin": m6, + "admin_took_dispute_users": m7, + "buyer_invoice_accepted": MessageLookupByLibrary.simpleMessage( + "Invoice has been successfully saved!"), + "buyer_took_order": m8, + "canceled": m9, + "cant_do": m10, + "cooperative_cancel_accepted": m11, + "cooperative_cancel_initiated_by_peer": m12, + "cooperative_cancel_initiated_by_you": m13, + "dispute_initiated_by_peer": m14, + "dispute_initiated_by_you": m15, + "fiat_sent_ok_buyer": m16, + "fiat_sent_ok_seller": m17, + "hold_invoice_payment_accepted": m18, + "hold_invoice_payment_canceled": MessageLookupByLibrary.simpleMessage( + "The invoice was cancelled; your Sats will be available in your wallet again."), + "hold_invoice_payment_settled": m19, + "incorrect_invoice_amount_buyer_add_invoice": m20, + "incorrect_invoice_amount_buyer_new_order": + MessageLookupByLibrary.simpleMessage( + "An invoice with non-zero amount was received for the new order. Please send an invoice with a zero amount or no invoice at all."), + "invalid_sats_amount": MessageLookupByLibrary.simpleMessage( + "The specified Sats amount is invalid."), + "invoice_updated": MessageLookupByLibrary.simpleMessage( + "Invoice has been successfully updated!"), + "is_not_your_dispute": MessageLookupByLibrary.simpleMessage( + "This dispute was not assigned to you!"), + "is_not_your_order": m21, + "new_order": m22, + "not_allowed_by_status": m23, + "not_found": MessageLookupByLibrary.simpleMessage("Dispute not found."), + "out_of_range_fiat_amount": m24, + "out_of_range_sats_amount": m25, + "pay_invoice": m26, + "payment_failed": m27, + "purchase_completed": MessageLookupByLibrary.simpleMessage( + "Your satoshis purchase has been completed successfully. I have paid your invoice, enjoy sound money!"), + "rate": MessageLookupByLibrary.simpleMessage( + "Please rate your counterparty"), + "rate_received": + MessageLookupByLibrary.simpleMessage("Rating successfully saved!"), + "released": m28, + "waiting_buyer_invoice": m29, + "waiting_seller_to_pay": m30 + }; +} diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart new file mode 100644 index 00000000..10d8c509 --- /dev/null +++ b/lib/generated/l10n.dart @@ -0,0 +1,494 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'intl/messages_all.dart'; + +// ************************************************************************** +// Generator: Flutter Intl IDE plugin +// Made by Localizely +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars +// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each +// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes + +class S { + S(); + + static S? _current; + + static S get current { + assert(_current != null, + 'No instance of S was loaded. Try to initialize the S delegate before accessing S.current.'); + return _current!; + } + + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); + + static Future load(Locale locale) { + final name = (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); + final localeName = Intl.canonicalizedLocale(name); + return initializeMessages(localeName).then((_) { + Intl.defaultLocale = localeName; + final instance = S(); + S._current = instance; + + return instance; + }); + } + + static S of(BuildContext context) { + final instance = S.maybeOf(context); + assert(instance != null, + 'No instance of S present in the widget tree. Did you add S.delegate in localizationsDelegates?'); + return instance!; + } + + static S? maybeOf(BuildContext context) { + return Localizations.of(context, S); + } + + /// `Your offer has been published! Please wait until another user picks your order. It will be available for {expiration_hours} hours. You can cancel this order before another user picks it up by executing: cancel.` + String new_order(Object expiration_hours) { + return Intl.message( + 'Your offer has been published! Please wait until another user picks your order. It will be available for $expiration_hours hours. You can cancel this order before another user picks it up by executing: cancel.', + name: 'new_order', + desc: '', + args: [expiration_hours], + ); + } + + /// `You have cancelled the order ID: {id}!` + String canceled(Object id) { + return Intl.message( + 'You have cancelled the order ID: $id!', + name: 'canceled', + desc: '', + args: [id], + ); + } + + /// `Please pay this hold invoice of {amount} Sats for {fiat_code} {fiat_amount} to start the operation. If you do not pay it within {expiration_seconds} the trade will be cancelled.` + String pay_invoice(Object amount, Object fiat_code, Object fiat_amount, + Object expiration_seconds) { + return Intl.message( + 'Please pay this hold invoice of $amount Sats for $fiat_code $fiat_amount to start the operation. If you do not pay it within $expiration_seconds the trade will be cancelled.', + name: 'pay_invoice', + desc: '', + args: [amount, fiat_code, fiat_amount, expiration_seconds], + ); + } + + /// `Please send me an invoice for {amount} satoshis equivalent to {fiat_code} {fiat_amount}. This is where I'll send the funds upon completion of the trade. If you don't provide the invoice within {expiration_seconds} this trade will be cancelled.` + String add_invoice(Object amount, Object fiat_code, Object fiat_amount, + Object expiration_seconds) { + return Intl.message( + 'Please send me an invoice for $amount satoshis equivalent to $fiat_code $fiat_amount. This is where I\'ll send the funds upon completion of the trade. If you don\'t provide the invoice within $expiration_seconds this trade will be cancelled.', + name: 'add_invoice', + desc: '', + args: [amount, fiat_code, fiat_amount, expiration_seconds], + ); + } + + /// `Please wait a bit. I've sent a payment request to the seller to send the Sats for the order ID {id}. Once the payment is made, I'll connect you both. If the seller doesn't complete the payment within {expiration_seconds} minutes the trade will be cancelled.` + String waiting_seller_to_pay(Object id, Object expiration_seconds) { + return Intl.message( + 'Please wait a bit. I\'ve sent a payment request to the seller to send the Sats for the order ID $id. Once the payment is made, I\'ll connect you both. If the seller doesn\'t complete the payment within $expiration_seconds minutes the trade will be cancelled.', + name: 'waiting_seller_to_pay', + desc: '', + args: [id, expiration_seconds], + ); + } + + /// `Payment received! Your Sats are now 'held' in your own wallet. Please wait a bit. I've requested the buyer to provide an invoice. Once they do, I'll connect you both. If they do not do so within {expiration_seconds} your Sats will be available in your wallet again and the trade will be cancelled.` + String waiting_buyer_invoice(Object expiration_seconds) { + return Intl.message( + 'Payment received! Your Sats are now \'held\' in your own wallet. Please wait a bit. I\'ve requested the buyer to provide an invoice. Once they do, I\'ll connect you both. If they do not do so within $expiration_seconds your Sats will be available in your wallet again and the trade will be cancelled.', + name: 'waiting_buyer_invoice', + desc: '', + args: [expiration_seconds], + ); + } + + /// `Invoice has been successfully saved!` + String get buyer_invoice_accepted { + return Intl.message( + 'Invoice has been successfully saved!', + name: 'buyer_invoice_accepted', + desc: '', + args: [], + ); + } + + /// `Get in touch with the seller, this is their npub {seller_npub} to get the details on how to send the fiat money for the order {id}, you must send {fiat_code} {fiat_amount} using {payment_method}. Once you send the fiat money, please let me know with fiat-sent.` + String hold_invoice_payment_accepted(Object seller_npub, Object id, + Object fiat_code, Object fiat_amount, Object payment_method) { + return Intl.message( + 'Get in touch with the seller, this is their npub $seller_npub to get the details on how to send the fiat money for the order $id, you must send $fiat_code $fiat_amount using $payment_method. Once you send the fiat money, please let me know with fiat-sent.', + name: 'hold_invoice_payment_accepted', + desc: '', + args: [seller_npub, id, fiat_code, fiat_amount, payment_method], + ); + } + + /// `Get in touch with the buyer, this is their npub {buyer_npub} to inform them how to send you {fiat_code} {fiat_amount} through {payment_method}. I will notify you once the buyer indicates the fiat money has been sent. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first.` + String buyer_took_order(Object buyer_npub, Object fiat_code, + Object fiat_amount, Object payment_method) { + return Intl.message( + 'Get in touch with the buyer, this is their npub $buyer_npub to inform them how to send you $fiat_code $fiat_amount through $payment_method. I will notify you once the buyer indicates the fiat money has been sent. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first.', + name: 'buyer_took_order', + desc: '', + args: [buyer_npub, fiat_code, fiat_amount, payment_method], + ); + } + + /// `I have informed {seller_npub} that you have sent the fiat money. When the seller confirms they have received your fiat money, they should release the funds. If they refuse, you can open a dispute.` + String fiat_sent_ok_buyer(Object seller_npub) { + return Intl.message( + 'I have informed $seller_npub that you have sent the fiat money. When the seller confirms they have received your fiat money, they should release the funds. If they refuse, you can open a dispute.', + name: 'fiat_sent_ok_buyer', + desc: '', + args: [seller_npub], + ); + } + + /// `{buyer_npub} has informed that they have sent you the fiat money. Once you confirm receipt, please release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, send me release-order-message.` + String fiat_sent_ok_seller(Object buyer_npub) { + return Intl.message( + '$buyer_npub has informed that they have sent you the fiat money. Once you confirm receipt, please release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, send me release-order-message.', + name: 'fiat_sent_ok_seller', + desc: '', + args: [buyer_npub], + ); + } + + /// `{seller_npub} has already released the Sats! Expect your invoice to be paid any time. Remember your wallet needs to be online to receive through the Lightning Network.` + String released(Object seller_npub) { + return Intl.message( + '$seller_npub has already released the Sats! Expect your invoice to be paid any time. Remember your wallet needs to be online to receive through the Lightning Network.', + name: 'released', + desc: '', + args: [seller_npub], + ); + } + + /// `Your satoshis purchase has been completed successfully. I have paid your invoice, enjoy sound money!` + String get purchase_completed { + return Intl.message( + 'Your satoshis purchase has been completed successfully. I have paid your invoice, enjoy sound money!', + name: 'purchase_completed', + desc: '', + args: [], + ); + } + + /// `Your Sats sale has been completed after confirming the payment from {buyer_npub}.` + String hold_invoice_payment_settled(Object buyer_npub) { + return Intl.message( + 'Your Sats sale has been completed after confirming the payment from $buyer_npub.', + name: 'hold_invoice_payment_settled', + desc: '', + args: [buyer_npub], + ); + } + + /// `Please rate your counterparty` + String get rate { + return Intl.message( + 'Please rate your counterparty', + name: 'rate', + desc: '', + args: [], + ); + } + + /// `Rating successfully saved!` + String get rate_received { + return Intl.message( + 'Rating successfully saved!', + name: 'rate_received', + desc: '', + args: [], + ); + } + + /// `You have initiated the cancellation of the order ID: {id}. Your counterparty must agree to the cancellation too. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.` + String cooperative_cancel_initiated_by_you(Object id) { + return Intl.message( + 'You have initiated the cancellation of the order ID: $id. Your counterparty must agree to the cancellation too. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.', + name: 'cooperative_cancel_initiated_by_you', + desc: '', + args: [id], + ); + } + + /// `Your counterparty wants to cancel order ID: {id}. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message.` + String cooperative_cancel_initiated_by_peer(Object id) { + return Intl.message( + 'Your counterparty wants to cancel order ID: $id. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message.', + name: 'cooperative_cancel_initiated_by_peer', + desc: '', + args: [id], + ); + } + + /// `Order {id} has been successfully cancelled!` + String cooperative_cancel_accepted(Object id) { + return Intl.message( + 'Order $id has been successfully cancelled!', + name: 'cooperative_cancel_accepted', + desc: '', + args: [id], + ); + } + + /// `You have initiated a dispute for order Id: {id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: {user_token}.` + String dispute_initiated_by_you(Object id, Object user_token) { + return Intl.message( + 'You have initiated a dispute for order Id: $id. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: $user_token.', + name: 'dispute_initiated_by_you', + desc: '', + args: [id, user_token], + ); + } + + /// `Your counterparty has initiated a dispute for order Id: {id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: {user_token}.` + String dispute_initiated_by_peer(Object id, Object user_token) { + return Intl.message( + 'Your counterparty has initiated a dispute for order Id: $id. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: $user_token.', + name: 'dispute_initiated_by_peer', + desc: '', + args: [id, user_token], + ); + } + + /// `Here are the details of the dispute order you have taken: {details}. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed.` + String admin_took_dispute_admin(Object details) { + return Intl.message( + 'Here are the details of the dispute order you have taken: $details. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed.', + name: 'admin_took_dispute_admin', + desc: '', + args: [details], + ); + } + + /// `The solver {admin_npub} will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token.` + String admin_took_dispute_users(Object admin_npub) { + return Intl.message( + 'The solver $admin_npub will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token.', + name: 'admin_took_dispute_users', + desc: '', + args: [admin_npub], + ); + } + + /// `You have cancelled the order ID: {id}!` + String admin_canceled_admin(Object id) { + return Intl.message( + 'You have cancelled the order ID: $id!', + name: 'admin_canceled_admin', + desc: '', + args: [id], + ); + } + + /// `Admin has cancelled the order ID: {id}!` + String admin_canceled_users(Object id) { + return Intl.message( + 'Admin has cancelled the order ID: $id!', + name: 'admin_canceled_users', + desc: '', + args: [id], + ); + } + + /// `You have completed the order ID: {id}!` + String admin_settled_admin(Object id) { + return Intl.message( + 'You have completed the order ID: $id!', + name: 'admin_settled_admin', + desc: '', + args: [id], + ); + } + + /// `Admin has completed the order ID: {id}!` + String admin_settled_users(Object id) { + return Intl.message( + 'Admin has completed the order ID: $id!', + name: 'admin_settled_users', + desc: '', + args: [id], + ); + } + + /// `This dispute was not assigned to you!` + String get is_not_your_dispute { + return Intl.message( + 'This dispute was not assigned to you!', + name: 'is_not_your_dispute', + desc: '', + args: [], + ); + } + + /// `Dispute not found.` + String get not_found { + return Intl.message( + 'Dispute not found.', + name: 'not_found', + desc: '', + args: [], + ); + } + + /// `I tried to send you the Sats but the payment of your invoice failed. I will try {payment_attempts} more times in {payment_retries_interval} minutes window. Please ensure your node/wallet is online.` + String payment_failed( + Object payment_attempts, Object payment_retries_interval) { + return Intl.message( + 'I tried to send you the Sats but the payment of your invoice failed. I will try $payment_attempts more times in $payment_retries_interval minutes window. Please ensure your node/wallet is online.', + name: 'payment_failed', + desc: '', + args: [payment_attempts, payment_retries_interval], + ); + } + + /// `Invoice has been successfully updated!` + String get invoice_updated { + return Intl.message( + 'Invoice has been successfully updated!', + name: 'invoice_updated', + desc: '', + args: [], + ); + } + + /// `The invoice was cancelled; your Sats will be available in your wallet again.` + String get hold_invoice_payment_canceled { + return Intl.message( + 'The invoice was cancelled; your Sats will be available in your wallet again.', + name: 'hold_invoice_payment_canceled', + desc: '', + args: [], + ); + } + + /// `You are not allowed to {action} for this order!` + String cant_do(Object action) { + return Intl.message( + 'You are not allowed to $action for this order!', + name: 'cant_do', + desc: '', + args: [action], + ); + } + + /// `You have successfully added the solver {npub}.` + String admin_add_solver(Object npub) { + return Intl.message( + 'You have successfully added the solver $npub.', + name: 'admin_add_solver', + desc: '', + args: [npub], + ); + } + + /// `You did not create this order and are not authorized to {action} it.` + String is_not_your_order(Object action) { + return Intl.message( + 'You did not create this order and are not authorized to $action it.', + name: 'is_not_your_order', + desc: '', + args: [action], + ); + } + + /// `You are not allowed to {action} because order Id {id} status is {order_status}.` + String not_allowed_by_status(Object action, Object id, Object order_status) { + return Intl.message( + 'You are not allowed to $action because order Id $id status is $order_status.', + name: 'not_allowed_by_status', + desc: '', + args: [action, id, order_status], + ); + } + + /// `The requested amount is incorrect and may be outside the acceptable range. The minimum is {min_amount} and the maximum is {max_amount}.` + String out_of_range_fiat_amount(Object min_amount, Object max_amount) { + return Intl.message( + 'The requested amount is incorrect and may be outside the acceptable range. The minimum is $min_amount and the maximum is $max_amount.', + name: 'out_of_range_fiat_amount', + desc: '', + args: [min_amount, max_amount], + ); + } + + /// `An invoice with non-zero amount was received for the new order. Please send an invoice with a zero amount or no invoice at all.` + String get incorrect_invoice_amount_buyer_new_order { + return Intl.message( + 'An invoice with non-zero amount was received for the new order. Please send an invoice with a zero amount or no invoice at all.', + name: 'incorrect_invoice_amount_buyer_new_order', + desc: '', + args: [], + ); + } + + /// `The amount stated in the invoice is incorrect. Please send an invoice with an amount of {amount} satoshis, an invoice without an amount, or a lightning address.` + String incorrect_invoice_amount_buyer_add_invoice(Object amount) { + return Intl.message( + 'The amount stated in the invoice is incorrect. Please send an invoice with an amount of $amount satoshis, an invoice without an amount, or a lightning address.', + name: 'incorrect_invoice_amount_buyer_add_invoice', + desc: '', + args: [amount], + ); + } + + /// `The specified Sats amount is invalid.` + String get invalid_sats_amount { + return Intl.message( + 'The specified Sats amount is invalid.', + name: 'invalid_sats_amount', + desc: '', + args: [], + ); + } + + /// `The allowed Sats amount for this Mostro is between min {min_order_amount} and max {max_order_amount}. Please enter an amount within this range.` + String out_of_range_sats_amount( + Object min_order_amount, Object max_order_amount) { + return Intl.message( + 'The allowed Sats amount for this Mostro is between min $min_order_amount and max $max_order_amount. Please enter an amount within this range.', + name: 'out_of_range_sats_amount', + desc: '', + args: [min_order_amount, max_order_amount], + ); + } +} + +class AppLocalizationDelegate extends LocalizationsDelegate { + const AppLocalizationDelegate(); + + List get supportedLocales { + return const [ + Locale.fromSubtags(languageCode: 'en'), + ]; + } + + @override + bool isSupported(Locale locale) => _isSupported(locale); + @override + Future load(Locale locale) => S.load(locale); + @override + bool shouldReload(AppLocalizationDelegate old) => false; + + bool _isSupported(Locale locale) { + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode) { + return true; + } + } + return false; + } +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 00000000..d4a9a072 --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1,44 @@ +{ + "@@locale": "en", + "new_order": "Your offer has been published! Please wait until another user picks your order. It will be available for {expiration_hours} hours. You can cancel this order before another user picks it up by executing: cancel.", + "canceled": "You have cancelled the order ID: {id}!", + "pay_invoice": "Please pay this hold invoice of {amount} Sats for {fiat_code} {fiat_amount} to start the operation. If you do not pay it within {expiration_seconds} the trade will be cancelled.", + "add_invoice": "Please send me an invoice for {amount} satoshis equivalent to {fiat_code} {fiat_amount}. This is where I'll send the funds upon completion of the trade. If you don't provide the invoice within {expiration_seconds} this trade will be cancelled.", + "waiting_seller_to_pay": "Please wait a bit. I've sent a payment request to the seller to send the Sats for the order ID {id}. Once the payment is made, I'll connect you both. If the seller doesn't complete the payment within {expiration_seconds} minutes the trade will be cancelled.", + "waiting_buyer_invoice": "Payment received! Your Sats are now 'held' in your own wallet. Please wait a bit. I've requested the buyer to provide an invoice. Once they do, I'll connect you both. If they do not do so within {expiration_seconds} your Sats will be available in your wallet again and the trade will be cancelled.", + "buyer_invoice_accepted": "Invoice has been successfully saved!", + "hold_invoice_payment_accepted": "Get in touch with the seller, this is their npub {seller_npub} to get the details on how to send the fiat money for the order {id}, you must send {fiat_code} {fiat_amount} using {payment_method}. Once you send the fiat money, please let me know with fiat-sent.", + "buyer_took_order": "Get in touch with the buyer, this is their npub {buyer_npub} to inform them how to send you {fiat_code} {fiat_amount} through {payment_method}. I will notify you once the buyer indicates the fiat money has been sent. Afterward, you should verify if it has arrived. If the buyer does not respond, you can initiate a cancellation or a dispute. Remember, an administrator will NEVER contact you to resolve your order unless you open a dispute first.", + "fiat_sent_ok_buyer": "I have informed {seller_npub} that you have sent the fiat money. When the seller confirms they have received your fiat money, they should release the funds. If they refuse, you can open a dispute.", + "fiat_sent_ok_seller": "{buyer_npub} has informed that they have sent you the fiat money. Once you confirm receipt, please release the funds. After releasing, the money will go to the buyer and there will be no turning back, so only proceed if you are sure. If you want to release the Sats to the buyer, send me release-order-message.", + "released": "{seller_npub} has already released the Sats! Expect your invoice to be paid any time. Remember your wallet needs to be online to receive through the Lightning Network.", + "purchase_completed": "Your satoshis purchase has been completed successfully. I have paid your invoice, enjoy sound money!", + "hold_invoice_payment_settled": "Your Sats sale has been completed after confirming the payment from {buyer_npub}.", + "rate": "Please rate your counterparty", + "rate_received": "Rating successfully saved!", + "cooperative_cancel_initiated_by_you": "You have initiated the cancellation of the order ID: {id}. Your counterparty must agree to the cancellation too. If they do not respond, you can open a dispute. Note that no administrator will contact you regarding this cancellation unless you open a dispute first.", + "cooperative_cancel_initiated_by_peer": "Your counterparty wants to cancel order ID: {id}. Note that no administrator will contact you regarding this cancellation unless you open a dispute first. If you agree on such cancellation, please send me cancel-order-message.", + "cooperative_cancel_accepted": "Order {id} has been successfully cancelled!", + "dispute_initiated_by_you": "You have initiated a dispute for order Id: {id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: {user_token}.", + "dispute_initiated_by_peer": "Your counterparty has initiated a dispute for order Id: {id}. A solver will be assigned to your dispute soon. Once assigned, I will share their npub with you, and only they will be able to assist you. You may contact the solver directly, but if they reach out first, please ask them to provide the token for your dispute. Your dispute token is: {user_token}.", + "admin_took_dispute_admin": "Here are the details of the dispute order you have taken: {details}. You need to determine which user is correct and decide whether to cancel or complete the order. Please note that your decision will be final and cannot be reversed.", + "admin_took_dispute_users": "The solver {admin_npub} will handle your dispute. You can contact them directly, but if they reach out to you first, make sure to ask them for your dispute token.", + "admin_canceled_admin": "You have cancelled the order ID: {id}!", + "admin_canceled_users": "Admin has cancelled the order ID: {id}!", + "admin_settled_admin": "You have completed the order ID: {id}!", + "admin_settled_users": "Admin has completed the order ID: {id}!", + "is_not_your_dispute": "This dispute was not assigned to you!", + "not_found": "Dispute not found.", + "payment_failed": "I tried to send you the Sats but the payment of your invoice failed. I will try {payment_attempts} more times in {payment_retries_interval} minutes window. Please ensure your node/wallet is online.", + "invoice_updated": "Invoice has been successfully updated!", + "hold_invoice_payment_canceled": "The invoice was cancelled; your Sats will be available in your wallet again.", + "cant_do": "You are not allowed to {action} for this order!", + "admin_add_solver": "You have successfully added the solver {npub}.", + "is_not_your_order": "You did not create this order and are not authorized to {action} it.", + "not_allowed_by_status": "You are not allowed to {action} because order Id {id} status is {order_status}.", + "out_of_range_fiat_amount": "The requested amount is incorrect and may be outside the acceptable range. The minimum is {min_amount} and the maximum is {max_amount}.", + "incorrect_invoice_amount_buyer_new_order": "An invoice with non-zero amount was received for the new order. Please send an invoice with a zero amount or no invoice at all.", + "incorrect_invoice_amount_buyer_add_invoice": "The amount stated in the invoice is incorrect. Please send an invoice with an amount of {amount} satoshis, an invoice without an amount, or a lightning address.", + "invalid_sats_amount": "The specified Sats amount is invalid.", + "out_of_range_sats_amount": "The allowed Sats amount for this Mostro is between min {min_order_amount} and max {max_order_amount}. Please enter an amount within this range." +} diff --git a/lib/main.dart b/lib/main.dart index cd52c269..7f513f82 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/core/theme/app_theme.dart'; import 'package:mostro_mobile/presentation/auth/bloc/auth_state.dart'; @@ -13,6 +14,7 @@ import 'package:mostro_mobile/presentation/profile/bloc/profile_bloc.dart'; import 'package:mostro_mobile/presentation/auth/bloc/auth_bloc.dart'; import 'package:mostro_mobile/data/repositories/auth_repository.dart'; import 'package:mostro_mobile/core/utils/biometrics_helper.dart'; +import 'generated/l10n.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -80,6 +82,13 @@ class MyApp extends ConsumerWidget { initialRoute: isFirstLaunch ? AppRoutes.welcome : AppRoutes.home, routes: AppRoutes.routes, onGenerateRoute: AppRoutes.onGenerateRoute, + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, ), ), ); diff --git a/lib/presentation/add_order/bloc/add_order_bloc.dart b/lib/presentation/add_order/bloc/add_order_bloc.dart index 8dc506d9..9a511fae 100644 --- a/lib/presentation/add_order/bloc/add_order_bloc.dart +++ b/lib/presentation/add_order/bloc/add_order_bloc.dart @@ -21,20 +21,25 @@ class AddOrderBloc extends Bloc { SubmitOrder event, Emitter emit) async { emit(state.copyWith(status: AddOrderStatus.submitting)); - final order = await mostroService.publishOrder(event.order); - add(OrderUpdateReceived(order)); + try { + final order = await mostroService.publishOrder(event.order); + add(OrderUpdateReceived(order)); + } catch (e) { + emit(state.copyWith( + status: AddOrderStatus.failure, + errorMessage: e.toString(), + )); + } } void _onOrderUpdateReceived( OrderUpdateReceived event, Emitter emit) { switch (event.order.action) { case Action.newOrder: - print(event.order.content!.toJson()); emit(state.copyWith(status: AddOrderStatus.submitted)); break; case Action.outOfRangeSatsAmount: case Action.outOfRangeFiatAmount: - print("Error! ${event.order.action.value}"); emit(state.copyWith( status: AddOrderStatus.failure, errorMessage: "Invalid amount")); break; diff --git a/lib/presentation/add_order/screens/add_order_screen.dart b/lib/presentation/add_order/screens/add_order_screen.dart index 0b826801..c570cb96 100644 --- a/lib/presentation/add_order/screens/add_order_screen.dart +++ b/lib/presentation/add_order/screens/add_order_screen.dart @@ -6,6 +6,7 @@ import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/theme/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_bloc.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_event.dart'; import 'package:mostro_mobile/presentation/add_order/bloc/add_order_state.dart'; @@ -80,8 +81,7 @@ class AddOrderScreen extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( - 'Your offer has been published! Please wait until another user picks your order. It will be available for expiration_hours hours. You can cancel this order before another user picks it up by executing: cancel.', + Text(S.of(context).new_order('24'), style: TextStyle(fontSize: 18, color: AppTheme.cream1), textAlign: TextAlign.center, ), diff --git a/lib/presentation/chat_list/screens/chat_list_screen.dart b/lib/presentation/chat_list/screens/chat_list_screen.dart index 0d11ecdb..2171061c 100644 --- a/lib/presentation/chat_list/screens/chat_list_screen.dart +++ b/lib/presentation/chat_list/screens/chat_list_screen.dart @@ -27,7 +27,7 @@ class ChatListScreen extends StatelessWidget { child: Column( children: [ Padding( - padding: const EdgeInsets.all(16.0), + padding: EdgeInsets.all(16.0), child: Text( 'Chats', style: TextStyle( diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart index dd99797f..7d383576 100644 --- a/lib/presentation/home/screens/home_screen.dart +++ b/lib/presentation/home/screens/home_screen.dart @@ -164,7 +164,7 @@ class HomeScreen extends ConsumerWidget { } return OrderList( orders: events - .where((evt) => evt.orderType == state.orderType.value) + .where((evt) => evt.orderType == state.orderType) .toList()); }, ); diff --git a/lib/presentation/order/bloc/order_details_bloc.dart b/lib/presentation/order/bloc/order_details_bloc.dart index c71a4075..827f775e 100644 --- a/lib/presentation/order/bloc/order_details_bloc.dart +++ b/lib/presentation/order/bloc/order_details_bloc.dart @@ -1,5 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_state.dart'; @@ -12,6 +14,7 @@ class OrderDetailsBloc extends Bloc { on(_onLoadOrderDetails); on(_onCancelOrder); on(_onContinueOrder); + on(_onOrderUpdateReceived); } void _onLoadOrderDetails( @@ -28,12 +31,31 @@ class OrderDetailsBloc extends Bloc { ContinueOrder event, Emitter emit) async { emit(state.copyWith(status: OrderDetailsStatus.loading)); + late MostroMessage order; + if (event.order.orderType == OrderType.buy.value) { - await mostroService.takeBuyOrder(event.order.orderId!); + order = await mostroService.takeBuyOrder(event.order.orderId!); } else { - await mostroService.takeSellOrder(event.order.orderId!); + order = await mostroService.takeSellOrder(event.order.orderId!); } - emit(state.copyWith(status: OrderDetailsStatus.done)); + add(OrderUpdateReceived(order)); + } + + void _onOrderUpdateReceived( + OrderUpdateReceived event, Emitter emit) { + switch (event.order.action) { + case Action.addInvoice: + case Action.payInvoice: + case Action.waitingSellerToPay: + emit(state.copyWith(status: OrderDetailsStatus.done)); + break; + case Action.notAllowedByStatus: + emit(state.copyWith( + status: OrderDetailsStatus.error, errorMessage: "Not allowed by status")); + break; + default: + break; + } } } diff --git a/lib/presentation/order/bloc/order_details_event.dart b/lib/presentation/order/bloc/order_details_event.dart index 81b343d3..f4bd8add 100644 --- a/lib/presentation/order/bloc/order_details_event.dart +++ b/lib/presentation/order/bloc/order_details_event.dart @@ -1,5 +1,6 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:equatable/equatable.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; abstract class OrderDetailsEvent extends Equatable { const OrderDetailsEvent(); @@ -27,3 +28,12 @@ class ContinueOrder extends OrderDetailsEvent { @override List get props => [order]; } + +class OrderUpdateReceived extends OrderDetailsEvent { + final MostroMessage order; + + const OrderUpdateReceived(this.order); + + @override + List get props => [order]; +} diff --git a/lib/presentation/order/screens/order_details_screen.dart b/lib/presentation/order/screens/order_details_screen.dart index fb272e25..14d172f7 100644 --- a/lib/presentation/order/screens/order_details_screen.dart +++ b/lib/presentation/order/screens/order_details_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_bloc.dart'; import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; @@ -16,7 +17,7 @@ import 'package:mostro_mobile/providers/riverpod_providers.dart'; class OrderDetailsScreen extends ConsumerWidget { final NostrEvent initialOrder; - final _satsAmountController = TextEditingController(); + final TextEditingController _satsAmountController = TextEditingController(); OrderDetailsScreen({super.key, required this.initialOrder}); @@ -26,47 +27,74 @@ class OrderDetailsScreen extends ConsumerWidget { return BlocProvider( create: (context) => OrderDetailsBloc(mostroService)..add(LoadOrderDetails(initialOrder)), - child: BlocBuilder( + child: BlocConsumer( + listener: (context, state) { + if (state.status == OrderDetailsStatus.done) { + Navigator.of(context).pop(); + } + }, builder: (context, state) { - if (state.status == OrderDetailsStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } else if (state.status == OrderDetailsStatus.error) { - return Center( - child: Text(state.errorMessage ?? 'An error occurred')); - } else if (state.status == OrderDetailsStatus.loaded) { - return _buildContent(context, ref, state.order!); - } else if (state.status == OrderDetailsStatus.cancelled || - state.status == OrderDetailsStatus.done) { - return _buildCompletionMessage(context, state); + switch (state.status) { + case OrderDetailsStatus.loading: + return const Center(child: CircularProgressIndicator()); + case OrderDetailsStatus.error: + return _buildErrorScreen(state.errorMessage, context); + case OrderDetailsStatus.cancelled: + case OrderDetailsStatus.done: + return _buildCompletionMessage(context, state); + case OrderDetailsStatus.loaded: + return _buildContent(context, ref, state.order!); + default: + return const Center(child: Text('Order not found')); } - return const Center(child: Text('Order not found')); }, ), ); } + Widget _buildErrorScreen(String? errorMessage, BuildContext context) { + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: _buildAppBar('Error', context), + body: Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + errorMessage ?? 'An unexpected error occurred.', + style: const TextStyle( + color: Colors.red, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.cream1, + ), + child: const Text('Return to Main Screen'), + ), + ], + ), + ), + ), + ); + } + Widget _buildCompletionMessage( BuildContext context, OrderDetailsState state) { final message = state.status == OrderDetailsStatus.cancelled ? 'Order has been cancelled.' - : 'Order has been completed!'; + : state.errorMessage ?? + 'Order has been completed successfully!'; // Handles custom errors or success messages return Scaffold( - backgroundColor: const Color(0xFF1D212C), - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - message, - style: const TextStyle( - color: Colors.white, - fontSize: 20, - ), - ), - ), + backgroundColor: AppTheme.dark1, + appBar: _buildAppBar('Completion', context), body: Center( child: Padding( padding: const EdgeInsets.all(16.0), @@ -76,7 +104,7 @@ class OrderDetailsScreen extends ConsumerWidget { Text( message, style: const TextStyle( - color: Colors.white, + color: AppTheme.cream1, fontSize: 18, fontWeight: FontWeight.bold, ), @@ -85,16 +113,14 @@ class OrderDetailsScreen extends ConsumerWidget { const SizedBox(height: 16), const Text( 'Thank you for using our service!', - style: TextStyle(color: Colors.grey), + style: TextStyle(color: AppTheme.grey2), textAlign: TextAlign.center, ), const SizedBox(height: 32), ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, + onPressed: () => Navigator.of(context).pop(), style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF8CC541), + backgroundColor: AppTheme.mostroGreen, ), child: const Text('Return to Main Screen'), ), @@ -107,67 +133,55 @@ class OrderDetailsScreen extends ConsumerWidget { Widget _buildContent(BuildContext context, WidgetRef ref, NostrEvent order) { return Scaffold( - backgroundColor: const Color(0xFF1D212C), - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), + backgroundColor: AppTheme.dark1, + appBar: _buildAppBar('${order.orderType?.value.toUpperCase()} BITCOIN', context), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildSellerInfo(order), + const SizedBox(height: 16), + _buildSellerAmount(order, ref), + const SizedBox(height: 16), + ExchangeRateWidget(currency: order.currency!), + const SizedBox(height: 16), + _buildBuyerInfo(order), + const SizedBox(height: 16), + _buildBuyerAmount(order), + const SizedBox(height: 24), + _buildActionButtons(context), + ], + ), ), - title: Text('${order.orderType?.toUpperCase()} BITCOIN', - style: TextStyle( - color: Colors.white, - fontFamily: GoogleFonts.robotoCondensed().fontFamily)), ), - body: BlocConsumer( - listener: (context, state) { - if (state.status == OrderDetailsStatus.cancelled || - state.status == OrderDetailsStatus.done) { - Navigator.of(context).pop(); - } - }, builder: (context, state) { - return Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - _buildSellerInfo(order), - const SizedBox(height: 16), - _buildSellerAmount(order, ref), - const SizedBox(height: 16), - ExchangeRateWidget(currency: order.currency!), - const SizedBox(height: 16), - _buildBuyerInfo(order), - const SizedBox(height: 16), - _buildBuyerAmount(order), - const SizedBox(height: 24), - _buildActionButtons(context), - ], - ), - ), - ), - ), - ], - ); - }), ); } - Widget _buildSellerInfo(NostrEvent order) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(12), + PreferredSizeWidget? _buildAppBar(String title, BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + title, + style: TextStyle( + color: Colors.white, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, + ), ), - child: Row( + ); + } + + Widget _buildSellerInfo(NostrEvent order) { + return _infoContainer( + Row( children: [ - CircleAvatar( - backgroundColor: Colors.grey, + const CircleAvatar( + backgroundColor: AppTheme.grey2, child: Text('S', style: TextStyle(color: Colors.white)), ), const SizedBox(width: 12), @@ -178,17 +192,19 @@ class OrderDetailsScreen extends ConsumerWidget { Text(order.name!, style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold)), - Text('${order.rating}/5 (_)', - style: const TextStyle(color: Color(0xFF8CC541))), + Text( + '${order.rating?.totalRating}/${order.rating?.maxRate} (${order.rating?.totalReviews})', + style: const TextStyle(color: AppTheme.mostroGreen), + ), ], ), ), TextButton( onPressed: () { - // Implementar lógica para leer reseñas + // Implement review logic }, child: const Text('Read reviews', - style: TextStyle(color: Color(0xFF8CC541))), + style: TextStyle(color: AppTheme.mostroGreen)), ), ], ), @@ -202,40 +218,17 @@ class OrderDetailsScreen extends ConsumerWidget { loading: () => const CircularProgressIndicator(), error: (error, _) => Text('Error: $error'), data: (exchangeRate) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(12), - ), - child: Column( + return _infoContainer( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('${order.fiatAmount} ${order.currency} (${order.premium}%)', style: const TextStyle( - color: Colors.white, + color: AppTheme.cream1, fontSize: 18, fontWeight: FontWeight.bold)), Text('${order.amount} sats', - style: const TextStyle(color: Colors.grey)), - const SizedBox(height: 8), - Row( - children: [ - const HeroIcon(HeroIcons.creditCard, - style: HeroIconStyle.outline, - color: Colors.white, - size: 16), - const SizedBox(width: 8), - Flexible( - child: Text( - order.paymentMethods[0], - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.visible, - softWrap: true, - ), - ), - ], - ), + style: const TextStyle(color: AppTheme.grey2)), ], ), ); @@ -244,16 +237,11 @@ class OrderDetailsScreen extends ConsumerWidget { } Widget _buildBuyerInfo(NostrEvent order) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(12), - ), - child: const Row( + return _infoContainer( + const Row( children: [ CircleAvatar( - backgroundColor: Colors.grey, + backgroundColor: AppTheme.grey2, child: Text('A', style: TextStyle(color: Colors.white)), ), SizedBox(width: 12), @@ -264,41 +252,34 @@ class OrderDetailsScreen extends ConsumerWidget { Text('Anon (you)', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold)), - Text('0/5 (0)', style: TextStyle(color: Colors.grey)), ], ), ), - HeroIcon(HeroIcons.bolt, - style: HeroIconStyle.solid, color: Color(0xFF8CC541)), ], ), ); } - Widget _buildBuyerAmount(NostrEvent order) { + Widget _infoContainer(Widget child) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFF303544), + color: AppTheme.dark2, borderRadius: BorderRadius.circular(12), ), - child: Column( + child: child, + ); + } + + Widget _buildBuyerAmount(NostrEvent order) { + return _infoContainer( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CurrencyTextField(controller: _satsAmountController, label: 'sats'), + CurrencyTextField(controller: _satsAmountController, label: 'Sats'), const SizedBox(height: 8), Text('\$ ${order.amount}', - style: const TextStyle(color: Colors.grey)), - const SizedBox(height: 8), - const Row( - children: [ - HeroIcon(HeroIcons.bolt, - style: HeroIconStyle.solid, color: Colors.white, size: 16), - SizedBox(width: 8), - Text('Bitcoin Lightning Network', - style: TextStyle(color: Colors.white)), - ], - ), + style: const TextStyle(color: AppTheme.grey2)), ], ), ); @@ -314,8 +295,6 @@ class OrderDetailsScreen extends ConsumerWidget { }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), ), child: const Text('CANCEL'), ), @@ -327,9 +306,7 @@ class OrderDetailsScreen extends ConsumerWidget { context.read().add(ContinueOrder(initialOrder)); }, style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF8CC541), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8)), + backgroundColor: AppTheme.mostroGreen, ), child: const Text('CONTINUE'), ), diff --git a/lib/presentation/widgets/currency_dropdown.dart b/lib/presentation/widgets/currency_dropdown.dart index 86dbd0f8..2e33b830 100644 --- a/lib/presentation/widgets/currency_dropdown.dart +++ b/lib/presentation/widgets/currency_dropdown.dart @@ -13,7 +13,7 @@ class CurrencyDropdown extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currencyCodesAsync = ref.watch(currencyCodesProvider); - final selectedFiatCode = ref.watch(selectedFiatCodeProvider); + final selectedFiatCode = ref.watch(selectedFiatCodeProvider) ?? ''; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -24,12 +24,19 @@ class CurrencyDropdown extends ConsumerWidget { child: currencyCodesAsync.when( loading: () => const Center( child: SizedBox( - height: 24, - width: 24, + height: double.infinity, child: CircularProgressIndicator(), ), ), - error: (error, stackTrace) => Text('Error: $error'), + error: (error, stackTrace) => Row( + children: [ + Text('Failed to load currencies'), + TextButton( + onPressed: () => ref.refresh(currencyCodesProvider), + child: const Text('Retry'), + ), + ], + ), data: (currencyCodes) { final items = currencyCodes.keys.map((code) { return DropdownMenuItem( @@ -45,12 +52,18 @@ class CurrencyDropdown extends ConsumerWidget { decoration: InputDecoration( border: InputBorder.none, labelText: label, - labelStyle: const TextStyle(color: Colors.grey), + labelStyle: Theme.of(context).inputDecorationTheme.labelStyle, ), - dropdownColor: const Color(0xFF1D212C), - style: const TextStyle(color: Colors.white), + dropdownColor: Theme.of(context).colorScheme.surface, + style: Theme.of(context).textTheme.bodyMedium, items: items, value: selectedFiatCode, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please select a currency'; + } + return null; + }, onChanged: (value) => ref.read(selectedFiatCodeProvider.notifier).state = value, ); diff --git a/lib/presentation/widgets/exchange_rate_widget.dart b/lib/presentation/widgets/exchange_rate_widget.dart index 8689ba3e..b6f12203 100644 --- a/lib/presentation/widgets/exchange_rate_widget.dart +++ b/lib/presentation/widgets/exchange_rate_widget.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:mostro_mobile/providers/exchange_service_provider.dart'; class ExchangeRateWidget extends ConsumerWidget { @@ -19,7 +20,6 @@ class ExchangeRateWidget extends ConsumerWidget { loading: () => const CircularProgressIndicator(), error: (error, _) => Text('Error: $error'), data: (exchangeRate) { - return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -30,7 +30,10 @@ class ExchangeRateWidget extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '1 BTC = ${exchangeRate.toStringAsFixed(2)} $currency', + '1 BTC = ${NumberFormat.currency( + symbol: '', + decimalDigits: 2, + ).format(exchangeRate)} $currency', style: const TextStyle(color: Colors.white), ), Row( @@ -41,6 +44,12 @@ class ExchangeRateWidget extends ConsumerWidget { GestureDetector( onTap: () { // Trigger refresh for this specific currency + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Refreshing exchange rate...'), + duration: Duration(seconds: 1), + ), + ); ref .read(exchangeRateProvider(currency).notifier) .fetchExchangeRate(currency); diff --git a/lib/presentation/widgets/order_list_item.dart b/lib/presentation/widgets/order_list_item.dart index 255fd0ec..b28ba616 100644 --- a/lib/presentation/widgets/order_list_item.dart +++ b/lib/presentation/widgets/order_list_item.dart @@ -36,7 +36,7 @@ class OrderListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${order.name} ${order.rating}/5 (_)', + '${order.name} ${order.rating?.totalRating}/${order.rating?.maxRate} (${order.rating?.totalReviews})', style: const TextStyle(color: Colors.white), ), Text( diff --git a/lib/providers/event_store_providers.dart b/lib/providers/event_store_providers.dart index ee877f33..90ef3ba3 100644 --- a/lib/providers/event_store_providers.dart +++ b/lib/providers/event_store_providers.dart @@ -13,14 +13,18 @@ final orderRepositoryProvider = Provider((ref) { return OpenOrdersRepository(nostrService); }); +/// Event kind 38383 represents order events in the Nostr protocol as per NIP-69 +const orderEventKind = 38383; +const orderFilterDurationHours = 24; + final orderEventsProvider = StreamProvider>((ref) { final orderRepository = ref.watch(orderRepositoryProvider); - DateTime filterTime = DateTime.now().subtract(Duration(hours: 24)); + DateTime filterTime = DateTime.now().subtract(Duration(hours: orderFilterDurationHours)); var filter = NostrFilter( - kinds: const [38383], + kinds: const [orderEventKind], since: filterTime, ); orderRepository.subscribe(filter); return orderRepository.eventsStream; -}); +}); \ No newline at end of file diff --git a/lib/providers/exchange_service_provider.dart b/lib/providers/exchange_service_provider.dart index 47b67643..bb025653 100644 --- a/lib/providers/exchange_service_provider.dart +++ b/lib/providers/exchange_service_provider.dart @@ -15,6 +15,7 @@ final exchangeRateProvider = StateNotifierProvider.family>((ref) async { final exchangeService = ref.read(exchangeServiceProvider); + return await exchangeService.getCurrencyCodes(); }); diff --git a/lib/providers/riverpod_providers.dart b/lib/providers/riverpod_providers.dart index 7f78b2b0..0896f50f 100644 --- a/lib/providers/riverpod_providers.dart +++ b/lib/providers/riverpod_providers.dart @@ -17,7 +17,8 @@ final sessionManagerProvider = Provider((ref) { }); final mostroServiceProvider = Provider((ref) { - final sessionStorage = ref.read(sessionManagerProvider); - final nostrService = ref.read(nostrServicerProvider); + final sessionStorage = ref.watch(sessionManagerProvider); + final nostrService = ref.watch(nostrServicerProvider); return MostroService(nostrService, sessionStorage); }); + diff --git a/lib/services/exchange_service.dart b/lib/services/exchange_service.dart index bbeeac5f..9a9875f9 100644 --- a/lib/services/exchange_service.dart +++ b/lib/services/exchange_service.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; @@ -21,8 +23,21 @@ class ExchangeRateNotifier extends StateNotifier> { abstract class ExchangeService { final String baseUrl; + final Duration timeout; + final Map defaultHeaders; - ExchangeService(this.baseUrl); + ExchangeService( + this.baseUrl, { + this.timeout = const Duration(seconds: 30), + this.defaultHeaders = const {'Accept': 'application/json'}, + }) { + if (baseUrl.isEmpty) { + throw ArgumentError('baseUrl cannot be empty'); + } + if (!baseUrl.startsWith('http')) { + throw ArgumentError('baseUrl must start with http:// or https://'); + } + } Future getExchangeRate( String fromCurrency, @@ -31,14 +46,27 @@ abstract class ExchangeService { Future> getRequest(String endpoint) async { final url = Uri.parse('$baseUrl$endpoint'); - final response = await http.get(url); + try { + final response = await http + .get(url, headers: defaultHeaders) + .timeout(timeout); - if (response.statusCode == 200) { - return json.decode(response.body) as Map; - } else { - throw Exception('Failed to load data: ${response.statusCode}'); + if (response.statusCode == 200) { + return json.decode(response.body) as Map; + } + + throw HttpException( + 'Failed to load data: ${response.statusCode}', + uri: url, + ); + } on TimeoutException { + throw HttpException('Request timed out', uri: url); + } on FormatException catch (e) { + throw HttpException('Invalid response format: ${e.message}', uri: url); + } catch (e) { + throw HttpException('Request failed: $e', uri: url); } } Future> getCurrencyCodes(); -} +} \ No newline at end of file diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 7224d8c2..28916515 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -28,8 +28,7 @@ class MostroService { return _nostrService.subscribeToEvents(filter); } - Future publishOrder( - Order order) async { + Future publishOrder(Order order) async { final session = await _secureStorageManager.newSession(); final content = jsonEncode({ @@ -49,12 +48,12 @@ class MostroService { return await subscribeToOrders(filter).asyncMap((event) async { return await _nostrService.decryptNIP59Event(event, session.privateKey); - }).map((event) { - return MostroMessage.deserialized(event.content!); - }).first; + }).map((event) { + return MostroMessage.deserialized(event.content!); + }).first; } - Future takeSellOrder(String orderId, {int? amount}) async { + Future takeSellOrder(String orderId, {int? amount}) async { final session = await _secureStorageManager.newSession(); session.eventId = orderId; @@ -73,17 +72,15 @@ class MostroService { final filter = NostrFilter(p: [session.publicKey]); - subscribeToOrders(filter).listen((event) async { - final response = - await _nostrService.decryptNIP59Event(event, session.privateKey); - - final orderResponse = MostroMessage.deserialized(response.content!); - - print(response); - }); + return await subscribeToOrders(filter).asyncMap((event) async { + return await _nostrService.decryptNIP59Event(event, session.privateKey); + }).map((event) { + return MostroMessage.deserialized(event.content!); + }).first; } - Future> takeBuyOrder(String orderId, {int? amount}) async { + Future> takeBuyOrder(String orderId, + {int? amount}) async { final session = await _secureStorageManager.newSession(); session.eventId = orderId; @@ -102,19 +99,22 @@ class MostroService { return await subscribeToOrders(filter).asyncMap((event) async { return await _nostrService.decryptNIP59Event(event, session.privateKey); - }).map((event) { - return MostroMessage.deserialized(event.content!); - }).first; + }).map((event) { + return MostroMessage.deserialized(event.content!); + }).first; } Future cancelOrder(String orderId) async { final order = _mostroRepository.getOrder(orderId); + if (order == null) { + throw Exception('Order not found for order ID: $orderId'); + } + final session = await _secureStorageManager.loadSession(order!.requestId!); if (session == null) { - // TODO: throw error - return; + throw Exception('Session not found for order ID: $orderId'); } final content = jsonEncode({ @@ -134,8 +134,7 @@ class MostroService { final session = await _secureStorageManager.loadSession(orderId); if (session == null) { - // TODO: throw error - return; + throw Exception('Session not found for order ID: $orderId'); } final content = jsonEncode({ @@ -155,8 +154,7 @@ class MostroService { final session = await _secureStorageManager.loadSession(orderId); if (session == null) { - // TODO: throw error - return; + throw Exception('Session not found for order ID: $orderId'); } final content = jsonEncode({ diff --git a/lib/services/yadio_exchange_service.dart b/lib/services/yadio_exchange_service.dart index 83697660..a0f96cb8 100644 --- a/lib/services/yadio_exchange_service.dart +++ b/lib/services/yadio_exchange_service.dart @@ -8,22 +8,40 @@ class YadioExchangeService extends ExchangeService { String fromCurrency, String toCurrency, ) async { + if (fromCurrency.isEmpty || toCurrency.isEmpty) { + throw ArgumentError('Currency codes cannot be empty'); + } + final endpoint = 'rate/$fromCurrency/$toCurrency'; - final data = await getRequest(endpoint); + try { + final data = await getRequest(endpoint); - if (data.containsKey('rate')) { - return (data['rate'] as num).toDouble(); - } else { - throw Exception('Exchange rate not found in response'); + final rate = data['rate']; + if (rate == null) { + throw Exception('Rate not found for $fromCurrency to $toCurrency'); + } + + if (rate is! num) { + throw Exception('Invalid rate format received from API'); + } + return rate.toDouble(); + } catch (e) { + throw Exception('Failed to fetch exchange rate: $e'); } } @override Future> getCurrencyCodes() async { final endpoint = 'currencies'; - final data = await getRequest(endpoint); - - return data.map((key, value) => MapEntry(key, value.toString())); - + try { + final data = await getRequest(endpoint); + return Map.fromEntries( + data.entries.map((entry) { + return MapEntry(entry.key, entry.value?.toString() ?? ''); + }), + ); + } catch (e) { + throw Exception('Failed to fetch currency codes: $e'); + } } } diff --git a/pubspec.lock b/pubspec.lock index cf117bab..b48ca7ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -280,6 +280,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_intl: + dependency: "direct dev" + description: + name: flutter_intl + sha256: "17b138fab0477c7d17abd8ba67d294786eef8fe80342e58b6253aef7d0ca2bad" + url: "https://pub.dev" + source: hosted + version: "0.0.1" flutter_lints: dependency: "direct dev" description: @@ -288,6 +296,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -397,7 +410,7 @@ packages: source: sdk version: "0.0.0" intl: - dependency: transitive + dependency: "direct main" description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf diff --git a/pubspec.yaml b/pubspec.yaml index c7abee27..ec6dd4db 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,7 +55,10 @@ dependencies: git: url: https://github.com/chebizarro/dart-nip44.git ref: master + intl: ^0.19.0 + flutter_localizations: + sdk: flutter dev_dependencies: flutter_test: @@ -71,12 +74,13 @@ dev_dependencies: mockito: ^5.4.4 integration_test: sdk: flutter + flutter_intl: ^0.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec - # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in @@ -90,10 +94,8 @@ flutter: # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images - # For details regarding adding assets from package dependencies, see # https://flutter.dev/to/asset-from-package - # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a @@ -107,7 +109,8 @@ flutter: - asset: assets/fonts/RobotoCondensed-Medium.ttf weight: 500 - asset: assets/fonts/RobotoCondensed-Regular.ttf - weight: 400 - # + weight: 400 # For details regarding fonts from package dependencies, # see https://flutter.dev/to/font-from-package +flutter_intl: + enabled: true From 60f9840ce7b4f58cb5aa5253b2553a9a1d213a51 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Tue, 19 Nov 2024 22:08:25 -0800 Subject: [PATCH 5/9] more cr suggestions --- lib/data/models/nostr_event.dart | 2 +- lib/data/models/payment_request.dart | 25 ++++++---- lib/data/models/rating.dart | 73 ++++++++++++++++++++++++++++ lib/data/rating.dart | 34 ------------- 4 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 lib/data/models/rating.dart delete mode 100644 lib/data/rating.dart diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index 9e3d05cb..5ca0d78f 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -1,6 +1,6 @@ import 'dart:convert'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/rating.dart'; +import 'package:mostro_mobile/data/models/rating.dart'; import 'package:timeago/timeago.dart' as timeago; import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/data/models/order.dart'; diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart index 070ab9cf..df39bb68 100644 --- a/lib/data/models/payment_request.dart +++ b/lib/data/models/payment_request.dart @@ -16,31 +16,36 @@ class PaymentRequest implements Content { throw ArgumentError('At least one parameter must be provided'); } } - + @override Map toJson() { final typeKey = type; final List values = []; - + values.add(order?.toJson()); values.add(lnInvoice); - + if (amount != null) { values.add(amount); } - - final result = { - typeKey: values - }; + + final result = {typeKey: values}; return result; } factory PaymentRequest.fromJson(List json) { + if (json.length < 2) { + throw FormatException('Invalid JSON format: insufficient elements'); + } + final orderJson = json[0]; + final Order? order = orderJson != null ? Order.fromJson(orderJson) : null; + final amount = json.length > 2 ? json[2] as int? : null; return PaymentRequest( - order: json[0] ?? Order.fromJson(json[0]), - lnInvoice: json[1], - amount: json[2]); + order: order, + lnInvoice: json[1], + amount: amount, + ); } @override diff --git a/lib/data/models/rating.dart b/lib/data/models/rating.dart new file mode 100644 index 00000000..8d43a972 --- /dev/null +++ b/lib/data/models/rating.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +class Rating { + final int totalReviews; + final double totalRating; + final int lastRating; + final int maxRate; + final int minRate; + + Rating( + {required this.totalReviews, + required this.totalRating, + required this.lastRating, + required this.maxRate, + required this.minRate}); + + factory Rating.deserialized(String data) { + if (data.isEmpty) { + throw FormatException('Empty data string provided'); + } + if (data == 'none') { + return Rating( + totalReviews: 0, + totalRating: 0.0, + lastRating: 0, + maxRate: 0, + minRate: 0); + } + try { + final json = jsonDecode(data) as Map; + final rating = json['rating'] as Map?; + + if (rating == null) { + throw FormatException('Missing rating object in JSON'); + } + + return Rating( + totalReviews: _parseIntField(rating, 'total_reviews'), + totalRating: _parseDoubleField(rating, 'total_rating'), + lastRating: _parseIntField(rating, 'last_rating'), + maxRate: _parseIntField(rating, 'max_rate'), + minRate: _parseIntField(rating, 'min_rate'), + ); + } on FormatException { + rethrow; + } catch (e) { + throw FormatException('Failed to parse rating data: $e'); + } + } + static int _parseIntField(Map json, String field) { + final value = json[field]; + if (value == null) { + throw FormatException('Missing required field: $field'); + } + if (value is! num) { + throw FormatException( + 'Invalid type for $field: expected number, got ${value.runtimeType}'); + } + return value.toInt(); + } + + static double _parseDoubleField(Map json, String field) { + final value = json[field]; + if (value == null) { + throw FormatException('Missing required field: $field'); + } + if (value is! num) { + throw FormatException( + 'Invalid type for $field: expected number, got ${value.runtimeType}'); + } + return value.toDouble(); + } +} diff --git a/lib/data/rating.dart b/lib/data/rating.dart deleted file mode 100644 index 96f7ea3a..00000000 --- a/lib/data/rating.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:convert'; - -class Rating { - final int totalReviews; - final double totalRating; - final int lastRating; - final int maxRate; - final int minRate; - - Rating( - {required this.totalReviews, - required this.totalRating, - required this.lastRating, - required this.maxRate, - required this.minRate}); - - factory Rating.deserialized(String data) { - if (data == 'none') { - return Rating( - totalReviews: 0, - totalRating: 0.0, - lastRating: 0, - maxRate: 0, - minRate: 0); - } - final json = jsonDecode(data) as Map; - return Rating( - totalReviews: json['rating']['total_reviews'], - totalRating: json['rating']['total_rating'], - lastRating: json['rating']['last_rating'], - maxRate: json['rating']['max_rate'], - minRate: json['rating']['min_rate']); - } -} From dd64e6b87e050ea175904729131aa32015c599b3 Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Tue, 19 Nov 2024 22:39:12 -0800 Subject: [PATCH 6/9] fixme --- lib/presentation/order/bloc/order_details_bloc.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/presentation/order/bloc/order_details_bloc.dart b/lib/presentation/order/bloc/order_details_bloc.dart index 827f775e..4ed1dc45 100644 --- a/lib/presentation/order/bloc/order_details_bloc.dart +++ b/lib/presentation/order/bloc/order_details_bloc.dart @@ -19,7 +19,6 @@ class OrderDetailsBloc extends Bloc { void _onLoadOrderDetails( LoadOrderDetails event, Emitter emit) { - emit(state.copyWith(status: OrderDetailsStatus.loading)); emit(state.copyWith(status: OrderDetailsStatus.loaded, order: event.order)); } @@ -33,7 +32,7 @@ class OrderDetailsBloc extends Bloc { late MostroMessage order; - if (event.order.orderType == OrderType.buy.value) { + if (event.order.orderType == OrderType.buy) { order = await mostroService.takeBuyOrder(event.order.orderId!); } else { order = await mostroService.takeSellOrder(event.order.orderId!); From ff12b4915e513f7754ad86acac80698732c76eba Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Wed, 20 Nov 2024 01:15:49 -0800 Subject: [PATCH 7/9] Fixes to Code Rabbit's fixes... --- lib/data/models/mostro_message.dart | 2 +- lib/data/models/nostr_event.dart | 2 +- .../widgets/currency_dropdown.dart | 22 ++++++++----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index 5ccf9c12..c60781f3 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -39,7 +39,7 @@ class MostroMessage { ? order['id'] as String? : throw FormatException('Missing id field'); - final content = order['content'] ?? Content.fromJson(event['order']['content']) as T; + final content = order['content'] != null ? Content.fromJson(event['order']['content']) as T : null; return MostroMessage( action: action, diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index 5ca0d78f..33d7c758 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -9,7 +9,7 @@ extension NostrEventExtensions on NostrEvent { // Getters para acceder fácilmente a los tags específicos String? get recipient => _getTagValue('p'); String? get orderId => _getTagValue('d'); - OrderType? get orderType => OrderType.fromString(_getTagValue('k')!); + OrderType? get orderType => _getTagValue('k') != null ? OrderType.fromString(_getTagValue('k')!) : null; String? get currency => _getTagValue('f'); String? get status => _getTagValue('s'); String? get amount => _getTagValue('amt'); diff --git a/lib/presentation/widgets/currency_dropdown.dart b/lib/presentation/widgets/currency_dropdown.dart index 2e33b830..c0129464 100644 --- a/lib/presentation/widgets/currency_dropdown.dart +++ b/lib/presentation/widgets/currency_dropdown.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/theme/app_theme.dart'; import 'package:mostro_mobile/providers/exchange_service_provider.dart'; class CurrencyDropdown extends ConsumerWidget { @@ -13,18 +14,19 @@ class CurrencyDropdown extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currencyCodesAsync = ref.watch(currencyCodesProvider); - final selectedFiatCode = ref.watch(selectedFiatCodeProvider) ?? ''; + final selectedFiatCode = ref.watch(selectedFiatCodeProvider); return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFF1D212C), + color: AppTheme.dark1, borderRadius: BorderRadius.circular(8), ), child: currencyCodesAsync.when( loading: () => const Center( child: SizedBox( - height: double.infinity, + height: 24, + width: 24, child: CircularProgressIndicator(), ), ), @@ -43,7 +45,7 @@ class CurrencyDropdown extends ConsumerWidget { value: code, child: Text( '$code - ${currencyCodes[code]}', - style: const TextStyle(color: Colors.white), + style: const TextStyle(color: AppTheme.cream1), ), ); }).toList(); @@ -52,18 +54,12 @@ class CurrencyDropdown extends ConsumerWidget { decoration: InputDecoration( border: InputBorder.none, labelText: label, - labelStyle: Theme.of(context).inputDecorationTheme.labelStyle, + labelStyle: const TextStyle(color: AppTheme.grey2), ), - dropdownColor: Theme.of(context).colorScheme.surface, - style: Theme.of(context).textTheme.bodyMedium, + dropdownColor: AppTheme.dark1, + style: TextStyle(color: AppTheme.cream1), items: items, value: selectedFiatCode, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please select a currency'; - } - return null; - }, onChanged: (value) => ref.read(selectedFiatCodeProvider.notifier).state = value, ); From 64721c5381f8a294a973c59767c828347393a7bd Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Tue, 26 Nov 2024 08:18:55 -0800 Subject: [PATCH 8/9] more cr suggestions --- lib/core/utils/auth_utils.dart | 10 +- lib/core/utils/nostr_utils.dart | 70 ++++++---- lib/data/models/content.dart | 2 +- lib/data/models/conversion_result.dart | 123 ------------------ lib/data/models/payment_request.dart | 6 +- lib/presentation/widgets/order_list_item.dart | 35 ++--- 6 files changed, 81 insertions(+), 165 deletions(-) delete mode 100644 lib/data/models/conversion_result.dart diff --git a/lib/core/utils/auth_utils.dart b/lib/core/utils/auth_utils.dart index 42825eca..0cc55d5f 100644 --- a/lib/core/utils/auth_utils.dart +++ b/lib/core/utils/auth_utils.dart @@ -1,7 +1,13 @@ class AuthUtils { + /// Temporary implementation for alpha preview. + /// WARNING: This is not secure and should not be used in production. + /// TODO: Implement secure storage for credentials static Future savePrivateKeyAndPin( String privateKey, String pin) async {} + /// Temporary implementation for alpha preview. + /// WARNING: This always returns null and should not be used in production. + /// TODO: Implement secure key retrieval static Future getPrivateKey() async { return null; } @@ -14,7 +20,9 @@ class AuthUtils { throw UnimplementedError('deleteCredentials is not implemented yet'); } - static Future enableBiometrics() async {} + static Future enableBiometrics() async { + throw UnimplementedError('enableBiometrics is not implemented yet'); + } static Future isBiometricsEnabled() async { throw UnimplementedError('isBiometricsEnabled is not implemented yet'); diff --git a/lib/core/utils/nostr_utils.dart b/lib/core/utils/nostr_utils.dart index c2db4570..0ddb8c79 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/core/utils/nostr_utils.dart @@ -10,7 +10,15 @@ class NostrUtils { // Generación de claves static NostrKeyPairs generateKeyPair() { - return NostrKeyPairs(private: generatePrivateKey()); + try { + final privateKey = generatePrivateKey(); + if (!isValidPrivateKey(privateKey)) { + throw Exception('Generated invalid private key'); + } + return NostrKeyPairs(private: privateKey); + } catch (e) { + throw Exception('Failed to generate key pair: $e'); + } } static NostrKeyPairs generateKeyPairFromPrivateKey(String privateKey) { @@ -19,7 +27,11 @@ class NostrUtils { } static String generatePrivateKey() { - return getS256().generatePrivateKey().toHex(); + try { + return getS256().generatePrivateKey().toHex(); + } catch (e) { + throw Exception('Failed to generate private key: $e'); + } } // Codificación y decodificación de claves @@ -180,12 +192,17 @@ class NostrUtils { final pk = wrapperKeyPair.private; - final sealedContent = - _encryptNIP44(jsonEncode(sealEvent.toMap()), pk, '02$recipientPubKey'); + String sealedContent; + try { + sealedContent = await _encryptNIP44( + jsonEncode(sealEvent.toMap()), pk, '02$recipientPubKey'); + } catch (e) { + throw Exception('Failed to encrypt seal event: $e'); + } final wrapEvent = NostrEvent.fromPartialData( kind: 1059, - content: await sealedContent, + content: sealedContent, keyPairs: wrapperKeyPair, tags: [ ["p", recipientPubKey] @@ -265,23 +282,28 @@ class NostrUtils { } } - static Future _encryptNIP44( - String content, String privkey, String pubkey) async { - try { - return await Nip44.encryptMessage(content, privkey, pubkey); - } catch (e) { - // Handle encryption error appropriately - throw Exception('Encryption failed: $e'); - } - } - - static Future _decryptNIP44( - String encryptedContent, String privkey, String pubkey) async { - try { - return await Nip44.decryptMessage(encryptedContent, privkey, pubkey); - } catch (e) { - // Handle encryption error appropriately - throw Exception('Decryption failed: $e'); - } - } + static Future _encryptNIP44( + String content, String privkey, String pubkey) async { + if (content.isEmpty) throw ArgumentError('Content cannot be empty'); + if (privkey.length != 64) throw ArgumentError('Invalid private key length'); + if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format'); + try { + return await Nip44.encryptMessage(content, privkey, pubkey); + } catch (e) { + // Handle encryption error appropriately + throw Exception('Encryption failed: $e'); + } + } + + static Future _decryptNIP44( + String encryptedContent, String privkey, String pubkey) async { + if (encryptedContent.isEmpty) throw ArgumentError('Encrypted content cannot be empty'); + if (privkey.length != 64) throw ArgumentError('Invalid private key length'); + if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format'); + try { + return await Nip44.decryptMessage(encryptedContent, privkey, pubkey); + } catch (e) { + // Handle encryption error appropriately + throw Exception('Decryption failed: $e'); + } } diff --git a/lib/data/models/content.dart b/lib/data/models/content.dart index c5a80a11..f96e7467 100644 --- a/lib/data/models/content.dart +++ b/lib/data/models/content.dart @@ -5,7 +5,7 @@ abstract class Content { String get type; Map toJson(); - factory Content.fromJson(Map json) { + factory Content.fromJson(Map json) {fromJson if (json.containsKey('order')) { return Order.fromJson(json['order']); } else if (json.containsKey('payment_request')) { diff --git a/lib/data/models/conversion_result.dart b/lib/data/models/conversion_result.dart deleted file mode 100644 index 48c2240f..00000000 --- a/lib/data/models/conversion_result.dart +++ /dev/null @@ -1,123 +0,0 @@ -/// 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 json) { - if (json['request'] == null) { - throw FormatException('Missing required field: request'); - } - return ConversionResult( - request: ConversionRequest.fromJson(json['request']), - 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 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 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: amount, - from: from.toUpperCase(), - to: to.toUpperCase(), - ); - } - - Map 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)'; -} \ No newline at end of file diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart index df39bb68..b4f24b34 100644 --- a/lib/data/models/payment_request.dart +++ b/lib/data/models/payment_request.dart @@ -40,10 +40,14 @@ class PaymentRequest implements Content { } final orderJson = json[0]; final Order? order = orderJson != null ? Order.fromJson(orderJson) : null; + final lnInvoice = json[1]; + if (lnInvoice != null && lnInvoice is! String) { + throw FormatException('Invalid type for lnInvoice: expected String'); + } final amount = json.length > 2 ? json[2] as int? : null; return PaymentRequest( order: order, - lnInvoice: json[1], + lnInvoice: lnInvoice as String?, amount: amount, ); } diff --git a/lib/presentation/widgets/order_list_item.dart b/lib/presentation/widgets/order_list_item.dart index b28ba616..225514dd 100644 --- a/lib/presentation/widgets/order_list_item.dart +++ b/lib/presentation/widgets/order_list_item.dart @@ -36,9 +36,10 @@ class OrderListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${order.name} ${order.rating?.totalRating}/${order.rating?.maxRate} (${order.rating?.totalReviews})', - style: const TextStyle(color: Colors.white), - ), + '${order.name} ${order.rating?.totalRating ?? 0}/${order.rating?.maxRate ?? 5} (${order.rating?.totalReviews ?? 0})', + style: const TextStyle(color: Colors.white), + ), + Text( 'Time: ${order.expiration}', style: const TextStyle(color: Colors.white), @@ -111,18 +112,22 @@ class OrderListItem extends StatelessWidget { child: Row( children: [ HeroIcon( - _getPaymentMethodIcon(order.paymentMethods[0]), - style: HeroIconStyle.outline, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - order.paymentMethods[0], - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.visible, - softWrap: true, + _getPaymentMethodIcon(order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : ''), + style: HeroIconStyle.outline, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : 'No payment method', + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.visible, + softWrap: true, ), ), ], From ee67611e356560c6a7c5653628ceec5bce24097c Mon Sep 17 00:00:00 2001 From: Chris Daley Date: Tue, 26 Nov 2024 14:57:06 -0800 Subject: [PATCH 9/9] Addresses @Catrya comments --- lib/core/utils/nostr_utils.dart | 43 ++--- lib/data/models/amount.dart | 39 +++++ lib/data/models/content.dart | 2 +- lib/data/models/nostr_event.dart | 12 +- lib/data/models/order.dart | 2 +- lib/data/models/rating.dart | 79 ++++----- .../order/screens/order_details_screen.dart | 5 +- lib/presentation/widgets/group_box.dart | 53 ------ lib/presentation/widgets/order_filter.dart | 52 ++---- lib/presentation/widgets/order_list_item.dart | 164 ++++++++++-------- 10 files changed, 210 insertions(+), 241 deletions(-) create mode 100644 lib/data/models/amount.dart delete mode 100644 lib/presentation/widgets/group_box.dart diff --git a/lib/core/utils/nostr_utils.dart b/lib/core/utils/nostr_utils.dart index 0ddb8c79..e036aab0 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/core/utils/nostr_utils.dart @@ -282,28 +282,23 @@ class NostrUtils { } } - static Future _encryptNIP44( - String content, String privkey, String pubkey) async { - if (content.isEmpty) throw ArgumentError('Content cannot be empty'); - if (privkey.length != 64) throw ArgumentError('Invalid private key length'); - if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format'); - try { - return await Nip44.encryptMessage(content, privkey, pubkey); - } catch (e) { - // Handle encryption error appropriately - throw Exception('Encryption failed: $e'); - } - } - - static Future _decryptNIP44( - String encryptedContent, String privkey, String pubkey) async { - if (encryptedContent.isEmpty) throw ArgumentError('Encrypted content cannot be empty'); - if (privkey.length != 64) throw ArgumentError('Invalid private key length'); - if (!pubkey.startsWith('02')) throw ArgumentError('Invalid public key format'); - try { - return await Nip44.decryptMessage(encryptedContent, privkey, pubkey); - } catch (e) { - // Handle encryption error appropriately - throw Exception('Decryption failed: $e'); - } + static Future _encryptNIP44( + String content, String privkey, String pubkey) async { + try { + return await Nip44.encryptMessage(content, privkey, pubkey); + } catch (e) { + // Handle encryption error appropriately + throw Exception('Encryption failed: $e'); + } + } + + static Future _decryptNIP44( + String encryptedContent, String privkey, String pubkey) async { + try { + return await Nip44.decryptMessage(encryptedContent, privkey, pubkey); + } catch (e) { + // Handle encryption error appropriately + throw Exception('Decryption failed: $e'); + } + } } diff --git a/lib/data/models/amount.dart b/lib/data/models/amount.dart new file mode 100644 index 00000000..972c4cae --- /dev/null +++ b/lib/data/models/amount.dart @@ -0,0 +1,39 @@ +class Amount { + final int minimum; + final int? maximum; + + Amount(this.minimum, this.maximum); + + factory Amount.fromList(List fa) { + if (fa.length < 2) { + throw ArgumentError( + 'List must have at least two elements: a label and a minimum value.'); + } + + final min = int.tryParse(fa[1]); + if (min == null) { + throw ArgumentError( + 'Second element must be a valid integer representing the minimum value.'); + } + + int? max; + if (fa.length > 2) { + max = int.tryParse(fa[2]); + } + + return Amount(min, max); + } + + factory Amount.empty() { + return Amount(0, null); + } + + @override + String toString() { + if (maximum != null) { + return '$minimum - $maximum'; + } else { + return '$minimum'; + } + } +} diff --git a/lib/data/models/content.dart b/lib/data/models/content.dart index f96e7467..c5a80a11 100644 --- a/lib/data/models/content.dart +++ b/lib/data/models/content.dart @@ -5,7 +5,7 @@ abstract class Content { String get type; Map toJson(); - factory Content.fromJson(Map json) {fromJson + factory Content.fromJson(Map json) { if (json.containsKey('order')) { return Order.fromJson(json['order']); } else if (json.containsKey('payment_request')) { diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index 33d7c758..ab4cf4b1 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:mostro_mobile/data/models/amount.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/rating.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -9,11 +10,13 @@ extension NostrEventExtensions on NostrEvent { // Getters para acceder fácilmente a los tags específicos String? get recipient => _getTagValue('p'); String? get orderId => _getTagValue('d'); - OrderType? get orderType => _getTagValue('k') != null ? OrderType.fromString(_getTagValue('k')!) : null; + OrderType? get orderType => _getTagValue('k') != null + ? OrderType.fromString(_getTagValue('k')!) + : null; String? get currency => _getTagValue('f'); String? get status => _getTagValue('s'); String? get amount => _getTagValue('amt'); - String? get fiatAmount => _getTagValue('fa'); + Amount get fiatAmount => _getAmount('fa'); List get paymentMethods => _getTagValue('pm')?.split(',') ?? []; String? get premium => _getTagValue('premium'); String? get source => _getTagValue('source'); @@ -40,6 +43,11 @@ extension NostrEventExtensions on NostrEvent { return (tag != null && tag.length > 1) ? tag[1] : null; } + Amount _getAmount(String key) { + final tag = tags?.firstWhere((t) => t[0] == key, orElse: () => []); + return (tag != null && tag.length> 1) ? Amount.fromList(tag) : Amount.empty(); + } + String _timeAgo(String? ts) { if (ts == null) return "invalid date"; final timestamp = int.tryParse(ts); diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index afa5f28a..5758db26 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -121,7 +121,7 @@ class Order implements Content { status: Status.fromString(event.status!), amount: event.amount as int, fiatCode: event.currency!, - fiatAmount: int.parse(event.fiatAmount!), + fiatAmount: event.fiatAmount.minimum, paymentMethod: event.paymentMethods.join(','), premium: event.premium as int, createdAt: event.createdAt as int, diff --git a/lib/data/models/rating.dart b/lib/data/models/rating.dart index 8d43a972..1d053377 100644 --- a/lib/data/models/rating.dart +++ b/lib/data/models/rating.dart @@ -7,67 +7,58 @@ class Rating { final int maxRate; final int minRate; - Rating( - {required this.totalReviews, - required this.totalRating, - required this.lastRating, - required this.maxRate, - required this.minRate}); + const Rating({ + required this.totalReviews, + required this.totalRating, + required this.lastRating, + required this.maxRate, + required this.minRate, + }); factory Rating.deserialized(String data) { if (data.isEmpty) { throw FormatException('Empty data string provided'); } + if (data == 'none') { - return Rating( - totalReviews: 0, - totalRating: 0.0, - lastRating: 0, - maxRate: 0, - minRate: 0); + return Rating.empty(); } + try { final json = jsonDecode(data) as Map; - final rating = json['rating'] as Map?; - - if (rating == null) { - throw FormatException('Missing rating object in JSON'); - } - return Rating( - totalReviews: _parseIntField(rating, 'total_reviews'), - totalRating: _parseDoubleField(rating, 'total_rating'), - lastRating: _parseIntField(rating, 'last_rating'), - maxRate: _parseIntField(rating, 'max_rate'), - minRate: _parseIntField(rating, 'min_rate'), + totalReviews: _parseInt(json, 'total_reviews'), + totalRating: _parseDouble(json, 'total_rating'), + lastRating: _parseInt(json, 'last_rating'), + maxRate: _parseInt(json, 'max_rate'), + minRate: _parseInt(json, 'min_rate'), ); - } on FormatException { - rethrow; } catch (e) { throw FormatException('Failed to parse rating data: $e'); } } - static int _parseIntField(Map json, String field) { + + static int _parseInt(Map json, String field) { final value = json[field]; - if (value == null) { - throw FormatException('Missing required field: $field'); - } - if (value is! num) { - throw FormatException( - 'Invalid type for $field: expected number, got ${value.runtimeType}'); - } - return value.toInt(); + if (value is int) return value; + if (value is double) return value.toInt(); + throw FormatException('Invalid value for $field: $value'); } - static double _parseDoubleField(Map json, String field) { + static double _parseDouble(Map json, String field) { final value = json[field]; - if (value == null) { - throw FormatException('Missing required field: $field'); - } - if (value is! num) { - throw FormatException( - 'Invalid type for $field: expected number, got ${value.runtimeType}'); - } - return value.toDouble(); + if (value is double) return value; + if (value is int) return value.toDouble(); + throw FormatException('Invalid value for $field: $value'); + } + + static Rating empty() { + return const Rating( + totalReviews: 0, + totalRating: 0.0, + lastRating: 0, + maxRate: 0, + minRate: 0, + ); } -} +} \ No newline at end of file diff --git a/lib/presentation/order/screens/order_details_screen.dart b/lib/presentation/order/screens/order_details_screen.dart index 14d172f7..a974a1f2 100644 --- a/lib/presentation/order/screens/order_details_screen.dart +++ b/lib/presentation/order/screens/order_details_screen.dart @@ -134,7 +134,8 @@ class OrderDetailsScreen extends ConsumerWidget { Widget _buildContent(BuildContext context, WidgetRef ref, NostrEvent order) { return Scaffold( backgroundColor: AppTheme.dark1, - appBar: _buildAppBar('${order.orderType?.value.toUpperCase()} BITCOIN', context), + appBar: _buildAppBar( + '${order.orderType?.value.toUpperCase()} BITCOIN', context), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(16.0), @@ -291,7 +292,7 @@ class OrderDetailsScreen extends ConsumerWidget { Expanded( child: ElevatedButton( onPressed: () { - context.read().add(CancelOrder()); + Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, diff --git a/lib/presentation/widgets/group_box.dart b/lib/presentation/widgets/group_box.dart deleted file mode 100644 index b2b8c473..00000000 --- a/lib/presentation/widgets/group_box.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; - -class GroupBox extends StatelessWidget { - final String title; - final Widget child; - - const GroupBox({ - super.key, - required this.title, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return Stack( - clipBehavior: Clip.none, // Allow overflow of title - children: [ - // The group box with content - Container( - padding: const EdgeInsets.all(12.0), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey, width: 1.5), - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - child, - ], - ), - ), - // The title widget overlapping the border - Positioned( - top: -10, // Adjust this value for how much you want to overlap - left: 10, // Adjust horizontal alignment if needed - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - color: Colors.white, // This covers the border underneath the title - child: Text( - title, - style: TextStyle( - color: AppTheme.mostroGreen, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/presentation/widgets/order_filter.dart b/lib/presentation/widgets/order_filter.dart index 15e8d16d..05fa1873 100644 --- a/lib/presentation/widgets/order_filter.dart +++ b/lib/presentation/widgets/order_filter.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/theme/app_theme.dart'; -import 'package:mostro_mobile/presentation/widgets/group_box.dart'; class OrderFilter extends StatelessWidget { const OrderFilter({super.key}); @@ -51,55 +50,34 @@ class OrderFilter extends StatelessWidget { ], ), SizedBox(height: 20), - buildDropdownSection(context, 'Fiat currencies', ''), - buildDropdownSection(context, 'Payment methods', ''), - buildDropdownSection(context, 'Countries', ''), - buildDropdownSection(context, 'Rating', ''), + buildDropdownSection(context, 'Fiat currencies', []), + buildDropdownSection(context, 'Payment methods', []), + buildDropdownSection(context, 'Countries', []), + buildDropdownSection(context, 'Rating', []), ], ), ); } Widget buildDropdownSection( - BuildContext context, String title, String value) { + BuildContext context, String title, List> items) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 4), - Container( - padding: EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppTheme.grey), + DropdownButtonFormField( + decoration: InputDecoration( + border: InputBorder.none, + labelText: title, + labelStyle: const TextStyle(color: AppTheme.mostroGreen), ), - child: GroupBox( - title: title, - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: value, - onChanged: (String? newValue) {}, - items: [value] - .map>((String value) { - return DropdownMenuItem( - value: value, - child: Text( - value, - style: TextStyle( - color: AppTheme.dark1, - fontSize: 14, - ), - ), - ); - }).toList(), - icon: Icon( - Icons.arrow_drop_down, - color: AppTheme.dark1, - ), - ), - )), + dropdownColor: AppTheme.dark1, + style: TextStyle(color: AppTheme.cream1), + items: items, + value: '', onChanged: (String? value) { }, ), + SizedBox(height: 4), ], ), ); diff --git a/lib/presentation/widgets/order_list_item.dart b/lib/presentation/widgets/order_list_item.dart index 225514dd..a0befac3 100644 --- a/lib/presentation/widgets/order_list_item.dart +++ b/lib/presentation/widgets/order_list_item.dart @@ -2,6 +2,7 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/presentation/order/screens/order_details_screen.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -36,10 +37,9 @@ class OrderListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${order.name} ${order.rating?.totalRating ?? 0}/${order.rating?.maxRate ?? 5} (${order.rating?.totalReviews ?? 0})', - style: const TextStyle(color: Colors.white), - ), - + '${order.name} ${order.rating?.totalRating ?? 0}/${order.rating?.maxRate ?? 5} (${order.rating?.totalReviews ?? 0})', + style: const TextStyle(color: Colors.white), + ), Text( 'Time: ${order.expiration}', style: const TextStyle(color: Colors.white), @@ -51,83 +51,29 @@ class OrderListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan( - children: [ - _buildStyledTextSpan( - 'offering ', - '${order.amount}', - isValue: true, - isBold: true, - ), - const TextSpan( - text: "sats", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.normal, - fontSize: 16.0, - ), - ), - ], - ), - ), - const SizedBox(height: 8.0), - Text.rich( - TextSpan( - children: [ - _buildStyledTextSpan( - 'for ', - '${order.fiatAmount}', - isValue: true, - isBold: true, - ), - TextSpan( - text: '${order.currency} ', - style: const TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - TextSpan( - text: '(${order.premium}%)', - style: const TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - ], - ), - ), - ], - ), - ), + _getOrder(order), const SizedBox(width: 16), Expanded( flex: 4, child: Row( children: [ HeroIcon( - _getPaymentMethodIcon(order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : ''), - style: HeroIconStyle.outline, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : 'No payment method', - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.visible, - softWrap: true, + _getPaymentMethodIcon(order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : ''), + style: HeroIconStyle.outline, + color: Colors.white, + size: 16, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : 'No payment method', + style: const TextStyle(color: Colors.grey), + overflow: TextOverflow.visible, + softWrap: true, ), ), ], @@ -135,8 +81,7 @@ class OrderListItem extends StatelessWidget { ), ], ), - - const SizedBox(height: 8), // Optional spacer after the row + const SizedBox(height: 8), ], ), ), @@ -144,6 +89,71 @@ class OrderListItem extends StatelessWidget { ); } + Expanded _getOrder(NostrEvent order) { + String offering; + if (order.orderType == OrderType.buy) { + offering = 'Buying'; + } else { + offering = 'Selling'; + } + + return Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + _buildStyledTextSpan( + offering, + (order.amount != null && order.amount != '0') ? ' ${order.amount!}' : '', + isValue: true, + isBold: true, + ), + const TextSpan( + text: "sats", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.normal, + fontSize: 16.0, + ), + ), + ], + ), + ), + const SizedBox(height: 8.0), + Text.rich( + TextSpan( + children: [ + _buildStyledTextSpan( + 'for ', + '${order.fiatAmount}', + isValue: true, + isBold: true, + ), + TextSpan( + text: '${order.currency} ', + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + TextSpan( + text: '(${order.premium}%)', + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + ), + ), + ], + ), + ), + ], + ), + ); + } + HeroIcons _getPaymentMethodIcon(String method) { switch (method.toLowerCase()) { case 'wire transfer':