diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart index 5021e5bf0..a8e4d5e50 100644 --- a/lib/core/app_routes.dart +++ b/lib/core/app_routes.dart @@ -20,6 +20,8 @@ import 'package:mostro/features/rate/screens/rate_counterpart_screen.dart'; import 'package:mostro/features/about/screens/about_screen.dart'; import 'package:mostro/features/about/screens/node_technical_data_screen.dart'; import 'package:mostro/features/settings/screens/nwc_wallet_screen.dart'; +import 'package:mostro/features/reputation/screens/import_reputation_screen.dart'; +import 'package:mostro/features/reputation/screens/reputation_screen.dart'; import 'package:mostro/features/settings/screens/log_report_screen.dart'; import 'package:mostro/features/settings/screens/notification_settings_screen.dart'; import 'package:mostro/features/settings/screens/settings_screen.dart'; @@ -58,6 +60,8 @@ abstract final class AppRoute { static const disputeDetails = '/dispute_details/:disputeId'; static const notificationSettings = '/notification_settings'; static const logs = '/logs'; + static const importReputation = '/import_reputation'; + static const reputation = '/reputation'; static const disputeChat = '/dispute_chat/:disputeId'; /// Embedded Cashu wallet. Only reachable from Settings when the active node @@ -249,6 +253,14 @@ final GoRouter appRouter = GoRouter( builder: (_, __) => const NotificationSettingsScreen(), ), GoRoute(path: AppRoute.logs, builder: (_, __) => const LogReportScreen()), + GoRoute( + path: AppRoute.importReputation, + builder: (_, __) => const ImportReputationScreen(), + ), + GoRoute( + path: AppRoute.reputation, + builder: (_, __) => const ReputationScreen(), + ), GoRoute( path: AppRoute.disputeChat, builder: @@ -261,9 +273,9 @@ final GoRouter appRouter = GoRouter( ), GoRoute( path: AppRoute.lockEscrow, - builder: (context, state) => LockEscrowScreen( - orderId: state.pathParameters['orderId']!, - ), + builder: + (context, state) => + LockEscrowScreen(orderId: state.pathParameters['orderId']!), ), ], ); diff --git a/lib/core/automation/automation_ids.dart b/lib/core/automation/automation_ids.dart index 6ef88f7a6..692d9be9b 100644 --- a/lib/core/automation/automation_ids.dart +++ b/lib/core/automation/automation_ids.dart @@ -25,6 +25,20 @@ class AutomationIds { // App bar / navigation static const String appBarDrawer = 'appbar.drawer'; static const String appBarBack = 'appbar.back'; + + // Reputation portability (#674) + static const String settingsReputation = 'settings.reputation'; + static const String reputationOpenImport = 'reputation.open_import'; + static const String reputationExport = 'reputation.export'; + static const String reputationExportConfirm = 'reputation.export.confirm'; + static const String reputationExported = 'reputation.export.result'; + static const String reputationRebindIdentity = 'reputation.rebind.identity'; + static const String reputationRebindSign = 'reputation.rebind.sign'; + static const String reputationImportOpenBot = 'reputation.import.open_bot'; + static const String reputationImportInput = 'reputation.import.input'; + static const String reputationImportCheck = 'reputation.import.check'; + static const String reputationImportFigures = 'reputation.import.figures'; + static const String reputationImportConfirm = 'reputation.import.confirm'; static const String navOrderBook = 'nav.order_book'; static const String navTrades = 'nav.trades'; static const String navChat = 'nav.chat'; diff --git a/lib/features/reputation/lnp2pbot.dart b/lib/features/reputation/lnp2pbot.dart new file mode 100644 index 000000000..09e4473bb --- /dev/null +++ b/lib/features/reputation/lnp2pbot.dart @@ -0,0 +1,37 @@ +import 'dart:convert'; + +/// lnp2pBot issues reputation attestations through Telegram +/// (MostroP2P/protocol reputation_attestation.md, "Telegram hand-off"). +const lnp2pbotUsername = 'lnp2pbot'; + +final _hexKey = RegExp(r'^[0-9a-f]{64}$'); + +/// The link that asks lnp2pBot to attest the user's reputation for +/// [identityHex], or null for a value that is not a hex key. The identity +/// travels as 43 characters of unpadded base64url so the start parameter +/// fits Telegram's 64-character cap. +Uri? lnp2pbotExportUri(String identityHex) { + final hex = identityHex.toLowerCase(); + if (!_hexKey.hasMatch(hex)) return null; + final bytes = [ + for (var i = 0; i < hex.length; i += 2) + int.parse(hex.substring(i, i + 2), radix: 16), + ]; + final encoded = base64Url.encode(bytes).replaceAll('=', ''); + return Uri.parse('https://t.me/$lnp2pbotUsername?start=rep_$encoded'); +} + +/// The attestation event JSON inside pasted text: lnp2pBot sends it as a +/// message of its own, but a paste may carry surrounding text or whitespace. +/// Returns null when no JSON object is found. +String? extractAttestationJson(String text) { + final start = text.indexOf('{'); + final end = text.lastIndexOf('}'); + if (start < 0 || end <= start) return null; + final candidate = text.substring(start, end + 1); + try { + return jsonDecode(candidate) is Map ? candidate : null; + } on FormatException { + return null; + } +} diff --git a/lib/features/reputation/npub.dart b/lib/features/reputation/npub.dart new file mode 100644 index 000000000..65c34a1cf --- /dev/null +++ b/lib/features/reputation/npub.dart @@ -0,0 +1,44 @@ +/// A 64-character hex public key as an `npub` (NIP-19 bech32), for showing +/// the user which identity they confirm. Returns null for anything else. +String? hexToNpub(String hex) { + if (!RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(hex)) return null; + final bytes = [ + for (var i = 0; i < 64; i += 2) + int.parse(hex.substring(i, i + 2), radix: 16), + ]; + // Regroup 8-bit bytes into 5-bit words. + final words = []; + var acc = 0, bits = 0; + for (final b in bytes) { + acc = (acc << 8) | b; + bits += 8; + while (bits >= 5) { + bits -= 5; + words.add((acc >> bits) & 31); + } + } + if (bits > 0) words.add((acc << (5 - bits)) & 31); + const hrp = 'npub'; + const charset = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + int polymod(List values) { + const gen = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + var chk = 1; + for (final v in values) { + final top = chk >> 25; + chk = ((chk & 0x1ffffff) << 5) ^ v; + for (var i = 0; i < 5; i++) { + if ((top >> i) & 1 == 1) chk ^= gen[i]; + } + } + return chk; + } + + final expanded = [ + for (final c in hrp.codeUnits) c >> 5, + 0, + for (final c in hrp.codeUnits) c & 31, + ]; + final mod = polymod([...expanded, ...words, 0, 0, 0, 0, 0, 0]) ^ 1; + final checksum = [for (var i = 0; i < 6; i++) (mod >> (5 * (5 - i))) & 31]; + return '${hrp}1${[...words, ...checksum].map((w) => charset[w]).join()}'; +} diff --git a/lib/features/reputation/reputation_api.dart b/lib/features/reputation/reputation_api.dart new file mode 100644 index 000000000..8db499f95 --- /dev/null +++ b/lib/features/reputation/reputation_api.dart @@ -0,0 +1,39 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:mostro/src/rust/api/identity.dart' as identity_api; +import 'package:mostro/src/rust/api/reputation_transfer.dart' as rust; + +/// The reputation portability calls the screens make, behind one seam so the +/// widget tests never reach the Rust core. +class ReputationApi { + const ReputationApi(); + + rust.ReputationSupportInfo support() => rust.getReputationSupport(); + + rust.ReputationAttestationInfo parse(String json) => + rust.parseReputationAttestation(json: json); + + Future import(String json) => + rust.importReputation(attestationJson: json); + + Future export({ + String? destination, + String? rebind, + }) => rust.exportReputation(destination: destination, rebind: rebind); + + Future signRebind({ + required String issuer, + required String newIdentity, + }) => rust.signReputationRebind(issuer: issuer, newIdentity: newIdentity); + + Future pending() => + rust.getPendingReputationAttestation(); + + /// The user's identity public key, hex, or null without one. + Future identity() async => + (await identity_api.getIdentity())?.publicKey; +} + +final reputationApiProvider = Provider( + (ref) => const ReputationApi(), +); diff --git a/lib/features/reputation/reputation_errors.dart b/lib/features/reputation/reputation_errors.dart new file mode 100644 index 000000000..b55c25513 --- /dev/null +++ b/lib/features/reputation/reputation_errors.dart @@ -0,0 +1,34 @@ +import 'package:mostro/core/daemon_errors.dart'; +import 'package:mostro/l10n/app_localizations.dart'; + +/// User-facing text for a failed reputation export or import: the markers +/// `api::reputation_transfer` raises, then the shared daemon markers +/// (`NoDaemonResponse`, node capability), then [fallback]. +String localizedReputationError( + AppLocalizations l10n, + Object error, { + String? fallback, +}) { + final raw = error.toString(); + final byMarker = { + 'PrivacyModeEnabled': l10n.reputationIdentityRequired, + 'ReputationIdentityRequired': l10n.reputationIdentityRequired, + 'ReputationExportUnsupported': l10n.reputationNodeDoesNotExport, + 'ReputationImportUnsupported': l10n.reputationNodeDoesNotImport, + 'NotEligibleForReputationExport': l10n.reputationNotEligible, + 'ReputationBoundToOtherIdentity': l10n.reputationBoundToOtherIdentity, + 'InvalidReputationRebind': l10n.reputationInvalidRebind, + 'ExpiredReputationAttestation': l10n.reputationExpiredAttestation, + 'UntrustedReputationIssuer': l10n.reputationUntrustedIssuer, + 'ReputationIdentityMismatch': l10n.reputationIdentityMismatch, + 'ReputationAlreadyImported': l10n.reputationAlreadyImported, + }; + for (final entry in byMarker.entries) { + if (raw.contains(entry.key)) return entry.value; + } + return localizedDaemonError( + l10n, + error, + fallback: fallback ?? l10n.reputationInvalidAttestation, + ); +} diff --git a/lib/features/reputation/screens/import_reputation_screen.dart b/lib/features/reputation/screens/import_reputation_screen.dart new file mode 100644 index 000000000..bfbcb322d --- /dev/null +++ b/lib/features/reputation/screens/import_reputation_screen.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:mostro/core/app_routes.dart'; +import 'package:mostro/core/automation/automation_id.dart'; +import 'package:mostro/core/automation/automation_ids.dart'; +import 'package:mostro/core/order_book_palette.dart'; +import 'package:mostro/core/settings_palette.dart'; +import 'package:mostro/features/reputation/lnp2pbot.dart'; +import 'package:mostro/features/reputation/reputation_api.dart'; +import 'package:mostro/features/reputation/reputation_errors.dart'; +import 'package:mostro/features/reputation/widgets/reputation_controls.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:mostro/shared/utils/platform_int64.dart'; +import 'package:mostro/shared/widgets/redesign_app_bar.dart'; +import 'package:mostro/src/rust/api/reputation_transfer.dart'; + +/// Import reputation earned elsewhere: get an attestation from lnp2pBot on +/// Telegram, or exported from another Mostro, check its figures, and import +/// it into the active node (#674). +class ImportReputationScreen extends ConsumerStatefulWidget { + const ImportReputationScreen({super.key}); + + @override + ConsumerState createState() => + _ImportReputationScreenState(); +} + +class _ImportReputationScreenState + extends ConsumerState { + final _input = TextEditingController(); + ReputationAttestationInfo? _attestation; + String? _error; + bool _busy = false; + bool _imported = false; + + @override + void initState() { + super.initState(); + // An attestation exported from another Mostro waits to be imported. + ref.read(reputationApiProvider).pending().then((pending) { + if (!mounted || pending == null || _input.text.isNotEmpty) return; + _input.text = pending.json; + _check(); + }, onError: (_) {}); + } + + @override + void dispose() { + _input.dispose(); + super.dispose(); + } + + void _check() { + final l10n = AppLocalizations.of(context); + final json = extractAttestationJson(_input.text); + setState(() { + _attestation = null; + _imported = false; + _error = null; + if (json == null) { + _error = l10n.reputationInvalidAttestation; + return; + } + try { + _attestation = ref.read(reputationApiProvider).parse(json); + } catch (e) { + _error = localizedReputationError(l10n, e); + } + }); + } + + Future _import() async { + final attestation = _attestation; + if (attestation == null) return; + final l10n = AppLocalizations.of(context); + setState(() => _busy = true); + try { + await ref.read(reputationApiProvider).import(attestation.json); + if (mounted) setState(() => _imported = true); + } catch (e) { + if (mounted) setState(() => _error = localizedReputationError(l10n, e)); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + Future _openBot() async { + final identity = await ref.read(reputationApiProvider).identity(); + final uri = identity == null ? null : lnp2pbotExportUri(identity); + if (uri == null) return; + try { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } catch (_) { + // No app for the link: the user can still paste what they received. + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final book = OrderBookPalette.of(context); + final attestation = _attestation; + final settings = SettingsPalette.of(context); + return Scaffold( + backgroundColor: book.bg, + appBar: redesignAppBar( + context, + title: l10n.reputationImportTitle, + onBack: + () => + context.canPop() + ? context.pop() + : context.go(AppRoute.settings), + ), + body: ListView( + padding: EdgeInsets.fromLTRB( + redesignSidePadding, + 6, + redesignSidePadding, + 14 + MediaQuery.of(context).viewPadding.bottom, + ), + children: [ + Text( + l10n.reputationImportIntro, + style: TextStyle(color: book.textMuted, fontSize: 13), + ), + const SizedBox(height: 14), + ReputationSecondaryButton( + icon: Icons.send_outlined, + label: l10n.reputationOpenLnp2pbot, + onPressed: _openBot, + ).withAutomationId(AutomationIds.reputationImportOpenBot), + const SizedBox(height: 14), + TextField( + controller: _input, + minLines: 3, + maxLines: 6, + style: TextStyle(color: book.textPrimary, fontSize: 12), + decoration: InputDecoration( + hintText: l10n.reputationPasteHint, + hintStyle: TextStyle(color: settings.placeholder, fontSize: 12), + filled: false, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: settings.fieldUnderline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: settings.fieldUnderlineFocus, + width: 1.5, + ), + ), + ), + ).withAutomationId(AutomationIds.reputationImportInput), + const SizedBox(height: 8), + ReputationPrimaryButton( + label: l10n.reputationCheck, + onPressed: _busy ? null : _check, + ).withAutomationId(AutomationIds.reputationImportCheck), + if (_error != null) ...[ + const SizedBox(height: 12), + Text(_error!, style: TextStyle(color: book.sell)), + ], + if (attestation != null) ...[ + const SizedBox(height: 14), + _Figures( + attestation: attestation, + ).withAutomationId(AutomationIds.reputationImportFigures), + const SizedBox(height: 14), + if (_imported) + Text( + l10n.reputationImported, + style: TextStyle(color: book.limeText), + ) + else + ReputationPrimaryButton( + label: l10n.reputationImportConfirm, + onPressed: _busy ? null : _import, + ).withAutomationId(AutomationIds.reputationImportConfirm), + ], + ], + ), + ); + } +} + +class _Figures extends StatelessWidget { + final ReputationAttestationInfo attestation; + + const _Figures({required this.attestation}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final book = OrderBookPalette.of(context); + final since = DateTime.fromMillisecondsSinceEpoch( + platformInt64ToInt(attestation.since) * 1000, + isUtc: true, + ); + final days = DateTime.now().toUtc().difference(since).inDays; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: book.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: book.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.reputationFigures( + attestation.reviews, + attestation.rating, + days < 0 ? 0 : days, + ), + style: TextStyle(color: book.textPrimary, fontSize: 15), + ), + const SizedBox(height: 6), + Text( + l10n.reputationFiguresNote, + style: TextStyle(color: book.textMuted, fontSize: 12), + ), + ], + ), + ); + } +} diff --git a/lib/features/reputation/screens/reputation_screen.dart b/lib/features/reputation/screens/reputation_screen.dart new file mode 100644 index 000000000..71e993a07 --- /dev/null +++ b/lib/features/reputation/screens/reputation_screen.dart @@ -0,0 +1,287 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import 'package:mostro/core/app_routes.dart'; +import 'package:mostro/core/automation/automation_id.dart'; +import 'package:mostro/core/automation/automation_ids.dart'; +import 'package:mostro/core/order_book_palette.dart'; +import 'package:mostro/core/settings_palette.dart'; +import 'package:mostro/features/account/providers/privacy_mode_provider.dart'; +import 'package:mostro/features/reputation/npub.dart'; +import 'package:mostro/features/reputation/reputation_api.dart'; +import 'package:mostro/features/reputation/reputation_errors.dart'; +import 'package:mostro/features/reputation/widgets/reputation_controls.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:mostro/shared/widgets/mostro_modal.dart'; +import 'package:mostro/shared/widgets/redesign_app_bar.dart'; + +/// Reputation (#674): export what the user earned on the active node, import +/// what they earned elsewhere, and authorise moving an exported reputation to +/// a new identity. +class ReputationScreen extends ConsumerStatefulWidget { + const ReputationScreen({super.key}); + + @override + ConsumerState createState() => _ReputationScreenState(); +} + +class _ReputationScreenState extends ConsumerState { + final _newIdentity = TextEditingController(); + String? _exported; + String? _rebind; + String? _error; + bool _busy = false; + + @override + void dispose() { + _newIdentity.dispose(); + super.dispose(); + } + + Future _export() async { + final l10n = AppLocalizations.of(context); + final api = ref.read(reputationApiProvider); + final identity = await api.identity(); + if (identity == null || !mounted) return; + // The request binds the node's account to the identity: confirm it. + final confirmed = await showMostroDialog( + context: context, + builder: + (context) => MostroDialog( + title: l10n.reputationExportConfirmTitle, + content: SelectableText( + l10n.reputationExportConfirmBody(hexToNpub(identity) ?? identity), + ), + secondary: ModalAction( + label: l10n.cancel, + onPressed: () => Navigator.of(context).pop(false), + ), + primary: ModalAction( + label: l10n.reputationExportConfirmButton, + onPressed: () => Navigator.of(context).pop(true), + automationId: AutomationIds.reputationExportConfirm, + ), + ), + ); + if (confirmed != true || !mounted) return; + setState(() { + _busy = true; + _error = null; + _exported = null; + }); + try { + final attestation = await api.export(); + if (mounted) setState(() => _exported = attestation.json); + } catch (e) { + if (mounted) setState(() => _error = localizedReputationError(l10n, e)); + } finally { + if (mounted) setState(() => _busy = false); + } + } + + Future _signRebind(String issuer) async { + final l10n = AppLocalizations.of(context); + final input = _newIdentity.text.trim(); + setState(() { + _error = null; + _rebind = null; + }); + if (input.isEmpty) { + setState(() => _error = l10n.reputationInvalidIdentity); + return; + } + try { + final rebind = await ref + .read(reputationApiProvider) + .signRebind(issuer: issuer, newIdentity: input); + if (mounted) setState(() => _rebind = rebind); + } catch (e) { + if (mounted) { + setState( + () => + _error = localizedReputationError( + l10n, + e, + fallback: l10n.reputationInvalidIdentity, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final book = OrderBookPalette.of(context); + final settings = SettingsPalette.of(context); + final support = ref.read(reputationApiProvider).support(); + final privacy = ref.watch(privacyModeProvider); + final body = TextStyle(color: book.textMuted, fontSize: 13); + final issuer = support.issuer; + return Scaffold( + backgroundColor: book.bg, + appBar: redesignAppBar( + context, + title: l10n.reputationSettingTitle, + onBack: + () => + context.canPop() + ? context.pop() + : context.go(AppRoute.settings), + ), + body: ListView( + padding: EdgeInsets.fromLTRB( + redesignSidePadding, + 6, + redesignSidePadding, + 14 + MediaQuery.of(context).viewPadding.bottom, + ), + children: [ + if (privacy) + _Card(child: Text(l10n.reputationIdentityRequired, style: body)), + _Card( + title: l10n.reputationImportTitle, + children: [ + Text(l10n.reputationImportCardBody, style: body), + const SizedBox(height: 10), + ReputationSecondaryButton( + label: l10n.reputationImportTitle, + onPressed: + privacy + ? null + : () => context.push(AppRoute.importReputation), + ).withAutomationId(AutomationIds.reputationOpenImport), + ], + ), + _Card( + title: l10n.reputationExportTitle, + children: + issuer == null + ? [Text(l10n.reputationNodeDoesNotExport, style: body)] + : [ + Text(l10n.reputationExportCardBody, style: body), + const SizedBox(height: 10), + ReputationPrimaryButton( + label: l10n.reputationExportTitle, + onPressed: privacy || _busy ? null : _export, + ).withAutomationId(AutomationIds.reputationExport), + if (_exported != null) ...[ + const SizedBox(height: 10), + Text(l10n.reputationExported, style: body), + _Copyable( + text: _exported!, + ).withAutomationId(AutomationIds.reputationExported), + ], + const SizedBox(height: 16), + Text(l10n.reputationRebindBody, style: body), + TextField( + controller: _newIdentity, + style: TextStyle(color: book.textPrimary), + decoration: InputDecoration( + isDense: true, + hintText: l10n.reputationNewIdentityHint, + hintStyle: TextStyle(color: settings.placeholder), + filled: false, + contentPadding: const EdgeInsets.only( + top: 6, + bottom: 8, + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: settings.fieldUnderline, + ), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: settings.fieldUnderlineFocus, + width: 1.5, + ), + ), + ), + ).withAutomationId( + AutomationIds.reputationRebindIdentity, + ), + const SizedBox(height: 8), + ReputationSecondaryButton( + label: l10n.reputationRebindSign, + onPressed: privacy ? null : () => _signRebind(issuer), + ).withAutomationId(AutomationIds.reputationRebindSign), + if (_rebind != null) _Copyable(text: _rebind!), + ], + ), + if (_error != null) Text(_error!, style: TextStyle(color: book.sell)), + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + final String? title; + final Widget? child; + final List children; + + const _Card({this.title, this.child, this.children = const []}); + + @override + Widget build(BuildContext context) { + final book = OrderBookPalette.of(context); + return Container( + margin: const EdgeInsets.only(bottom: 14), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: book.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: book.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (title != null) ...[ + Text( + title!, + style: TextStyle( + color: book.textPrimary, + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + ], + if (child != null) child!, + ...children, + ], + ), + ); + } +} + +/// A long value the user copies whole into another app. +class _Copyable extends StatelessWidget { + final String text; + + const _Copyable({required this.text}); + + @override + Widget build(BuildContext context) { + final book = OrderBookPalette.of(context); + return Row( + children: [ + Expanded( + child: Text( + text, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: book.textPrimary, fontSize: 11), + ), + ), + IconButton( + icon: Icon(Icons.copy, color: book.textMuted), + onPressed: () => Clipboard.setData(ClipboardData(text: text)), + ), + ], + ); + } +} diff --git a/lib/features/reputation/widgets/reputation_controls.dart b/lib/features/reputation/widgets/reputation_controls.dart new file mode 100644 index 000000000..6b6538942 --- /dev/null +++ b/lib/features/reputation/widgets/reputation_controls.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; + +import 'package:mostro/core/app_theme.dart'; +import 'package:mostro/core/settings_palette.dart'; + +const _shape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), +); +const _label = TextStyle( + fontFamily: AppFonts.ui, + fontSize: 15, + fontWeight: FontWeight.w600, +); + +/// The reputation screens' main action: lime, radius 16 (DS-CMP-3), styled +/// like the NWC connect button. +class ReputationPrimaryButton extends StatelessWidget { + const ReputationPrimaryButton({ + super.key, + required this.label, + required this.onPressed, + }); + + final String label; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + final book = OrderBookPalette.of(context); + final pal = SettingsPalette.of(context); + return DecoratedBox( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(16)), + boxShadow: onPressed != null ? pal.ctaShadow : const [], + ), + child: FilledButton( + onPressed: onPressed, + style: FilledButton.styleFrom( + backgroundColor: book.lime, + foregroundColor: book.onLime, + disabledBackgroundColor: pal.ctaDisabledBg, + disabledForegroundColor: pal.ctaDisabledInk, + minimumSize: const Size.fromHeight(50), + shape: _shape, + textStyle: _label, + ), + child: Text(label), + ), + ); + } +} + +/// A secondary action: outlined in the palette's border, radius 16 (DS-CMP-4). +class ReputationSecondaryButton extends StatelessWidget { + const ReputationSecondaryButton({ + super.key, + required this.label, + required this.onPressed, + this.icon, + }); + + final String label; + final VoidCallback? onPressed; + final IconData? icon; + + @override + Widget build(BuildContext context) { + final book = OrderBookPalette.of(context); + final style = OutlinedButton.styleFrom( + foregroundColor: book.textPrimary, + side: BorderSide(color: book.border), + minimumSize: const Size.fromHeight(50), + shape: _shape, + textStyle: _label, + ); + final icon = this.icon; + if (icon == null) { + return OutlinedButton( + onPressed: onPressed, + style: style, + child: Text(label), + ); + } + return OutlinedButton.icon( + onPressed: onPressed, + style: style, + icon: Icon(icon, size: 18), + label: Text(label), + ); + } +} diff --git a/lib/features/settings/screens/settings_screen.dart b/lib/features/settings/screens/settings_screen.dart index ddf325e8d..040eb6f9c 100644 --- a/lib/features/settings/screens/settings_screen.dart +++ b/lib/features/settings/screens/settings_screen.dart @@ -124,6 +124,11 @@ class SettingsScreen extends ConsumerWidget { merge: false, ), _relaysRow(context, ref, l10n), + SettingsRow( + icon: Icons.workspace_premium_outlined, + label: l10n.reputationSettingTitle, + onTap: () => context.push(AppRoute.reputation), + ).withAutomationId(AutomationIds.settingsReputation), ], ), const SizedBox(height: settingsGroupGap), diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 22a4c8301..fee95a240 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1256,5 +1256,37 @@ "lockEscrowOrderMovedOn": "Der Node meldet, dass diese Order nicht mehr auf den Escrow wartet. Dein Escrow bleibt auf diesem Gerät gespeichert; der Status des Trades aktualisiert sich, sobald der Node ihn meldet.", "tradeHeadlineWaitingPaymentSellerCashu": "Sperre den Escrow, um den Trade zu starten", "tradeWaitingPaymentSellerInstructionCashu": "Sperre dein Ecash im Escrow bei der Mint des Nodes. Das startet den Trade, und es kommt zu dir zurück, falls der Node verschwindet.", - "tradeBodyWaitingPaymentBuyerCashu": "Die Gegenseite sperrt die Sats im Escrow. Sobald er gesperrt ist, bist du dran, den Fiat-Betrag zu zahlen." + "tradeBodyWaitingPaymentBuyerCashu": "Die Gegenseite sperrt die Sats im Escrow. Sobald er gesperrt ist, bist du dran, den Fiat-Betrag zu zahlen.", + "reputationNodeDoesNotExport": "Dieser Mostro exportiert keine Reputation.", + "reputationNodeDoesNotImport": "Dieser Mostro importiert keine Reputation.", + "reputationIdentityRequired": "Reputation kann nur von einem Konto übertragen werden, das seine Reputation behält. Schalte den vollständigen Privatsphäre-Modus aus und versuche es erneut.", + "reputationNotEligible": "Dein Konto kann seine Reputation noch nicht exportieren: Es braucht mindestens 10 abgeschlossene Trades und 5 erhaltene Bewertungen.", + "reputationBoundToOtherIdentity": "Deine Reputation dort ist bereits an eine andere Identität gebunden. Autorisiere den Wechsel mit der Identität, an die sie gebunden ist.", + "reputationInvalidRebind": "Die Autorisierung zum Übertragen deiner Reputation ist ungültig oder abgelaufen. Erstelle eine neue.", + "reputationExpiredAttestation": "Diese Reputation ist abgelaufen. Fordere sie erneut bei ihrer Quelle an.", + "reputationUntrustedIssuer": "Dieser Mostro akzeptiert keine Reputation aus dieser Quelle.", + "reputationIdentityMismatch": "Diese Reputation wurde für eine andere Identität ausgestellt als die, die du verwendest.", + "reputationAlreadyImported": "Diese Reputation wurde auf diesem Mostro bereits importiert.", + "reputationInvalidAttestation": "Das ist keine gültige Reputation. Fordere sie erneut bei ihrer Quelle an.", + "reputationImportTitle": "Reputation importieren", + "reputationImportIntro": "Bring die Reputation, die du auf lnp2pBot oder einem anderen Mostro verdient hast, auf diesen. Der Import verknüpft die beiden Konten: Die Quelle und dieser Mostro wissen, dass sie derselben Person gehören.", + "reputationOpenLnp2pbot": "Von lnp2pBot holen", + "reputationPasteHint": "Füge die erhaltene Reputation ein", + "reputationCheck": "Prüfen", + "reputationImported": "Deine Reputation wurde importiert.", + "reputationImportConfirm": "In diesen Mostro importieren", + "reputationFigures": "{reviews} erhaltene Bewertungen, im Schnitt {rating}, seit {days} Tagen am Handeln", + "reputationFiguresNote": "Importierte Reputation wird zu deiner hiesigen addiert; deine Handelspartner sehen beide zusammen.", + "reputationSettingTitle": "Reputation", + "reputationImportCardBody": "Bring die auf lnp2pBot oder einem anderen Mostro verdiente Reputation hierher.", + "reputationExportTitle": "Meine Reputation exportieren", + "reputationExportCardBody": "Hol dir eine signierte Kopie der auf diesem Mostro verdienten Reputation, um sie auf einem anderen zu importieren.", + "reputationExportConfirmTitle": "An diese Identität exportieren?", + "reputationExportConfirmBody": "Deine Reputation auf diesem Mostro wird für diese Identität exportiert:\n\n{npub}\n\nNach dem Export ist sie an sie gebunden: Ein späterer Export für eine andere Identität erfordert eine mit dieser signierte Autorisierung.", + "reputationExportConfirmButton": "Exportieren", + "reputationExported": "Deine Reputation ist bereit. Wechsle zu dem Mostro, auf dem du sie haben willst, und importiere sie; sie ist 7 Tage gültig.", + "reputationRebindBody": "Wechselst du zu einer neuen Identität? Signiere hier mit der Identität, an die deine Reputation jetzt gebunden ist, eine Autorisierung für die neue und verwende sie dann von der neuen Identität aus.", + "reputationNewIdentityHint": "Neue Identität (npub)", + "reputationRebindSign": "Autorisierung signieren", + "reputationInvalidIdentity": "Das ist kein gültiger npub." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 84c995dbb..62590b706 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -4294,5 +4294,39 @@ "tradeWaitingPaymentSellerInstructionCashu": "Lock your ecash in the escrow at the node's mint. That starts the trade, and it comes back to you if the node disappears.", "@tradeWaitingPaymentSellerInstructionCashu": {"description": "Trade screen instruction for the seller on a Cashu node: lock the escrow"}, "tradeBodyWaitingPaymentBuyerCashu": "They're locking the sats in escrow. Once it's locked, it's your turn to pay the fiat.", - "@tradeBodyWaitingPaymentBuyerCashu": {"description": "Trade screen body for the buyer while the seller locks the Cashu escrow"} + "@tradeBodyWaitingPaymentBuyerCashu": {"description": "Trade screen body for the buyer while the seller locks the Cashu escrow"}, + "reputationNodeDoesNotExport": "This Mostro does not export reputation.", + "reputationNodeDoesNotImport": "This Mostro does not import reputation.", + "reputationIdentityRequired": "Reputation can only be moved from an account that keeps its reputation. Turn off full privacy mode and try again.", + "reputationNotEligible": "Your account cannot export its reputation yet: it needs at least 10 completed trades and 5 ratings received.", + "reputationBoundToOtherIdentity": "Your reputation there is already bound to another identity. Authorize the move from the identity it is bound to.", + "reputationInvalidRebind": "The authorization to move your reputation is invalid or has expired. Create a new one.", + "reputationExpiredAttestation": "This reputation has expired. Request it again from its source.", + "reputationUntrustedIssuer": "This Mostro does not accept reputation from that source.", + "reputationIdentityMismatch": "This reputation was issued for another identity than the one you are using.", + "reputationAlreadyImported": "This reputation was already imported on this Mostro.", + "reputationInvalidAttestation": "This is not a valid reputation. Request it again from its source.", + "reputationImportTitle": "Import reputation", + "reputationImportIntro": "Bring the reputation you earned on lnp2pBot or on another Mostro to this one. Importing links the two accounts: the source and this Mostro learn they belong to the same person.", + "reputationOpenLnp2pbot": "Get it from lnp2pBot", + "reputationPasteHint": "Paste the reputation you received", + "reputationCheck": "Check", + "reputationImported": "Your reputation was imported.", + "reputationImportConfirm": "Import into this Mostro", + "reputationFigures": "{reviews} ratings received, {rating} on average, trading for {days} days", + "@reputationFigures": {"placeholders": {"reviews": {"type": "int"}, "rating": {"type": "String"}, "days": {"type": "int"}}}, + "reputationFiguresNote": "Imported reputation is added to the one you have here; your counterparties see them merged.", + "reputationSettingTitle": "Reputation", + "reputationImportCardBody": "Bring the reputation you earned on lnp2pBot or on another Mostro to this one.", + "reputationExportTitle": "Export my reputation", + "reputationExportCardBody": "Get a signed copy of the reputation you earned on this Mostro, to import it on another one.", + "reputationExportConfirmTitle": "Export to this identity?", + "reputationExportConfirmBody": "Your reputation on this Mostro will be exported for this identity:\n\n{npub}\n\nOnce exported, it is bound to it: exporting it for another identity later needs an authorization signed with this one.", + "@reputationExportConfirmBody": {"placeholders": {"npub": {"type": "String"}}}, + "reputationExportConfirmButton": "Export", + "reputationExported": "Your reputation is ready. Switch to the Mostro where you want it and import it; it is valid for 7 days.", + "reputationRebindBody": "Moving to a new identity? Sign here, with the identity your reputation is bound to now, an authorization for the new one, then use it from the new identity.", + "reputationNewIdentityHint": "New identity (npub)", + "reputationRebindSign": "Sign authorization", + "reputationInvalidIdentity": "That is not a valid npub." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 74b0f6c1b..4dc2b9622 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1256,5 +1256,37 @@ "lockEscrowOrderMovedOn": "El nodo dice que esta orden ya no espera el escrow. Tu escrow sigue guardado en este dispositivo; el estado del trade se actualizará cuando el nodo lo informe.", "tradeHeadlineWaitingPaymentSellerCashu": "Bloquea el escrow para empezar el trade", "tradeWaitingPaymentSellerInstructionCashu": "Bloquea tu ecash en el escrow de la mint del nodo. Eso inicia el trade, y vuelve a ti si el nodo desaparece.", - "tradeBodyWaitingPaymentBuyerCashu": "Están bloqueando los sats en el escrow. Cuando esté bloqueado, te toca pagar el fiat." + "tradeBodyWaitingPaymentBuyerCashu": "Están bloqueando los sats en el escrow. Cuando esté bloqueado, te toca pagar el fiat.", + "reputationNodeDoesNotExport": "Este Mostro no exporta reputación.", + "reputationNodeDoesNotImport": "Este Mostro no importa reputación.", + "reputationIdentityRequired": "La reputación solo se puede mover desde una cuenta que la conserva. Desactiva el modo de privacidad total e inténtalo de nuevo.", + "reputationNotEligible": "Tu cuenta todavía no puede exportar su reputación: necesita al menos 10 operaciones completadas y 5 calificaciones recibidas.", + "reputationBoundToOtherIdentity": "Tu reputación allí ya está ligada a otra identidad. Autoriza el cambio desde la identidad a la que está ligada.", + "reputationInvalidRebind": "La autorización para mover tu reputación no es válida o expiró. Crea una nueva.", + "reputationExpiredAttestation": "Esta reputación expiró. Pídela de nuevo a su origen.", + "reputationUntrustedIssuer": "Este Mostro no acepta reputación de ese origen.", + "reputationIdentityMismatch": "Esta reputación se emitió para otra identidad distinta de la que estás usando.", + "reputationAlreadyImported": "Esta reputación ya se importó en este Mostro.", + "reputationInvalidAttestation": "Esta no es una reputación válida. Pídela de nuevo a su origen.", + "reputationImportTitle": "Importar reputación", + "reputationImportIntro": "Trae a este Mostro la reputación que ganaste en lnp2pBot o en otro Mostro. Importar vincula las dos cuentas: el origen y este Mostro saben que son de la misma persona.", + "reputationOpenLnp2pbot": "Obtenerla de lnp2pBot", + "reputationPasteHint": "Pega la reputación que recibiste", + "reputationCheck": "Comprobar", + "reputationImported": "Tu reputación se importó.", + "reputationImportConfirm": "Importar en este Mostro", + "reputationFigures": "{reviews} calificaciones recibidas, {rating} de promedio, operando desde hace {days} días", + "reputationFiguresNote": "La reputación importada se suma a la que tienes aquí; tus contrapartes las ven juntas.", + "reputationSettingTitle": "Reputación", + "reputationImportCardBody": "Trae a este Mostro la reputación que ganaste en lnp2pBot o en otro Mostro.", + "reputationExportTitle": "Exportar mi reputación", + "reputationExportCardBody": "Obtén una copia firmada de la reputación que ganaste en este Mostro para importarla en otro.", + "reputationExportConfirmTitle": "¿Exportar a esta identidad?", + "reputationExportConfirmBody": "Tu reputación en este Mostro se exportará para esta identidad:\n\n{npub}\n\nUna vez exportada queda ligada a ella: exportarla después para otra identidad requiere una autorización firmada con esta.", + "reputationExportConfirmButton": "Exportar", + "reputationExported": "Tu reputación está lista. Cambia al Mostro donde la quieres e impórtala; es válida durante 7 días.", + "reputationRebindBody": "¿Te mudas a una identidad nueva? Firma aquí, con la identidad a la que está ligada tu reputación ahora, una autorización para la nueva y luego úsala desde la nueva identidad.", + "reputationNewIdentityHint": "Identidad nueva (npub)", + "reputationRebindSign": "Firmar autorización", + "reputationInvalidIdentity": "Ese no es un npub válido." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 02da9a7c1..f03332654 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1256,5 +1256,37 @@ "lockEscrowOrderMovedOn": "Le nœud indique que cet ordre n'attend plus l'escrow. Votre escrow reste enregistré sur cet appareil ; le statut du trade se mettra à jour quand le nœud le signalera.", "tradeHeadlineWaitingPaymentSellerCashu": "Verrouillez l'escrow pour démarrer le trade", "tradeWaitingPaymentSellerInstructionCashu": "Verrouillez votre ecash dans l'escrow au mint du nœud. Cela démarre le trade, et il vous revient si le nœud disparaît.", - "tradeBodyWaitingPaymentBuyerCashu": "Ils verrouillent les sats dans l'escrow. Une fois verrouillé, c'est à vous de payer le fiat." + "tradeBodyWaitingPaymentBuyerCashu": "Ils verrouillent les sats dans l'escrow. Une fois verrouillé, c'est à vous de payer le fiat.", + "reputationNodeDoesNotExport": "Ce Mostro n'exporte pas la réputation.", + "reputationNodeDoesNotImport": "Ce Mostro n'importe pas la réputation.", + "reputationIdentityRequired": "La réputation ne peut être transférée que depuis un compte qui la conserve. Désactivez le mode confidentialité totale et réessayez.", + "reputationNotEligible": "Votre compte ne peut pas encore exporter sa réputation : il lui faut au moins 10 échanges terminés et 5 évaluations reçues.", + "reputationBoundToOtherIdentity": "Votre réputation là-bas est déjà liée à une autre identité. Autorisez le transfert depuis l'identité à laquelle elle est liée.", + "reputationInvalidRebind": "L'autorisation de transférer votre réputation n'est pas valide ou a expiré. Créez-en une nouvelle.", + "reputationExpiredAttestation": "Cette réputation a expiré. Redemandez-la à sa source.", + "reputationUntrustedIssuer": "Ce Mostro n'accepte pas la réputation de cette source.", + "reputationIdentityMismatch": "Cette réputation a été émise pour une autre identité que celle que vous utilisez.", + "reputationAlreadyImported": "Cette réputation a déjà été importée sur ce Mostro.", + "reputationInvalidAttestation": "Ce n'est pas une réputation valide. Redemandez-la à sa source.", + "reputationImportTitle": "Importer la réputation", + "reputationImportIntro": "Apportez sur ce Mostro la réputation gagnée sur lnp2pBot ou sur un autre Mostro. L'import relie les deux comptes : la source et ce Mostro savent qu'ils appartiennent à la même personne.", + "reputationOpenLnp2pbot": "L'obtenir de lnp2pBot", + "reputationPasteHint": "Collez la réputation reçue", + "reputationCheck": "Vérifier", + "reputationImported": "Votre réputation a été importée.", + "reputationImportConfirm": "Importer sur ce Mostro", + "reputationFigures": "{reviews} évaluations reçues, {rating} en moyenne, échange depuis {days} jours", + "reputationFiguresNote": "La réputation importée s'ajoute à celle que vous avez ici ; vos contreparties les voient fusionnées.", + "reputationSettingTitle": "Réputation", + "reputationImportCardBody": "Apportez sur ce Mostro la réputation gagnée sur lnp2pBot ou sur un autre Mostro.", + "reputationExportTitle": "Exporter ma réputation", + "reputationExportCardBody": "Obtenez une copie signée de la réputation gagnée sur ce Mostro pour l'importer sur un autre.", + "reputationExportConfirmTitle": "Exporter vers cette identité ?", + "reputationExportConfirmBody": "Votre réputation sur ce Mostro sera exportée pour cette identité :\n\n{npub}\n\nUne fois exportée, elle y est liée : l'exporter plus tard pour une autre identité exige une autorisation signée avec celle-ci.", + "reputationExportConfirmButton": "Exporter", + "reputationExported": "Votre réputation est prête. Passez au Mostro où vous la voulez et importez-la ; elle est valable 7 jours.", + "reputationRebindBody": "Vous changez d'identité ? Signez ici, avec l'identité à laquelle votre réputation est liée maintenant, une autorisation pour la nouvelle, puis utilisez-la depuis la nouvelle identité.", + "reputationNewIdentityHint": "Nouvelle identité (npub)", + "reputationRebindSign": "Signer l'autorisation", + "reputationInvalidIdentity": "Ce n'est pas un npub valide." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index f17c3316c..bedb6c8fd 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1256,5 +1256,37 @@ "lockEscrowOrderMovedOn": "Il nodo dice che questo ordine non attende più l'escrow. Il tuo escrow resta salvato su questo dispositivo; lo stato del trade si aggiornerà quando il nodo lo comunicherà.", "tradeHeadlineWaitingPaymentSellerCashu": "Blocca l'escrow per avviare il trade", "tradeWaitingPaymentSellerInstructionCashu": "Blocca il tuo ecash nell'escrow presso il mint del nodo. Questo avvia il trade, e torna a te se il nodo scompare.", - "tradeBodyWaitingPaymentBuyerCashu": "Stanno bloccando i sats nell'escrow. Una volta bloccato, tocca a te pagare il fiat." + "tradeBodyWaitingPaymentBuyerCashu": "Stanno bloccando i sats nell'escrow. Una volta bloccato, tocca a te pagare il fiat.", + "reputationNodeDoesNotExport": "Questo Mostro non esporta la reputazione.", + "reputationNodeDoesNotImport": "Questo Mostro non importa la reputazione.", + "reputationIdentityRequired": "La reputazione si può spostare solo da un account che la conserva. Disattiva la modalità di privacy totale e riprova.", + "reputationNotEligible": "Il tuo account non può ancora esportare la reputazione: servono almeno 10 scambi completati e 5 valutazioni ricevute.", + "reputationBoundToOtherIdentity": "La tua reputazione lì è già legata a un'altra identità. Autorizza lo spostamento dall'identità a cui è legata.", + "reputationInvalidRebind": "L'autorizzazione a spostare la tua reputazione non è valida o è scaduta. Creane una nuova.", + "reputationExpiredAttestation": "Questa reputazione è scaduta. Richiedila di nuovo alla sua origine.", + "reputationUntrustedIssuer": "Questo Mostro non accetta reputazione da quell'origine.", + "reputationIdentityMismatch": "Questa reputazione è stata emessa per un'identità diversa da quella che stai usando.", + "reputationAlreadyImported": "Questa reputazione è già stata importata su questo Mostro.", + "reputationInvalidAttestation": "Questa non è una reputazione valida. Richiedila di nuovo alla sua origine.", + "reputationImportTitle": "Importa reputazione", + "reputationImportIntro": "Porta su questo Mostro la reputazione guadagnata su lnp2pBot o su un altro Mostro. L'importazione collega i due account: l'origine e questo Mostro sanno che appartengono alla stessa persona.", + "reputationOpenLnp2pbot": "Ottienila da lnp2pBot", + "reputationPasteHint": "Incolla la reputazione ricevuta", + "reputationCheck": "Verifica", + "reputationImported": "La tua reputazione è stata importata.", + "reputationImportConfirm": "Importa su questo Mostro", + "reputationFigures": "{reviews} valutazioni ricevute, {rating} di media, scambia da {days} giorni", + "reputationFiguresNote": "La reputazione importata si somma a quella che hai qui; le tue controparti le vedono unite.", + "reputationSettingTitle": "Reputazione", + "reputationImportCardBody": "Porta su questo Mostro la reputazione guadagnata su lnp2pBot o su un altro Mostro.", + "reputationExportTitle": "Esporta la mia reputazione", + "reputationExportCardBody": "Ottieni una copia firmata della reputazione guadagnata su questo Mostro per importarla su un altro.", + "reputationExportConfirmTitle": "Esportare verso questa identità?", + "reputationExportConfirmBody": "La tua reputazione su questo Mostro verrà esportata per questa identità:\n\n{npub}\n\nUna volta esportata resta legata a essa: esportarla poi per un'altra identità richiede un'autorizzazione firmata con questa.", + "reputationExportConfirmButton": "Esporta", + "reputationExported": "La tua reputazione è pronta. Passa al Mostro dove la vuoi e importala; è valida per 7 giorni.", + "reputationRebindBody": "Passi a una nuova identità? Firma qui, con l'identità a cui è legata ora la tua reputazione, un'autorizzazione per la nuova, poi usala dalla nuova identità.", + "reputationNewIdentityHint": "Nuova identità (npub)", + "reputationRebindSign": "Firma autorizzazione", + "reputationInvalidIdentity": "Questo non è un npub valido." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 6cb70b4a5..1fad7a1c5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -6855,6 +6855,198 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'They\'re locking the sats in escrow. Once it\'s locked, it\'s your turn to pay the fiat.'** String get tradeBodyWaitingPaymentBuyerCashu; + + /// No description provided for @reputationNodeDoesNotExport. + /// + /// In en, this message translates to: + /// **'This Mostro does not export reputation.'** + String get reputationNodeDoesNotExport; + + /// No description provided for @reputationNodeDoesNotImport. + /// + /// In en, this message translates to: + /// **'This Mostro does not import reputation.'** + String get reputationNodeDoesNotImport; + + /// No description provided for @reputationIdentityRequired. + /// + /// In en, this message translates to: + /// **'Reputation can only be moved from an account that keeps its reputation. Turn off full privacy mode and try again.'** + String get reputationIdentityRequired; + + /// No description provided for @reputationNotEligible. + /// + /// In en, this message translates to: + /// **'Your account cannot export its reputation yet: it needs at least 10 completed trades and 5 ratings received.'** + String get reputationNotEligible; + + /// No description provided for @reputationBoundToOtherIdentity. + /// + /// In en, this message translates to: + /// **'Your reputation there is already bound to another identity. Authorize the move from the identity it is bound to.'** + String get reputationBoundToOtherIdentity; + + /// No description provided for @reputationInvalidRebind. + /// + /// In en, this message translates to: + /// **'The authorization to move your reputation is invalid or has expired. Create a new one.'** + String get reputationInvalidRebind; + + /// No description provided for @reputationExpiredAttestation. + /// + /// In en, this message translates to: + /// **'This reputation has expired. Request it again from its source.'** + String get reputationExpiredAttestation; + + /// No description provided for @reputationUntrustedIssuer. + /// + /// In en, this message translates to: + /// **'This Mostro does not accept reputation from that source.'** + String get reputationUntrustedIssuer; + + /// No description provided for @reputationIdentityMismatch. + /// + /// In en, this message translates to: + /// **'This reputation was issued for another identity than the one you are using.'** + String get reputationIdentityMismatch; + + /// No description provided for @reputationAlreadyImported. + /// + /// In en, this message translates to: + /// **'This reputation was already imported on this Mostro.'** + String get reputationAlreadyImported; + + /// No description provided for @reputationInvalidAttestation. + /// + /// In en, this message translates to: + /// **'This is not a valid reputation. Request it again from its source.'** + String get reputationInvalidAttestation; + + /// No description provided for @reputationImportTitle. + /// + /// In en, this message translates to: + /// **'Import reputation'** + String get reputationImportTitle; + + /// No description provided for @reputationImportIntro. + /// + /// In en, this message translates to: + /// **'Bring the reputation you earned on lnp2pBot or on another Mostro to this one. Importing links the two accounts: the source and this Mostro learn they belong to the same person.'** + String get reputationImportIntro; + + /// No description provided for @reputationOpenLnp2pbot. + /// + /// In en, this message translates to: + /// **'Get it from lnp2pBot'** + String get reputationOpenLnp2pbot; + + /// No description provided for @reputationPasteHint. + /// + /// In en, this message translates to: + /// **'Paste the reputation you received'** + String get reputationPasteHint; + + /// No description provided for @reputationCheck. + /// + /// In en, this message translates to: + /// **'Check'** + String get reputationCheck; + + /// No description provided for @reputationImported. + /// + /// In en, this message translates to: + /// **'Your reputation was imported.'** + String get reputationImported; + + /// No description provided for @reputationImportConfirm. + /// + /// In en, this message translates to: + /// **'Import into this Mostro'** + String get reputationImportConfirm; + + /// No description provided for @reputationFigures. + /// + /// In en, this message translates to: + /// **'{reviews} ratings received, {rating} on average, trading for {days} days'** + String reputationFigures(int reviews, String rating, int days); + + /// No description provided for @reputationFiguresNote. + /// + /// In en, this message translates to: + /// **'Imported reputation is added to the one you have here; your counterparties see them merged.'** + String get reputationFiguresNote; + + /// No description provided for @reputationSettingTitle. + /// + /// In en, this message translates to: + /// **'Reputation'** + String get reputationSettingTitle; + + /// No description provided for @reputationImportCardBody. + /// + /// In en, this message translates to: + /// **'Bring the reputation you earned on lnp2pBot or on another Mostro to this one.'** + String get reputationImportCardBody; + + /// No description provided for @reputationExportTitle. + /// + /// In en, this message translates to: + /// **'Export my reputation'** + String get reputationExportTitle; + + /// No description provided for @reputationExportCardBody. + /// + /// In en, this message translates to: + /// **'Get a signed copy of the reputation you earned on this Mostro, to import it on another one.'** + String get reputationExportCardBody; + + /// No description provided for @reputationExportConfirmTitle. + /// + /// In en, this message translates to: + /// **'Export to this identity?'** + String get reputationExportConfirmTitle; + + /// No description provided for @reputationExportConfirmBody. + /// + /// In en, this message translates to: + /// **'Your reputation on this Mostro will be exported for this identity:\n\n{npub}\n\nOnce exported, it is bound to it: exporting it for another identity later needs an authorization signed with this one.'** + String reputationExportConfirmBody(String npub); + + /// No description provided for @reputationExportConfirmButton. + /// + /// In en, this message translates to: + /// **'Export'** + String get reputationExportConfirmButton; + + /// No description provided for @reputationExported. + /// + /// In en, this message translates to: + /// **'Your reputation is ready. Switch to the Mostro where you want it and import it; it is valid for 7 days.'** + String get reputationExported; + + /// No description provided for @reputationRebindBody. + /// + /// In en, this message translates to: + /// **'Moving to a new identity? Sign here, with the identity your reputation is bound to now, an authorization for the new one, then use it from the new identity.'** + String get reputationRebindBody; + + /// No description provided for @reputationNewIdentityHint. + /// + /// In en, this message translates to: + /// **'New identity (npub)'** + String get reputationNewIdentityHint; + + /// No description provided for @reputationRebindSign. + /// + /// In en, this message translates to: + /// **'Sign authorization'** + String get reputationRebindSign; + + /// No description provided for @reputationInvalidIdentity. + /// + /// In en, this message translates to: + /// **'That is not a valid npub.'** + String get reputationInvalidIdentity; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ab8c0fa32..25f685c2b 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -4151,4 +4151,121 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'Die Gegenseite sperrt die Sats im Escrow. Sobald er gesperrt ist, bist du dran, den Fiat-Betrag zu zahlen.'; + + @override + String get reputationNodeDoesNotExport => + 'Dieser Mostro exportiert keine Reputation.'; + + @override + String get reputationNodeDoesNotImport => + 'Dieser Mostro importiert keine Reputation.'; + + @override + String get reputationIdentityRequired => + 'Reputation kann nur von einem Konto übertragen werden, das seine Reputation behält. Schalte den vollständigen Privatsphäre-Modus aus und versuche es erneut.'; + + @override + String get reputationNotEligible => + 'Dein Konto kann seine Reputation noch nicht exportieren: Es braucht mindestens 10 abgeschlossene Trades und 5 erhaltene Bewertungen.'; + + @override + String get reputationBoundToOtherIdentity => + 'Deine Reputation dort ist bereits an eine andere Identität gebunden. Autorisiere den Wechsel mit der Identität, an die sie gebunden ist.'; + + @override + String get reputationInvalidRebind => + 'Die Autorisierung zum Übertragen deiner Reputation ist ungültig oder abgelaufen. Erstelle eine neue.'; + + @override + String get reputationExpiredAttestation => + 'Diese Reputation ist abgelaufen. Fordere sie erneut bei ihrer Quelle an.'; + + @override + String get reputationUntrustedIssuer => + 'Dieser Mostro akzeptiert keine Reputation aus dieser Quelle.'; + + @override + String get reputationIdentityMismatch => + 'Diese Reputation wurde für eine andere Identität ausgestellt als die, die du verwendest.'; + + @override + String get reputationAlreadyImported => + 'Diese Reputation wurde auf diesem Mostro bereits importiert.'; + + @override + String get reputationInvalidAttestation => + 'Das ist keine gültige Reputation. Fordere sie erneut bei ihrer Quelle an.'; + + @override + String get reputationImportTitle => 'Reputation importieren'; + + @override + String get reputationImportIntro => + 'Bring die Reputation, die du auf lnp2pBot oder einem anderen Mostro verdient hast, auf diesen. Der Import verknüpft die beiden Konten: Die Quelle und dieser Mostro wissen, dass sie derselben Person gehören.'; + + @override + String get reputationOpenLnp2pbot => 'Von lnp2pBot holen'; + + @override + String get reputationPasteHint => 'Füge die erhaltene Reputation ein'; + + @override + String get reputationCheck => 'Prüfen'; + + @override + String get reputationImported => 'Deine Reputation wurde importiert.'; + + @override + String get reputationImportConfirm => 'In diesen Mostro importieren'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews erhaltene Bewertungen, im Schnitt $rating, seit $days Tagen am Handeln'; + } + + @override + String get reputationFiguresNote => + 'Importierte Reputation wird zu deiner hiesigen addiert; deine Handelspartner sehen beide zusammen.'; + + @override + String get reputationSettingTitle => 'Reputation'; + + @override + String get reputationImportCardBody => + 'Bring die auf lnp2pBot oder einem anderen Mostro verdiente Reputation hierher.'; + + @override + String get reputationExportTitle => 'Meine Reputation exportieren'; + + @override + String get reputationExportCardBody => + 'Hol dir eine signierte Kopie der auf diesem Mostro verdienten Reputation, um sie auf einem anderen zu importieren.'; + + @override + String get reputationExportConfirmTitle => 'An diese Identität exportieren?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'Deine Reputation auf diesem Mostro wird für diese Identität exportiert:\n\n$npub\n\nNach dem Export ist sie an sie gebunden: Ein späterer Export für eine andere Identität erfordert eine mit dieser signierte Autorisierung.'; + } + + @override + String get reputationExportConfirmButton => 'Exportieren'; + + @override + String get reputationExported => + 'Deine Reputation ist bereit. Wechsle zu dem Mostro, auf dem du sie haben willst, und importiere sie; sie ist 7 Tage gültig.'; + + @override + String get reputationRebindBody => + 'Wechselst du zu einer neuen Identität? Signiere hier mit der Identität, an die deine Reputation jetzt gebunden ist, eine Autorisierung für die neue und verwende sie dann von der neuen Identität aus.'; + + @override + String get reputationNewIdentityHint => 'Neue Identität (npub)'; + + @override + String get reputationRebindSign => 'Autorisierung signieren'; + + @override + String get reputationInvalidIdentity => 'Das ist kein gültiger npub.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index ae3d3533f..f244c57b3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -4098,4 +4098,121 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'They\'re locking the sats in escrow. Once it\'s locked, it\'s your turn to pay the fiat.'; + + @override + String get reputationNodeDoesNotExport => + 'This Mostro does not export reputation.'; + + @override + String get reputationNodeDoesNotImport => + 'This Mostro does not import reputation.'; + + @override + String get reputationIdentityRequired => + 'Reputation can only be moved from an account that keeps its reputation. Turn off full privacy mode and try again.'; + + @override + String get reputationNotEligible => + 'Your account cannot export its reputation yet: it needs at least 10 completed trades and 5 ratings received.'; + + @override + String get reputationBoundToOtherIdentity => + 'Your reputation there is already bound to another identity. Authorize the move from the identity it is bound to.'; + + @override + String get reputationInvalidRebind => + 'The authorization to move your reputation is invalid or has expired. Create a new one.'; + + @override + String get reputationExpiredAttestation => + 'This reputation has expired. Request it again from its source.'; + + @override + String get reputationUntrustedIssuer => + 'This Mostro does not accept reputation from that source.'; + + @override + String get reputationIdentityMismatch => + 'This reputation was issued for another identity than the one you are using.'; + + @override + String get reputationAlreadyImported => + 'This reputation was already imported on this Mostro.'; + + @override + String get reputationInvalidAttestation => + 'This is not a valid reputation. Request it again from its source.'; + + @override + String get reputationImportTitle => 'Import reputation'; + + @override + String get reputationImportIntro => + 'Bring the reputation you earned on lnp2pBot or on another Mostro to this one. Importing links the two accounts: the source and this Mostro learn they belong to the same person.'; + + @override + String get reputationOpenLnp2pbot => 'Get it from lnp2pBot'; + + @override + String get reputationPasteHint => 'Paste the reputation you received'; + + @override + String get reputationCheck => 'Check'; + + @override + String get reputationImported => 'Your reputation was imported.'; + + @override + String get reputationImportConfirm => 'Import into this Mostro'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews ratings received, $rating on average, trading for $days days'; + } + + @override + String get reputationFiguresNote => + 'Imported reputation is added to the one you have here; your counterparties see them merged.'; + + @override + String get reputationSettingTitle => 'Reputation'; + + @override + String get reputationImportCardBody => + 'Bring the reputation you earned on lnp2pBot or on another Mostro to this one.'; + + @override + String get reputationExportTitle => 'Export my reputation'; + + @override + String get reputationExportCardBody => + 'Get a signed copy of the reputation you earned on this Mostro, to import it on another one.'; + + @override + String get reputationExportConfirmTitle => 'Export to this identity?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'Your reputation on this Mostro will be exported for this identity:\n\n$npub\n\nOnce exported, it is bound to it: exporting it for another identity later needs an authorization signed with this one.'; + } + + @override + String get reputationExportConfirmButton => 'Export'; + + @override + String get reputationExported => + 'Your reputation is ready. Switch to the Mostro where you want it and import it; it is valid for 7 days.'; + + @override + String get reputationRebindBody => + 'Moving to a new identity? Sign here, with the identity your reputation is bound to now, an authorization for the new one, then use it from the new identity.'; + + @override + String get reputationNewIdentityHint => 'New identity (npub)'; + + @override + String get reputationRebindSign => 'Sign authorization'; + + @override + String get reputationInvalidIdentity => 'That is not a valid npub.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 7b740a63f..74b159186 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -4126,4 +4126,121 @@ class AppLocalizationsEs extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'Están bloqueando los sats en el escrow. Cuando esté bloqueado, te toca pagar el fiat.'; + + @override + String get reputationNodeDoesNotExport => + 'Este Mostro no exporta reputación.'; + + @override + String get reputationNodeDoesNotImport => + 'Este Mostro no importa reputación.'; + + @override + String get reputationIdentityRequired => + 'La reputación solo se puede mover desde una cuenta que la conserva. Desactiva el modo de privacidad total e inténtalo de nuevo.'; + + @override + String get reputationNotEligible => + 'Tu cuenta todavía no puede exportar su reputación: necesita al menos 10 operaciones completadas y 5 calificaciones recibidas.'; + + @override + String get reputationBoundToOtherIdentity => + 'Tu reputación allí ya está ligada a otra identidad. Autoriza el cambio desde la identidad a la que está ligada.'; + + @override + String get reputationInvalidRebind => + 'La autorización para mover tu reputación no es válida o expiró. Crea una nueva.'; + + @override + String get reputationExpiredAttestation => + 'Esta reputación expiró. Pídela de nuevo a su origen.'; + + @override + String get reputationUntrustedIssuer => + 'Este Mostro no acepta reputación de ese origen.'; + + @override + String get reputationIdentityMismatch => + 'Esta reputación se emitió para otra identidad distinta de la que estás usando.'; + + @override + String get reputationAlreadyImported => + 'Esta reputación ya se importó en este Mostro.'; + + @override + String get reputationInvalidAttestation => + 'Esta no es una reputación válida. Pídela de nuevo a su origen.'; + + @override + String get reputationImportTitle => 'Importar reputación'; + + @override + String get reputationImportIntro => + 'Trae a este Mostro la reputación que ganaste en lnp2pBot o en otro Mostro. Importar vincula las dos cuentas: el origen y este Mostro saben que son de la misma persona.'; + + @override + String get reputationOpenLnp2pbot => 'Obtenerla de lnp2pBot'; + + @override + String get reputationPasteHint => 'Pega la reputación que recibiste'; + + @override + String get reputationCheck => 'Comprobar'; + + @override + String get reputationImported => 'Tu reputación se importó.'; + + @override + String get reputationImportConfirm => 'Importar en este Mostro'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews calificaciones recibidas, $rating de promedio, operando desde hace $days días'; + } + + @override + String get reputationFiguresNote => + 'La reputación importada se suma a la que tienes aquí; tus contrapartes las ven juntas.'; + + @override + String get reputationSettingTitle => 'Reputación'; + + @override + String get reputationImportCardBody => + 'Trae a este Mostro la reputación que ganaste en lnp2pBot o en otro Mostro.'; + + @override + String get reputationExportTitle => 'Exportar mi reputación'; + + @override + String get reputationExportCardBody => + 'Obtén una copia firmada de la reputación que ganaste en este Mostro para importarla en otro.'; + + @override + String get reputationExportConfirmTitle => '¿Exportar a esta identidad?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'Tu reputación en este Mostro se exportará para esta identidad:\n\n$npub\n\nUna vez exportada queda ligada a ella: exportarla después para otra identidad requiere una autorización firmada con esta.'; + } + + @override + String get reputationExportConfirmButton => 'Exportar'; + + @override + String get reputationExported => + 'Tu reputación está lista. Cambia al Mostro donde la quieres e impórtala; es válida durante 7 días.'; + + @override + String get reputationRebindBody => + '¿Te mudas a una identidad nueva? Firma aquí, con la identidad a la que está ligada tu reputación ahora, una autorización para la nueva y luego úsala desde la nueva identidad.'; + + @override + String get reputationNewIdentityHint => 'Identidad nueva (npub)'; + + @override + String get reputationRebindSign => 'Firmar autorización'; + + @override + String get reputationInvalidIdentity => 'Ese no es un npub válido.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 7ba7d5d19..b7c2f2cc9 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -4161,4 +4161,121 @@ class AppLocalizationsFr extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'Ils verrouillent les sats dans l\'escrow. Une fois verrouillé, c\'est à vous de payer le fiat.'; + + @override + String get reputationNodeDoesNotExport => + 'Ce Mostro n\'exporte pas la réputation.'; + + @override + String get reputationNodeDoesNotImport => + 'Ce Mostro n\'importe pas la réputation.'; + + @override + String get reputationIdentityRequired => + 'La réputation ne peut être transférée que depuis un compte qui la conserve. Désactivez le mode confidentialité totale et réessayez.'; + + @override + String get reputationNotEligible => + 'Votre compte ne peut pas encore exporter sa réputation : il lui faut au moins 10 échanges terminés et 5 évaluations reçues.'; + + @override + String get reputationBoundToOtherIdentity => + 'Votre réputation là-bas est déjà liée à une autre identité. Autorisez le transfert depuis l\'identité à laquelle elle est liée.'; + + @override + String get reputationInvalidRebind => + 'L\'autorisation de transférer votre réputation n\'est pas valide ou a expiré. Créez-en une nouvelle.'; + + @override + String get reputationExpiredAttestation => + 'Cette réputation a expiré. Redemandez-la à sa source.'; + + @override + String get reputationUntrustedIssuer => + 'Ce Mostro n\'accepte pas la réputation de cette source.'; + + @override + String get reputationIdentityMismatch => + 'Cette réputation a été émise pour une autre identité que celle que vous utilisez.'; + + @override + String get reputationAlreadyImported => + 'Cette réputation a déjà été importée sur ce Mostro.'; + + @override + String get reputationInvalidAttestation => + 'Ce n\'est pas une réputation valide. Redemandez-la à sa source.'; + + @override + String get reputationImportTitle => 'Importer la réputation'; + + @override + String get reputationImportIntro => + 'Apportez sur ce Mostro la réputation gagnée sur lnp2pBot ou sur un autre Mostro. L\'import relie les deux comptes : la source et ce Mostro savent qu\'ils appartiennent à la même personne.'; + + @override + String get reputationOpenLnp2pbot => 'L\'obtenir de lnp2pBot'; + + @override + String get reputationPasteHint => 'Collez la réputation reçue'; + + @override + String get reputationCheck => 'Vérifier'; + + @override + String get reputationImported => 'Votre réputation a été importée.'; + + @override + String get reputationImportConfirm => 'Importer sur ce Mostro'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews évaluations reçues, $rating en moyenne, échange depuis $days jours'; + } + + @override + String get reputationFiguresNote => + 'La réputation importée s\'ajoute à celle que vous avez ici ; vos contreparties les voient fusionnées.'; + + @override + String get reputationSettingTitle => 'Réputation'; + + @override + String get reputationImportCardBody => + 'Apportez sur ce Mostro la réputation gagnée sur lnp2pBot ou sur un autre Mostro.'; + + @override + String get reputationExportTitle => 'Exporter ma réputation'; + + @override + String get reputationExportCardBody => + 'Obtenez une copie signée de la réputation gagnée sur ce Mostro pour l\'importer sur un autre.'; + + @override + String get reputationExportConfirmTitle => 'Exporter vers cette identité ?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'Votre réputation sur ce Mostro sera exportée pour cette identité :\n\n$npub\n\nUne fois exportée, elle y est liée : l\'exporter plus tard pour une autre identité exige une autorisation signée avec celle-ci.'; + } + + @override + String get reputationExportConfirmButton => 'Exporter'; + + @override + String get reputationExported => + 'Votre réputation est prête. Passez au Mostro où vous la voulez et importez-la ; elle est valable 7 jours.'; + + @override + String get reputationRebindBody => + 'Vous changez d\'identité ? Signez ici, avec l\'identité à laquelle votre réputation est liée maintenant, une autorisation pour la nouvelle, puis utilisez-la depuis la nouvelle identité.'; + + @override + String get reputationNewIdentityHint => 'Nouvelle identité (npub)'; + + @override + String get reputationRebindSign => 'Signer l\'autorisation'; + + @override + String get reputationInvalidIdentity => 'Ce n\'est pas un npub valide.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 5efee0be9..957bf15e2 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -4139,4 +4139,121 @@ class AppLocalizationsIt extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'Stanno bloccando i sats nell\'escrow. Una volta bloccato, tocca a te pagare il fiat.'; + + @override + String get reputationNodeDoesNotExport => + 'Questo Mostro non esporta la reputazione.'; + + @override + String get reputationNodeDoesNotImport => + 'Questo Mostro non importa la reputazione.'; + + @override + String get reputationIdentityRequired => + 'La reputazione si può spostare solo da un account che la conserva. Disattiva la modalità di privacy totale e riprova.'; + + @override + String get reputationNotEligible => + 'Il tuo account non può ancora esportare la reputazione: servono almeno 10 scambi completati e 5 valutazioni ricevute.'; + + @override + String get reputationBoundToOtherIdentity => + 'La tua reputazione lì è già legata a un\'altra identità. Autorizza lo spostamento dall\'identità a cui è legata.'; + + @override + String get reputationInvalidRebind => + 'L\'autorizzazione a spostare la tua reputazione non è valida o è scaduta. Creane una nuova.'; + + @override + String get reputationExpiredAttestation => + 'Questa reputazione è scaduta. Richiedila di nuovo alla sua origine.'; + + @override + String get reputationUntrustedIssuer => + 'Questo Mostro non accetta reputazione da quell\'origine.'; + + @override + String get reputationIdentityMismatch => + 'Questa reputazione è stata emessa per un\'identità diversa da quella che stai usando.'; + + @override + String get reputationAlreadyImported => + 'Questa reputazione è già stata importata su questo Mostro.'; + + @override + String get reputationInvalidAttestation => + 'Questa non è una reputazione valida. Richiedila di nuovo alla sua origine.'; + + @override + String get reputationImportTitle => 'Importa reputazione'; + + @override + String get reputationImportIntro => + 'Porta su questo Mostro la reputazione guadagnata su lnp2pBot o su un altro Mostro. L\'importazione collega i due account: l\'origine e questo Mostro sanno che appartengono alla stessa persona.'; + + @override + String get reputationOpenLnp2pbot => 'Ottienila da lnp2pBot'; + + @override + String get reputationPasteHint => 'Incolla la reputazione ricevuta'; + + @override + String get reputationCheck => 'Verifica'; + + @override + String get reputationImported => 'La tua reputazione è stata importata.'; + + @override + String get reputationImportConfirm => 'Importa su questo Mostro'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews valutazioni ricevute, $rating di media, scambia da $days giorni'; + } + + @override + String get reputationFiguresNote => + 'La reputazione importata si somma a quella che hai qui; le tue controparti le vedono unite.'; + + @override + String get reputationSettingTitle => 'Reputazione'; + + @override + String get reputationImportCardBody => + 'Porta su questo Mostro la reputazione guadagnata su lnp2pBot o su un altro Mostro.'; + + @override + String get reputationExportTitle => 'Esporta la mia reputazione'; + + @override + String get reputationExportCardBody => + 'Ottieni una copia firmata della reputazione guadagnata su questo Mostro per importarla su un altro.'; + + @override + String get reputationExportConfirmTitle => 'Esportare verso questa identità?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'La tua reputazione su questo Mostro verrà esportata per questa identità:\n\n$npub\n\nUna volta esportata resta legata a essa: esportarla poi per un\'altra identità richiede un\'autorizzazione firmata con questa.'; + } + + @override + String get reputationExportConfirmButton => 'Esporta'; + + @override + String get reputationExported => + 'La tua reputazione è pronta. Passa al Mostro dove la vuoi e importala; è valida per 7 giorni.'; + + @override + String get reputationRebindBody => + 'Passi a una nuova identità? Firma qui, con l\'identità a cui è legata ora la tua reputazione, un\'autorizzazione per la nuova, poi usala dalla nuova identità.'; + + @override + String get reputationNewIdentityHint => 'Nuova identità (npub)'; + + @override + String get reputationRebindSign => 'Firma autorizzazione'; + + @override + String get reputationInvalidIdentity => 'Questo non è un npub valido.'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d453cc011..d375b4621 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -4132,4 +4132,121 @@ class AppLocalizationsNl extends AppLocalizations { @override String get tradeBodyWaitingPaymentBuyerCashu => 'Ze vergrendelen de sats in de escrow. Zodra die vergrendeld is, ben jij aan de beurt om de fiat te betalen.'; + + @override + String get reputationNodeDoesNotExport => + 'Deze Mostro exporteert geen reputatie.'; + + @override + String get reputationNodeDoesNotImport => + 'Deze Mostro importeert geen reputatie.'; + + @override + String get reputationIdentityRequired => + 'Reputatie kan alleen worden verplaatst vanaf een account dat zijn reputatie bewaart. Zet de volledige privacymodus uit en probeer het opnieuw.'; + + @override + String get reputationNotEligible => + 'Je account kan zijn reputatie nog niet exporteren: er zijn minstens 10 voltooide transacties en 5 ontvangen beoordelingen nodig.'; + + @override + String get reputationBoundToOtherIdentity => + 'Je reputatie daar is al gekoppeld aan een andere identiteit. Geef toestemming voor de verplaatsing vanuit de identiteit waaraan ze gekoppeld is.'; + + @override + String get reputationInvalidRebind => + 'De toestemming om je reputatie te verplaatsen is ongeldig of verlopen. Maak een nieuwe aan.'; + + @override + String get reputationExpiredAttestation => + 'Deze reputatie is verlopen. Vraag ze opnieuw aan bij de bron.'; + + @override + String get reputationUntrustedIssuer => + 'Deze Mostro accepteert geen reputatie van die bron.'; + + @override + String get reputationIdentityMismatch => + 'Deze reputatie is uitgegeven voor een andere identiteit dan die je gebruikt.'; + + @override + String get reputationAlreadyImported => + 'Deze reputatie is al geïmporteerd op deze Mostro.'; + + @override + String get reputationInvalidAttestation => + 'Dit is geen geldige reputatie. Vraag ze opnieuw aan bij de bron.'; + + @override + String get reputationImportTitle => 'Reputatie importeren'; + + @override + String get reputationImportIntro => + 'Breng de reputatie die je op lnp2pBot of op een andere Mostro hebt verdiend naar deze. Importeren koppelt de twee accounts: de bron en deze Mostro weten dat ze van dezelfde persoon zijn.'; + + @override + String get reputationOpenLnp2pbot => 'Ophalen bij lnp2pBot'; + + @override + String get reputationPasteHint => 'Plak de ontvangen reputatie'; + + @override + String get reputationCheck => 'Controleren'; + + @override + String get reputationImported => 'Je reputatie is geïmporteerd.'; + + @override + String get reputationImportConfirm => 'Importeren in deze Mostro'; + + @override + String reputationFigures(int reviews, String rating, int days) { + return '$reviews ontvangen beoordelingen, gemiddeld $rating, handelt al $days dagen'; + } + + @override + String get reputationFiguresNote => + 'Geïmporteerde reputatie wordt opgeteld bij die van hier; je tegenpartijen zien ze samen.'; + + @override + String get reputationSettingTitle => 'Reputatie'; + + @override + String get reputationImportCardBody => + 'Breng de reputatie die je op lnp2pBot of een andere Mostro verdiende naar deze.'; + + @override + String get reputationExportTitle => 'Mijn reputatie exporteren'; + + @override + String get reputationExportCardBody => + 'Haal een ondertekende kopie op van de reputatie die je op deze Mostro verdiende, om ze op een andere te importeren.'; + + @override + String get reputationExportConfirmTitle => 'Exporteren naar deze identiteit?'; + + @override + String reputationExportConfirmBody(String npub) { + return 'Je reputatie op deze Mostro wordt geëxporteerd voor deze identiteit:\n\n$npub\n\nNa het exporteren is ze eraan gekoppeld: later exporteren voor een andere identiteit vereist een toestemming ondertekend met deze.'; + } + + @override + String get reputationExportConfirmButton => 'Exporteren'; + + @override + String get reputationExported => + 'Je reputatie is klaar. Schakel over naar de Mostro waar je ze wilt en importeer ze; ze is 7 dagen geldig.'; + + @override + String get reputationRebindBody => + 'Stap je over op een nieuwe identiteit? Onderteken hier, met de identiteit waaraan je reputatie nu gekoppeld is, een toestemming voor de nieuwe en gebruik die vanaf de nieuwe identiteit.'; + + @override + String get reputationNewIdentityHint => 'Nieuwe identiteit (npub)'; + + @override + String get reputationRebindSign => 'Toestemming ondertekenen'; + + @override + String get reputationInvalidIdentity => 'Dat is geen geldige npub.'; } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 0d4acc7d7..eb960b40d 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1256,5 +1256,37 @@ "lockEscrowOrderMovedOn": "De node zegt dat deze order niet meer op de escrow wacht. Je escrow blijft op dit apparaat opgeslagen; de status van de trade wordt bijgewerkt zodra de node die meldt.", "tradeHeadlineWaitingPaymentSellerCashu": "Vergrendel de escrow om de trade te starten", "tradeWaitingPaymentSellerInstructionCashu": "Vergrendel je ecash in de escrow bij de mint van de node. Dat start de trade, en het komt naar je terug als de node verdwijnt.", - "tradeBodyWaitingPaymentBuyerCashu": "Ze vergrendelen de sats in de escrow. Zodra die vergrendeld is, ben jij aan de beurt om de fiat te betalen." + "tradeBodyWaitingPaymentBuyerCashu": "Ze vergrendelen de sats in de escrow. Zodra die vergrendeld is, ben jij aan de beurt om de fiat te betalen.", + "reputationNodeDoesNotExport": "Deze Mostro exporteert geen reputatie.", + "reputationNodeDoesNotImport": "Deze Mostro importeert geen reputatie.", + "reputationIdentityRequired": "Reputatie kan alleen worden verplaatst vanaf een account dat zijn reputatie bewaart. Zet de volledige privacymodus uit en probeer het opnieuw.", + "reputationNotEligible": "Je account kan zijn reputatie nog niet exporteren: er zijn minstens 10 voltooide transacties en 5 ontvangen beoordelingen nodig.", + "reputationBoundToOtherIdentity": "Je reputatie daar is al gekoppeld aan een andere identiteit. Geef toestemming voor de verplaatsing vanuit de identiteit waaraan ze gekoppeld is.", + "reputationInvalidRebind": "De toestemming om je reputatie te verplaatsen is ongeldig of verlopen. Maak een nieuwe aan.", + "reputationExpiredAttestation": "Deze reputatie is verlopen. Vraag ze opnieuw aan bij de bron.", + "reputationUntrustedIssuer": "Deze Mostro accepteert geen reputatie van die bron.", + "reputationIdentityMismatch": "Deze reputatie is uitgegeven voor een andere identiteit dan die je gebruikt.", + "reputationAlreadyImported": "Deze reputatie is al geïmporteerd op deze Mostro.", + "reputationInvalidAttestation": "Dit is geen geldige reputatie. Vraag ze opnieuw aan bij de bron.", + "reputationImportTitle": "Reputatie importeren", + "reputationImportIntro": "Breng de reputatie die je op lnp2pBot of op een andere Mostro hebt verdiend naar deze. Importeren koppelt de twee accounts: de bron en deze Mostro weten dat ze van dezelfde persoon zijn.", + "reputationOpenLnp2pbot": "Ophalen bij lnp2pBot", + "reputationPasteHint": "Plak de ontvangen reputatie", + "reputationCheck": "Controleren", + "reputationImported": "Je reputatie is geïmporteerd.", + "reputationImportConfirm": "Importeren in deze Mostro", + "reputationFigures": "{reviews} ontvangen beoordelingen, gemiddeld {rating}, handelt al {days} dagen", + "reputationFiguresNote": "Geïmporteerde reputatie wordt opgeteld bij die van hier; je tegenpartijen zien ze samen.", + "reputationSettingTitle": "Reputatie", + "reputationImportCardBody": "Breng de reputatie die je op lnp2pBot of een andere Mostro verdiende naar deze.", + "reputationExportTitle": "Mijn reputatie exporteren", + "reputationExportCardBody": "Haal een ondertekende kopie op van de reputatie die je op deze Mostro verdiende, om ze op een andere te importeren.", + "reputationExportConfirmTitle": "Exporteren naar deze identiteit?", + "reputationExportConfirmBody": "Je reputatie op deze Mostro wordt geëxporteerd voor deze identiteit:\n\n{npub}\n\nNa het exporteren is ze eraan gekoppeld: later exporteren voor een andere identiteit vereist een toestemming ondertekend met deze.", + "reputationExportConfirmButton": "Exporteren", + "reputationExported": "Je reputatie is klaar. Schakel over naar de Mostro waar je ze wilt en importeer ze; ze is 7 dagen geldig.", + "reputationRebindBody": "Stap je over op een nieuwe identiteit? Onderteken hier, met de identiteit waaraan je reputatie nu gekoppeld is, een toestemming voor de nieuwe en gebruik die vanaf de nieuwe identiteit.", + "reputationNewIdentityHint": "Nieuwe identiteit (npub)", + "reputationRebindSign": "Toestemming ondertekenen", + "reputationInvalidIdentity": "Dat is geen geldige npub." } diff --git a/lib/src/rust/api/orders.dart b/lib/src/rust/api/orders.dart index 1902c8538..f371fd4e9 100644 --- a/lib/src/rust/api/orders.dart +++ b/lib/src/rust/api/orders.dart @@ -7,7 +7,7 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'types.dart'; -// These functions are ignored because they are not marked as `pub`: `_run_order_subscription`, `abandon_maker_bond`, `admin_pubkey_from_payload`, `adopt_range_remainder`, `advance_claim_phase`, `apply_ingested_order`, `apply_local_cancel`, `apply_payout_completed`, `apply_payout_request`, `apply_peer_reveal`, `apply_restored_peer`, `apply_restored_peers`, `apply_restored_status`, `apply_single_order_update`, `ask_daemon`, `bond_cancel_reason`, `bond_deadline`, `bond_expired`, `bond_refresh_is_stale`, `bond_requested`, `bridge_revision`, `bridge_snapshot`, `build_trade_key_map`, `cancel_maker_bond`, `chat_grace_of`, `claim_finished_trade_release`, `claim_mine`, `claim_single_order_task`, `classify_ingested_order`, `clear_maker_step_start`, `clear`, `close_expired_bond_trade`, `close_grace_chat_if_over`, `completes_trade`, `confirm_maker_bond`, `confirm_payout_completion`, `create_order_once`, `current_local_status`, `dispatch_mostro_message`, `dispute_id_from_payload`, `emit_trade_update_at`, `emit_trade_update_with`, `emit_trade_update`, `end_maker_waiting_step`, `ensure_global_dm_coverage`, `fetch_own_orders`, `fetch_public_order_revision`, `fetch_public_order_status`, `fetch_public_order`, `fetch_public_success_time`, `fill_restored_maker_row`, `forget_book_ownership`, `forget_ownership`, `forget_processed_daemon_messages`, `forget_range_slice`, `forget_trade_key_miss`, `forget_wire_order`, `fresh_request_id`, `get_trade_key_index`, `global_dm_keys`, `grace_check_delay`, `handle_create_bond_reply`, `handle_global_daemon_message`, `handle_single_order_event`, `ingest_order_event_with`, `ingest_order_event`, `is_claim_action`, `is_duplicate_daemon_message`, `is_maker_waiting_step`, `is_matching_cant_do_refusal`, `is_matching_last_trade_index_reply`, `is_matching_orders_reply`, `is_restored_maker_placeholder`, `last_trade_index`, `load_status_cursor`, `local_trade_status`, `lock_maker_bond`, `lock_order`, `log_wire_status_sync`, `lookup_trade_key_index`, `maker_bond_window_row`, `maker_order_is_published`, `maybe_capture_peer_reveal`, `mostro_dm_subscription_id`, `newest_book_order`, `newest_book_revision`, `next_trade_for_range_remainder`, `note_bond_locked`, `note_bond_released`, `note_public_success`, `note_trade_key_miss`, `note_user_cancel`, `note_wire_order`, `order_book_filters`, `order_book`, `order_locks`, `orders_subscription_id`, `over`, `ownership_view`, `peer_reveal_pubkeys`, `persist_bond`, `persist_confirmed_take`, `persist_late_create_confirmation`, `persist_peer_reputation`, `persist_restored_bond_rows`, `persist_restored_trade_row`, `persist_restored_trade_rows`, `persist_trade_row_in`, `persist_trade_row`, `process_order_event`, `public_view`, `publish_event_json`, `publish_event`, `publish_on_stored_events_end`, `publish`, `rebuild_trade_from_dm`, `recent_orders_subscription_id`, `reconcile_history_with`, `reconcile_published_maker_order`, `reconcile_restored_history`, `record_completion`, `record_miss`, `record_restore_snapshot`, `record_status_event`, `record`, `recovered_max_trade_index`, `refetch_active_node_orders`, `refresh_subscriptions_for_active_node`, `refresh_wire_order`, `relay_list_subscription_id`, `release_finished_chats_with`, `release_finished_chats`, `release_finished_trade_subscriptions`, `release_identity_subscriptions`, `release_single_order_task`, `remove_order_deferred`, `remove`, `replace_all`, `replace_global_dm_filter`, `replace_subscription`, `resolve_add_invoice_destination`, `resolve_dm_recipient`, `resolve_peer_side`, `restored_bond_row`, `restored_chat_relevant`, `restored_rows_to_fetch`, `restored_status_applies`, `restored_trade_row`, `resubscribe_global_dm_filter`, `resync_floor`, `resync_republished_maker_order`, `resync_trade_key_index`, `resync_watched_orders`, `reveal_warrants_chat_with`, `reveal_warrants_chat`, `run_stale_sweep_once`, `sanitize_trade_index`, `schedule_chat_grace_end`, `schedule_publish`, `seed_global_dm_coverage`, `settle_after_lost_take`, `settle_refused_maker_cancel`, `single_order_task_is_current`, `single_order_tasks`, `snapshot`, `spawn_stale_sweep`, `status_arm_gate`, `status_sync_blocked_by_terminal`, `status_write_blocked`, `store_trade_key_index`, `store_trade_pubkeys`, `subscribe_accepted`, `subscribe_daemon_messages`, `subscribe_deltas`, `subscribe_node_filters`, `subscribe_single_order`, `subscribe`, `sweep_action`, `sync_range_slice`, `sync_trade_fields_if_changed`, `sync_watched_orders`, `take_order_once`, `take_user_cancel`, `tombstone_covers`, `trade_is_over`, `trade_key_for_order`, `trade_key_index_of`, `trade_key_map`, `trade_key_misses`, `trade_row_from_small_order`, `trade_row_state`, `trade_updates_tx`, `trade`, `upsert`, `user_cancels`, `waiting_bond_status`, `watched_orders_subscription_id`, `wipe_never_active_trade`, `wipe_on_public_cancel`, `wipe_trade_row`, `wire_notes`, `with_bond_window`, `write_maker_step_end` +// These functions are ignored because they are not marked as `pub`: `_run_order_subscription`, `abandon_maker_bond`, `admin_pubkey_from_payload`, `adopt_range_remainder`, `advance_claim_phase`, `apply_ingested_order`, `apply_local_cancel`, `apply_payout_completed`, `apply_payout_request`, `apply_peer_reveal`, `apply_restored_peer`, `apply_restored_peers`, `apply_restored_status`, `apply_single_order_update`, `ask_daemon_with`, `ask_daemon`, `bond_cancel_reason`, `bond_deadline`, `bond_expired`, `bond_refresh_is_stale`, `bond_requested`, `bridge_revision`, `bridge_snapshot`, `build_trade_key_map`, `cancel_maker_bond`, `chat_grace_of`, `claim_finished_trade_release`, `claim_mine`, `claim_single_order_task`, `classify_ingested_order`, `clear_maker_step_start`, `clear`, `close_expired_bond_trade`, `close_grace_chat_if_over`, `completes_trade`, `confirm_maker_bond`, `confirm_payout_completion`, `create_order_once`, `current_local_status`, `dispatch_mostro_message`, `dispute_id_from_payload`, `emit_trade_update_at`, `emit_trade_update_with`, `emit_trade_update`, `end_maker_waiting_step`, `ensure_global_dm_coverage`, `fetch_own_orders`, `fetch_public_order_revision`, `fetch_public_order_status`, `fetch_public_order`, `fetch_public_success_time`, `fill_restored_maker_row`, `forget_book_ownership`, `forget_ownership`, `forget_processed_daemon_messages`, `forget_range_slice`, `forget_trade_key_miss`, `forget_wire_order`, `fresh_request_id`, `get_trade_key_index`, `global_dm_keys`, `grace_check_delay`, `handle_create_bond_reply`, `handle_global_daemon_message`, `handle_single_order_event`, `ingest_order_event_with`, `ingest_order_event`, `is_claim_action`, `is_duplicate_daemon_message`, `is_maker_waiting_step`, `is_matching_cant_do_refusal`, `is_matching_last_trade_index_reply`, `is_matching_orders_reply`, `is_restored_maker_placeholder`, `last_trade_index`, `load_status_cursor`, `local_trade_status`, `lock_maker_bond`, `lock_order`, `log_wire_status_sync`, `lookup_trade_key_index`, `maker_bond_window_row`, `maker_order_is_published`, `maybe_capture_peer_reveal`, `mostro_dm_subscription_id`, `newest_book_order`, `newest_book_revision`, `next_trade_for_range_remainder`, `note_bond_locked`, `note_bond_released`, `note_public_success`, `note_trade_key_miss`, `note_user_cancel`, `note_wire_order`, `order_book_filters`, `order_book`, `order_locks`, `orders_subscription_id`, `over`, `ownership_view`, `peer_reveal_pubkeys`, `persist_bond`, `persist_confirmed_take`, `persist_late_create_confirmation`, `persist_peer_reputation`, `persist_restored_bond_rows`, `persist_restored_trade_row`, `persist_restored_trade_rows`, `persist_trade_row_in`, `persist_trade_row`, `process_order_event`, `public_view`, `publish_event_json`, `publish_event`, `publish_on_stored_events_end`, `publish`, `rebuild_trade_from_dm`, `recent_orders_subscription_id`, `reconcile_history_with`, `reconcile_published_maker_order`, `reconcile_restored_history`, `record_completion`, `record_miss`, `record_restore_snapshot`, `record_status_event`, `record`, `recovered_max_trade_index`, `refetch_active_node_orders`, `refresh_subscriptions_for_active_node`, `refresh_wire_order`, `relay_list_subscription_id`, `release_finished_chats_with`, `release_finished_chats`, `release_finished_trade_subscriptions`, `release_identity_subscriptions`, `release_single_order_task`, `remove_order_deferred`, `remove`, `replace_all`, `replace_global_dm_filter`, `replace_subscription`, `resolve_add_invoice_destination`, `resolve_dm_recipient`, `resolve_peer_side`, `restored_bond_row`, `restored_chat_relevant`, `restored_rows_to_fetch`, `restored_status_applies`, `restored_trade_row`, `resubscribe_global_dm_filter`, `resync_floor`, `resync_republished_maker_order`, `resync_trade_key_index`, `resync_watched_orders`, `reveal_warrants_chat_with`, `reveal_warrants_chat`, `run_stale_sweep_once`, `sanitize_trade_index`, `schedule_chat_grace_end`, `schedule_publish`, `seed_global_dm_coverage`, `settle_after_lost_take`, `settle_refused_maker_cancel`, `single_order_task_is_current`, `single_order_tasks`, `snapshot`, `spawn_stale_sweep`, `status_arm_gate`, `status_sync_blocked_by_terminal`, `status_write_blocked`, `store_trade_key_index`, `store_trade_pubkeys`, `subscribe_accepted`, `subscribe_daemon_messages`, `subscribe_deltas`, `subscribe_node_filters`, `subscribe_single_order`, `subscribe`, `sweep_action`, `sync_range_slice`, `sync_trade_fields_if_changed`, `sync_watched_orders`, `take_order_once`, `take_user_cancel`, `tombstone_covers`, `trade_is_over`, `trade_key_for_order`, `trade_key_index_of`, `trade_key_map`, `trade_key_misses`, `trade_row_from_small_order`, `trade_row_state`, `trade_updates_tx`, `trade`, `upsert`, `user_cancels`, `waiting_bond_status`, `watched_orders_subscription_id`, `wipe_never_active_trade`, `wipe_on_public_cancel`, `wipe_trade_row`, `wire_notes`, `with_bond_window`, `write_maker_step_end` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `BookState`, `CashuLockWake`, `DaemonAnswer`, `DedupWindow`, `FinishedTradeRelease`, `IngestedOrder`, `MakerCancelWake`, `OrderBookDelta`, `Publish`, `ResetGuard`, `RowState`, `SingleOrderEvent`, `SweepAction` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `clone`, `clone`, `clone`, `drop`, `drop`, `drop`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt` // These functions are ignored (category: IgnoreBecauseExplicitAttribute): `restore_session` diff --git a/lib/src/rust/api/reputation_transfer.dart b/lib/src/rust/api/reputation_transfer.dart new file mode 100644 index 000000000..f59aa7efc --- /dev/null +++ b/lib/src/rust/api/reputation_transfer.dart @@ -0,0 +1,155 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `ask`, `identity_keys`, `is_export_reply`, `is_import_reply`, `new`, `parse`, `refusal` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `eq`, `eq`, `fmt`, `fmt` + +/// What the active node advertises, as of its last capability fetch. +ReputationSupportInfo getReputationSupport() => + RustLib.instance.api.crateApiReputationTransferGetReputationSupport(); + +/// Verify an attestation pasted or received, without sending anything: the +/// event's own rules (signature, kind, tags, clock). Whether the node trusts +/// its issuer is the node's to say on import. +ReputationAttestationInfo parseReputationAttestation({required String json}) => + RustLib.instance.api.crateApiReputationTransferParseReputationAttestation( + json: json, + ); + +/// Ask the active node to attest the user's reputation there for +/// `destination` (the user's own identity when `None`). The UI confirms the +/// destination with the user first: the request binds the node's account to +/// it. `rebind` is an authorisation from [`sign_reputation_rebind`], needed +/// when the account is bound to another identity. The attestation is +/// verified, kept until imported, and returned. +Future exportReputation({ + String? destination, + String? rebind, +}) => RustLib.instance.api.crateApiReputationTransferExportReputation( + destination: destination, + rebind: rebind, +); + +/// Import `attestation_json` into the active node. It must name the user's +/// identity and be signed by a key the node advertises it trusts; the node +/// runs the full checks and answers `reputation-imported` or a refusal. +Future importReputation({ + required String attestationJson, +}) => RustLib.instance.api.crateApiReputationTransferImportReputation( + attestationJson: attestationJson, +); + +/// The attestation exported last and not imported yet, while still valid. +Future getPendingReputationAttestation() => + RustLib.instance.api + .crateApiReputationTransferGetPendingReputationAttestation(); + +/// Sign, with the identity the reputation at `issuer` is bound to now, the +/// authorisation to move that binding to `new_identity` (hex). Pass it as +/// `rebind` to [`export_reputation`] from the new identity, or paste it into +/// lnp2pBot. Valid for an hour. +Future signReputationRebind({ + required String issuer, + required String newIdentity, +}) => RustLib.instance.api.crateApiReputationTransferSignReputationRebind( + issuer: issuer, + newIdentity: newIdentity, +); + +/// A verified reputation attestation, for display and import. +class ReputationAttestationInfo { + final String id; + + /// Key that signed it. + final String issuer; + + /// Identity it is addressed to. + final String destination; + + /// The source account, opaque. + final String subject; + + /// Ratings received on the source. + final int reviews; + + /// Their average, with two decimals (`"4.87"`). + final String rating; + + /// UTC day start of the first completed trade on the source. + final PlatformInt64 since; + final PlatformInt64 createdAt; + final PlatformInt64 expiration; + + /// The event JSON, as received, to import unchanged. + final String json; + + const ReputationAttestationInfo({ + required this.id, + required this.issuer, + required this.destination, + required this.subject, + required this.reviews, + required this.rating, + required this.since, + required this.createdAt, + required this.expiration, + required this.json, + }); + + @override + int get hashCode => + id.hashCode ^ + issuer.hashCode ^ + destination.hashCode ^ + subject.hashCode ^ + reviews.hashCode ^ + rating.hashCode ^ + since.hashCode ^ + createdAt.hashCode ^ + expiration.hashCode ^ + json.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReputationAttestationInfo && + runtimeType == other.runtimeType && + id == other.id && + issuer == other.issuer && + destination == other.destination && + subject == other.subject && + reviews == other.reviews && + rating == other.rating && + since == other.since && + createdAt == other.createdAt && + expiration == other.expiration && + json == other.json; +} + +/// What the active node offers for reputation portability. +class ReputationSupportInfo { + /// The key the node signs attestations with; `None` when it does not + /// export. + final String? issuer; + + /// The issuer keys it imports from; `None` when it does not import. + final List? importIssuers; + + const ReputationSupportInfo({this.issuer, this.importIssuers}); + + @override + int get hashCode => issuer.hashCode ^ importIssuers.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReputationSupportInfo && + runtimeType == other.runtimeType && + issuer == other.issuer && + importIssuers == other.importIssuers; +} diff --git a/lib/src/rust/frb_generated.dart b/lib/src/rust/frb_generated.dart index 57fb12fb4..0e5f59594 100644 --- a/lib/src/rust/frb_generated.dart +++ b/lib/src/rust/frb_generated.dart @@ -19,6 +19,7 @@ import 'api/nwc.dart'; import 'api/orders.dart'; import 'api/push.dart'; import 'api/reputation.dart'; +import 'api/reputation_transfer.dart'; import 'api/restore_progress.dart'; import 'api/settings.dart'; import 'api/trade_touch.dart'; @@ -83,7 +84,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -1351531358; + int get rustContentHash => -3555436; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -317,6 +318,11 @@ abstract class RustLibApi extends BaseApi { required String passphrase, }); + Future crateApiReputationTransferExportReputation({ + String? destination, + String? rebind, + }); + Future crateApiNostrFetchExchangeRate({ required String mostroPubkeyHex, required String fiatCode, @@ -375,6 +381,9 @@ abstract class RustLibApi extends BaseApi { Future> crateApiOrdersGetOrders({OrderFilters? filters}); + Future + crateApiReputationTransferGetPendingReputationAttestation(); + Future crateApiReputationGetPrivacyMode(); Future crateApiPushGetPushStatus(); @@ -385,6 +394,8 @@ abstract class RustLibApi extends BaseApi { Future> crateApiNostrGetRelays(); + ReputationSupportInfo crateApiReputationTransferGetReputationSupport(); + Future crateApiSettingsGetSettings(); Future crateApiIdentityGetTradeKey({required int index}); @@ -417,6 +428,10 @@ abstract class RustLibApi extends BaseApi { Future crateApiIdentityImportFromNsec({required String nsec}); + Future crateApiReputationTransferImportReputation({ + required String attestationJson, + }); + Future crateApiInitDb({required String path}); Future crateApiNostrInitialize({List? relays}); @@ -505,6 +520,9 @@ abstract class RustLibApi extends BaseApi { Future crateApiOrdersOrderFiltersDefault(); + ReputationAttestationInfo + crateApiReputationTransferParseReputationAttestation({required String json}); + Future crateApiNwcPayInvoice({required String bolt11}); Future crateApiTypesPushPlatformAsWire({required PushPlatform that}); @@ -595,6 +613,11 @@ abstract class RustLibApi extends BaseApi { Future crateApiSettingsSetTheme({required ThemeMode theme}); + Future crateApiReputationTransferSignReputationRebind({ + required String issuer, + required String newIdentity, + }); + Future crateApiDisputesSolverRole({ required String tradeId, required String solverPubkey, @@ -3005,6 +3028,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["passphrase"], ); + @override + Future crateApiReputationTransferExportReputation({ + String? destination, + String? rebind, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_opt_String(destination, serializer); + sse_encode_opt_String(rebind, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 67, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_reputation_attestation_info, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiReputationTransferExportReputationConstMeta, + argValues: [destination, rebind], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiReputationTransferExportReputationConstMeta => + const TaskConstMeta( + debugName: "export_reputation", + argNames: ["destination", "rebind"], + ); + @override Future crateApiNostrFetchExchangeRate({ required String mostroPubkeyHex, @@ -3019,7 +3077,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 67, + funcId: 68, port: port_, ); }, @@ -3052,7 +3110,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 68, + funcId: 69, port: port_, ); }, @@ -3085,7 +3143,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 69, + funcId: 70, port: port_, ); }, @@ -3115,7 +3173,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 70, + funcId: 71, port: port_, ); }, @@ -3142,7 +3200,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 71, + funcId: 72, port: port_, ); }, @@ -3169,7 +3227,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 72, + funcId: 73, port: port_, ); }, @@ -3199,7 +3257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 73, + funcId: 74, port: port_, ); }, @@ -3229,7 +3287,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 74, + funcId: 75, port: port_, ); }, @@ -3257,7 +3315,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 75, + funcId: 76, port: port_, ); }, @@ -3289,7 +3347,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 76, + funcId: 77, port: port_, ); }, @@ -3319,7 +3377,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 77, + funcId: 78, port: port_, ); }, @@ -3346,7 +3404,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 78, + funcId: 79, port: port_, ); }, @@ -3374,7 +3432,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 79, + funcId: 80, port: port_, ); }, @@ -3401,7 +3459,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 80, + funcId: 81, port: port_, ); }, @@ -3428,7 +3486,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 81, + funcId: 82, port: port_, ); }, @@ -3458,7 +3516,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 82, + funcId: 83, port: port_, ); }, @@ -3485,7 +3543,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 83, + funcId: 84, port: port_, ); }, @@ -3515,7 +3573,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 84, + funcId: 85, port: port_, ); }, @@ -3546,7 +3604,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 85, + funcId: 86, port: port_, ); }, @@ -3573,7 +3631,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 86, + funcId: 87, port: port_, ); }, @@ -3601,7 +3659,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 87, + funcId: 88, port: port_, ); }, @@ -3619,6 +3677,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiOrdersGetOrdersConstMeta => const TaskConstMeta(debugName: "get_orders", argNames: ["filters"]); + @override + Future + crateApiReputationTransferGetPendingReputationAttestation() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 89, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: + sse_decode_opt_box_autoadd_reputation_attestation_info, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateApiReputationTransferGetPendingReputationAttestationConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiReputationTransferGetPendingReputationAttestationConstMeta => + const TaskConstMeta( + debugName: "get_pending_reputation_attestation", + argNames: [], + ); + @override Future crateApiReputationGetPrivacyMode() { return handler.executeNormal( @@ -3628,7 +3720,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 88, + funcId: 90, port: port_, ); }, @@ -3655,7 +3747,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 89, + funcId: 91, port: port_, ); }, @@ -3685,7 +3777,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 90, + funcId: 92, port: port_, ); }, @@ -3715,7 +3807,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 91, + funcId: 93, port: port_, ); }, @@ -3733,6 +3825,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiNostrGetRelaysConstMeta => const TaskConstMeta(debugName: "get_relays", argNames: []); + @override + ReputationSupportInfo crateApiReputationTransferGetReputationSupport() { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_reputation_support_info, + decodeErrorData: null, + ), + constMeta: kCrateApiReputationTransferGetReputationSupportConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiReputationTransferGetReputationSupportConstMeta => + const TaskConstMeta(debugName: "get_reputation_support", argNames: []); + @override Future crateApiSettingsGetSettings() { return handler.executeNormal( @@ -3742,7 +3856,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 92, + funcId: 95, port: port_, ); }, @@ -3770,7 +3884,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 93, + funcId: 96, port: port_, ); }, @@ -3798,7 +3912,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 94, + funcId: 97, port: port_, ); }, @@ -3825,7 +3939,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 95, + funcId: 98, port: port_, ); }, @@ -3852,7 +3966,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 96, + funcId: 99, port: port_, ); }, @@ -3880,7 +3994,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 97, + funcId: 100, port: port_, ); }, @@ -3911,7 +4025,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 98, + funcId: 101, port: port_, ); }, @@ -3946,7 +4060,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 99, + funcId: 102, port: port_, ); }, @@ -3983,7 +4097,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 100, + funcId: 103, port: port_, ); }, @@ -4018,7 +4132,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 101, + funcId: 104, port: port_, ); }, @@ -4049,7 +4163,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 102, + funcId: 105, port: port_, ); }, @@ -4067,6 +4181,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiIdentityImportFromNsecConstMeta => const TaskConstMeta(debugName: "import_from_nsec", argNames: ["nsec"]); + @override + Future crateApiReputationTransferImportReputation({ + required String attestationJson, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(attestationJson, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 106, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_reputation_attestation_info, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiReputationTransferImportReputationConstMeta, + argValues: [attestationJson], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiReputationTransferImportReputationConstMeta => + const TaskConstMeta( + debugName: "import_reputation", + argNames: ["attestationJson"], + ); + @override Future crateApiInitDb({required String path}) { return handler.executeNormal( @@ -4077,7 +4224,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 103, + funcId: 107, port: port_, ); }, @@ -4105,7 +4252,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 104, + funcId: 108, port: port_, ); }, @@ -4132,7 +4279,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 105, + funcId: 109, port: port_, ); }, @@ -4159,7 +4306,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 106, + funcId: 110, port: port_, ); }, @@ -4186,7 +4333,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 107, + funcId: 111, port: port_, ); }, @@ -4213,7 +4360,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 108, + funcId: 112, port: port_, ); }, @@ -4249,7 +4396,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 109, + funcId: 113, port: port_, ); }, @@ -4280,7 +4427,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 110, + funcId: 114, port: port_, ); }, @@ -4314,7 +4461,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 111, + funcId: 115, port: port_, ); }, @@ -4344,7 +4491,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 112, + funcId: 116, port: port_, ); }, @@ -4371,7 +4518,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 113, + funcId: 117, port: port_, ); }, @@ -4398,7 +4545,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 114, + funcId: 118, port: port_, ); }, @@ -4429,7 +4576,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 115, + funcId: 119, port: port_, ); }, @@ -4460,7 +4607,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 116, + funcId: 120, port: port_, ); }, @@ -4488,7 +4635,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 117, + funcId: 121, port: port_, ); }, @@ -4516,7 +4663,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 118, + funcId: 122, port: port_, ); }, @@ -4544,7 +4691,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 119, + funcId: 123, port: port_, ); }, @@ -4578,7 +4725,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 120, + funcId: 124, port: port_, ); }, @@ -4609,7 +4756,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 121, + funcId: 125, port: port_, ); }, @@ -4637,7 +4784,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 122, + funcId: 126, port: port_, ); }, @@ -4668,7 +4815,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 123, + funcId: 127, port: port_, ); }, @@ -4696,7 +4843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 124, + funcId: 128, port: port_, ); }, @@ -4724,7 +4871,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 125, + funcId: 129, port: port_, ); }, @@ -4752,7 +4899,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 126, + funcId: 130, port: port_, ); }, @@ -4780,7 +4927,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 127, + funcId: 131, port: port_, ); }, @@ -4808,7 +4955,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 128, + funcId: 132, port: port_, ); }, @@ -4836,7 +4983,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 129, + funcId: 133, port: port_, ); }, @@ -4864,7 +5011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 130, + funcId: 134, port: port_, ); }, @@ -4892,7 +5039,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 131, + funcId: 135, port: port_, ); }, @@ -4920,7 +5067,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 132, + funcId: 136, port: port_, ); }, @@ -4951,7 +5098,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 133, + funcId: 137, port: port_, ); }, @@ -4979,7 +5126,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 134, + funcId: 138, port: port_, ); }, @@ -5007,7 +5154,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 135, + funcId: 139, port: port_, ); }, @@ -5035,7 +5182,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 136, + funcId: 140, port: port_, ); }, @@ -5068,7 +5215,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 137, + funcId: 141, port: port_, ); }, @@ -5098,7 +5245,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 138, + funcId: 142, port: port_, ); }, @@ -5116,6 +5263,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiOrdersOrderFiltersDefaultConstMeta => const TaskConstMeta(debugName: "order_filters_default", argNames: []); + @override + ReputationAttestationInfo + crateApiReputationTransferParseReputationAttestation({required String json}) { + return handler.executeSync( + SyncTask( + callFfi: () { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(json, serializer); + return pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 143, + )!; + }, + codec: SseCodec( + decodeSuccessData: sse_decode_reputation_attestation_info, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: + kCrateApiReputationTransferParseReputationAttestationConstMeta, + argValues: [json], + apiImpl: this, + ), + ); + } + + TaskConstMeta + get kCrateApiReputationTransferParseReputationAttestationConstMeta => + const TaskConstMeta( + debugName: "parse_reputation_attestation", + argNames: ["json"], + ); + @override Future crateApiNwcPayInvoice({required String bolt11}) { return handler.executeNormal( @@ -5126,7 +5306,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 139, + funcId: 144, port: port_, ); }, @@ -5154,7 +5334,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 140, + funcId: 145, port: port_, ); }, @@ -5187,7 +5367,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 141, + funcId: 146, port: port_, ); }, @@ -5217,7 +5397,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 142, + funcId: 147, port: port_, ); }, @@ -5244,7 +5424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 143, + funcId: 148, port: port_, ); }, @@ -5271,7 +5451,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 144, + funcId: 149, port: port_, ); }, @@ -5298,7 +5478,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 145, + funcId: 150, port: port_, ); }, @@ -5328,7 +5508,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 146, + funcId: 151, port: port_, ); }, @@ -5358,7 +5538,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 147, + funcId: 152, port: port_, ); }, @@ -5388,7 +5568,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 148, + funcId: 153, port: port_, ); }, @@ -5419,7 +5599,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 149, + funcId: 154, port: port_, ); }, @@ -5447,7 +5627,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 150, + funcId: 155, port: port_, ); }, @@ -5478,7 +5658,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 151, + funcId: 156, port: port_, ); }, @@ -5508,7 +5688,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 152, + funcId: 157, port: port_, ); }, @@ -5538,7 +5718,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 153, + funcId: 158, port: port_, ); }, @@ -5568,7 +5748,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 154, + funcId: 159, port: port_, ); }, @@ -5604,7 +5784,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 155, + funcId: 160, port: port_, ); }, @@ -5635,7 +5815,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 156, + funcId: 161, port: port_, ); }, @@ -5671,7 +5851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 157, + funcId: 162, port: port_, ); }, @@ -5707,7 +5887,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 158, + funcId: 163, port: port_, ); }, @@ -5741,7 +5921,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 159, + funcId: 164, port: port_, ); }, @@ -5772,7 +5952,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 160, + funcId: 165, port: port_, ); }, @@ -5803,7 +5983,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 161, + funcId: 166, port: port_, ); }, @@ -5834,7 +6014,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 162, + funcId: 167, port: port_, ); }, @@ -5865,7 +6045,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 163, + funcId: 168, port: port_, ); }, @@ -5896,7 +6076,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 164, + funcId: 169, port: port_, ); }, @@ -5927,7 +6107,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 165, + funcId: 170, port: port_, ); }, @@ -5955,7 +6135,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 166, + funcId: 171, port: port_, ); }, @@ -5986,7 +6166,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 167, + funcId: 172, port: port_, ); }, @@ -6014,7 +6194,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 168, + funcId: 173, port: port_, ); }, @@ -6046,7 +6226,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 169, + funcId: 174, port: port_, ); }, @@ -6076,7 +6256,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 170, + funcId: 175, port: port_, ); }, @@ -6107,7 +6287,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 171, + funcId: 176, port: port_, ); }, @@ -6125,6 +6305,41 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiSettingsSetThemeConstMeta => const TaskConstMeta(debugName: "set_theme", argNames: ["theme"]); + @override + Future crateApiReputationTransferSignReputationRebind({ + required String issuer, + required String newIdentity, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_String(issuer, serializer); + sse_encode_String(newIdentity, serializer); + pdeCallFfi( + generalizedFrbRustBinding, + serializer, + funcId: 177, + port: port_, + ); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_String, + decodeErrorData: sse_decode_AnyhowException, + ), + constMeta: kCrateApiReputationTransferSignReputationRebindConstMeta, + argValues: [issuer, newIdentity], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiReputationTransferSignReputationRebindConstMeta => + const TaskConstMeta( + debugName: "sign_reputation_rebind", + argNames: ["issuer", "newIdentity"], + ); + @override Future crateApiDisputesSolverRole({ required String tradeId, @@ -6139,7 +6354,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 172, + funcId: 178, port: port_, ); }, @@ -6173,7 +6388,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 173, + funcId: 179, port: port_, ); }, @@ -6208,7 +6423,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 174, + funcId: 180, port: port_, ); }, @@ -6243,7 +6458,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 175, + funcId: 181, port: port_, ); }, @@ -6273,7 +6488,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 176, + funcId: 182, port: port_, ); }, @@ -6307,7 +6522,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 177, + funcId: 183, port: port_, ); }, @@ -6339,7 +6554,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { pdeCallFfi( generalizedFrbRustBinding, serializer, - funcId: 178, + funcId: 184, port: port_, ); }, @@ -7561,6 +7776,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_relay_info(raw); } + @protected + ReputationAttestationInfo dco_decode_box_autoadd_reputation_attestation_info( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_reputation_attestation_info(raw); + } + @protected RestoreProgress dco_decode_box_autoadd_restore_progress(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -8228,6 +8451,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_relay_info(raw); } + @protected + ReputationAttestationInfo? + dco_decode_opt_box_autoadd_reputation_attestation_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_reputation_attestation_info(raw); + } + @protected RestoreProgress? dco_decode_opt_box_autoadd_restore_progress(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -8499,6 +8731,40 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return RelayStatus.values[raw as int]; } + @protected + ReputationAttestationInfo dco_decode_reputation_attestation_info( + dynamic raw, + ) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 10) + throw Exception('unexpected arr length: expect 10 but see ${arr.length}'); + return ReputationAttestationInfo( + id: dco_decode_String(arr[0]), + issuer: dco_decode_String(arr[1]), + destination: dco_decode_String(arr[2]), + subject: dco_decode_String(arr[3]), + reviews: dco_decode_u_32(arr[4]), + rating: dco_decode_String(arr[5]), + since: dco_decode_i_64(arr[6]), + createdAt: dco_decode_i_64(arr[7]), + expiration: dco_decode_i_64(arr[8]), + json: dco_decode_String(arr[9]), + ); + } + + @protected + ReputationSupportInfo dco_decode_reputation_support_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return ReputationSupportInfo( + issuer: dco_decode_opt_String(arr[0]), + importIssuers: dco_decode_opt_list_String(arr[1]), + ); + } + @protected RestoreProgress dco_decode_restore_progress(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -9991,6 +10257,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_relay_info(deserializer)); } + @protected + ReputationAttestationInfo sse_decode_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_reputation_attestation_info(deserializer)); + } + @protected RestoreProgress sse_decode_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -10995,6 +11269,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + ReputationAttestationInfo? + sse_decode_opt_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_box_autoadd_reputation_attestation_info(deserializer)); + } else { + return null; + } + } + @protected RestoreProgress? sse_decode_opt_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -11373,6 +11661,48 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return RelayStatus.values[inner]; } + @protected + ReputationAttestationInfo sse_decode_reputation_attestation_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_String(deserializer); + var var_issuer = sse_decode_String(deserializer); + var var_destination = sse_decode_String(deserializer); + var var_subject = sse_decode_String(deserializer); + var var_reviews = sse_decode_u_32(deserializer); + var var_rating = sse_decode_String(deserializer); + var var_since = sse_decode_i_64(deserializer); + var var_createdAt = sse_decode_i_64(deserializer); + var var_expiration = sse_decode_i_64(deserializer); + var var_json = sse_decode_String(deserializer); + return ReputationAttestationInfo( + id: var_id, + issuer: var_issuer, + destination: var_destination, + subject: var_subject, + reviews: var_reviews, + rating: var_rating, + since: var_since, + createdAt: var_createdAt, + expiration: var_expiration, + json: var_json, + ); + } + + @protected + ReputationSupportInfo sse_decode_reputation_support_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_issuer = sse_decode_opt_String(deserializer); + var var_importIssuers = sse_decode_opt_list_String(deserializer); + return ReputationSupportInfo( + issuer: var_issuer, + importIssuers: var_importIssuers, + ); + } + @protected RestoreProgress sse_decode_restore_progress(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -12946,6 +13276,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_relay_info(self, serializer); } + @protected + void sse_encode_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_reputation_attestation_info(self, serializer); + } + @protected void sse_encode_box_autoadd_restore_progress( RestoreProgress self, @@ -13848,6 +14187,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_reputation_attestation_info(self, serializer); + } + } + @protected void sse_encode_opt_box_autoadd_restore_progress( RestoreProgress? self, @@ -14151,6 +14503,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_i_32(self.index, serializer); } + @protected + void sse_encode_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.id, serializer); + sse_encode_String(self.issuer, serializer); + sse_encode_String(self.destination, serializer); + sse_encode_String(self.subject, serializer); + sse_encode_u_32(self.reviews, serializer); + sse_encode_String(self.rating, serializer); + sse_encode_i_64(self.since, serializer); + sse_encode_i_64(self.createdAt, serializer); + sse_encode_i_64(self.expiration, serializer); + sse_encode_String(self.json, serializer); + } + + @protected + void sse_encode_reputation_support_info( + ReputationSupportInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_opt_String(self.issuer, serializer); + sse_encode_opt_list_String(self.importIssuers, serializer); + } + @protected void sse_encode_restore_progress( RestoreProgress self, diff --git a/lib/src/rust/frb_generated.io.dart b/lib/src/rust/frb_generated.io.dart index 67c0f660a..842cb754b 100644 --- a/lib/src/rust/frb_generated.io.dart +++ b/lib/src/rust/frb_generated.io.dart @@ -19,6 +19,7 @@ import 'api/nwc.dart'; import 'api/orders.dart'; import 'api/push.dart'; import 'api/reputation.dart'; +import 'api/reputation_transfer.dart'; import 'api/restore_progress.dart'; import 'api/settings.dart'; import 'api/trade_touch.dart'; @@ -695,6 +696,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo dco_decode_box_autoadd_relay_info(dynamic raw); + @protected + ReputationAttestationInfo dco_decode_box_autoadd_reputation_attestation_info( + dynamic raw, + ); + @protected RestoreProgress dco_decode_box_autoadd_restore_progress(dynamic raw); @@ -934,6 +940,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo? dco_decode_opt_box_autoadd_relay_info(dynamic raw); + @protected + ReputationAttestationInfo? + dco_decode_opt_box_autoadd_reputation_attestation_info(dynamic raw); + @protected RestoreProgress? dco_decode_opt_box_autoadd_restore_progress(dynamic raw); @@ -1014,6 +1024,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayStatus dco_decode_relay_status(dynamic raw); + @protected + ReputationAttestationInfo dco_decode_reputation_attestation_info(dynamic raw); + + @protected + ReputationSupportInfo dco_decode_reputation_support_info(dynamic raw); + @protected RestoreProgress dco_decode_restore_progress(dynamic raw); @@ -1664,6 +1680,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo sse_decode_box_autoadd_relay_info(SseDeserializer deserializer); + @protected + ReputationAttestationInfo sse_decode_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress sse_decode_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -1959,6 +1980,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ReputationAttestationInfo? + sse_decode_opt_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress? sse_decode_opt_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -2057,6 +2084,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayStatus sse_decode_relay_status(SseDeserializer deserializer); + @protected + ReputationAttestationInfo sse_decode_reputation_attestation_info( + SseDeserializer deserializer, + ); + + @protected + ReputationSupportInfo sse_decode_reputation_support_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress sse_decode_restore_progress(SseDeserializer deserializer); @@ -2845,6 +2882,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_restore_progress( RestoreProgress self, @@ -3247,6 +3290,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_restore_progress( RestoreProgress? self, @@ -3358,6 +3407,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_relay_status(RelayStatus self, SseSerializer serializer); + @protected + void sse_encode_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ); + + @protected + void sse_encode_reputation_support_info( + ReputationSupportInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_restore_progress( RestoreProgress self, diff --git a/lib/src/rust/frb_generated.web.dart b/lib/src/rust/frb_generated.web.dart index e22efbfc5..26b275408 100644 --- a/lib/src/rust/frb_generated.web.dart +++ b/lib/src/rust/frb_generated.web.dart @@ -22,6 +22,7 @@ import 'api/nwc.dart'; import 'api/orders.dart'; import 'api/push.dart'; import 'api/reputation.dart'; +import 'api/reputation_transfer.dart'; import 'api/restore_progress.dart'; import 'api/settings.dart'; import 'api/trade_touch.dart'; @@ -697,6 +698,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo dco_decode_box_autoadd_relay_info(dynamic raw); + @protected + ReputationAttestationInfo dco_decode_box_autoadd_reputation_attestation_info( + dynamic raw, + ); + @protected RestoreProgress dco_decode_box_autoadd_restore_progress(dynamic raw); @@ -936,6 +942,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo? dco_decode_opt_box_autoadd_relay_info(dynamic raw); + @protected + ReputationAttestationInfo? + dco_decode_opt_box_autoadd_reputation_attestation_info(dynamic raw); + @protected RestoreProgress? dco_decode_opt_box_autoadd_restore_progress(dynamic raw); @@ -1016,6 +1026,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayStatus dco_decode_relay_status(dynamic raw); + @protected + ReputationAttestationInfo dco_decode_reputation_attestation_info(dynamic raw); + + @protected + ReputationSupportInfo dco_decode_reputation_support_info(dynamic raw); + @protected RestoreProgress dco_decode_restore_progress(dynamic raw); @@ -1666,6 +1682,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayInfo sse_decode_box_autoadd_relay_info(SseDeserializer deserializer); + @protected + ReputationAttestationInfo sse_decode_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress sse_decode_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -1961,6 +1982,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + ReputationAttestationInfo? + sse_decode_opt_box_autoadd_reputation_attestation_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress? sse_decode_opt_box_autoadd_restore_progress( SseDeserializer deserializer, @@ -2059,6 +2086,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected RelayStatus sse_decode_relay_status(SseDeserializer deserializer); + @protected + ReputationAttestationInfo sse_decode_reputation_attestation_info( + SseDeserializer deserializer, + ); + + @protected + ReputationSupportInfo sse_decode_reputation_support_info( + SseDeserializer deserializer, + ); + @protected RestoreProgress sse_decode_restore_progress(SseDeserializer deserializer); @@ -2847,6 +2884,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_box_autoadd_restore_progress( RestoreProgress self, @@ -3249,6 +3292,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_opt_box_autoadd_reputation_attestation_info( + ReputationAttestationInfo? self, + SseSerializer serializer, + ); + @protected void sse_encode_opt_box_autoadd_restore_progress( RestoreProgress? self, @@ -3360,6 +3409,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_relay_status(RelayStatus self, SseSerializer serializer); + @protected + void sse_encode_reputation_attestation_info( + ReputationAttestationInfo self, + SseSerializer serializer, + ); + + @protected + void sse_encode_reputation_support_info( + ReputationSupportInfo self, + SseSerializer serializer, + ); + @protected void sse_encode_restore_progress( RestoreProgress self, diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 18a757ddd..e4e08c6fe 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2597,8 +2597,7 @@ dependencies = [ [[package]] name = "mostro-core" version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a75b4e5ba5756d8d6e0a952b59e752080694cfeb92112d2c75f33af8e45608" +source = "git+https://github.com/MostroP2P/mostro-core?branch=feat%2Freputation-merge#64ad62d065901773cf84668c8f0ef3583cf7a8b4" dependencies = [ "bitcoin", "chrono", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index f18afc468..638612516 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -25,7 +25,7 @@ nostr = { version = "0.45", default-features = false, features = ["std", "nip04" # requires nostr 0.45. The Cashu wire form pinned by `mostro/cashu_wire.rs` and # documented in docs/cashu/README.md §2 was confirmed against 0.14.1 and is # unchanged since. -mostro-core = "0.16.0" +mostro-core = { git = "https://github.com/MostroP2P/mostro-core", branch = "feat/reputation-merge" } # Serialization serde = { version = "1", features = ["derive"] } diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index d814168cf..307d858d7 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -14,6 +14,7 @@ pub mod nwc; pub mod orders; pub mod push; pub mod reputation; +pub mod reputation_transfer; pub mod restore_progress; pub mod settings; pub mod trade_touch; diff --git a/rust/src/api/nostr.rs b/rust/src/api/nostr.rs index bc5607696..45f1671ca 100644 --- a/rust/src/api/nostr.rs +++ b/rust/src/api/nostr.rs @@ -903,6 +903,9 @@ fn apply_node_capabilities(node: &str, fetched: Result>>> // the person who takes a case over (#637). A fetch without the tag // retracts an older announcement. See mostro::serbero. crate::mostro::serbero::set_from_tags(&mostro_pubkey_hex, &tags); + // Whether it exports or imports reputation; a fetch without the + // tags retracts an older answer. See mostro::reputation_support. + crate::mostro::reputation_support::set_from_tags(&mostro_pubkey_hex, &tags); // The service fee. Only Cashu mode needs it client-side — there the // seller funds the whole fee as its own token — but it rides in the // same event, so reading it here costs nothing. @@ -924,6 +927,7 @@ fn apply_node_capabilities(node: &str, fetched: Result>>> crate::mostro::bond_policy::clear(); // Nor a Serbero: retract an older announcement of this node. crate::mostro::serbero::set_from_tags(&mostro_pubkey_hex, &[]); + crate::mostro::reputation_support::set_from_tags(&mostro_pubkey_hex, &[]); } Err(e) => { log::warn!("[nostr] failed to fetch Kind 38385 for node capabilities: {e}"); @@ -937,6 +941,8 @@ fn apply_node_capabilities(node: &str, fetched: Result>>> // Same reasoning: a stale bond policy would pre-warn (or fail to // pre-warn) for the wrong node. crate::mostro::bond_policy::clear(); + // Nor send reputation requests the node may not understand. + crate::mostro::reputation_support::set_from_tags(&mostro_pubkey_hex, &[]); } } } diff --git a/rust/src/api/orders.rs b/rust/src/api/orders.rs index e5e64df59..dbb8364e2 100644 --- a/rust/src/api/orders.rs +++ b/rust/src/api/orders.rs @@ -9926,13 +9926,13 @@ async fn last_trade_index(sender_keys: &nostr_sdk::prelude::Keys) -> Result u64 { +pub(crate) fn fresh_request_id() -> u64 { use rand::RngCore; rand::rngs::OsRng.next_u64().max(1) } /// How the daemon answered a self-contained request (see [`ask_daemon`]). -enum DaemonAnswer { +pub(crate) enum DaemonAnswer { /// The reply `is_reply` recognised, echoing the request's nonce, and the /// node's timestamp on it. Reply(Box, i64), @@ -9955,7 +9955,37 @@ async fn ask_daemon( label: &str, is_reply: fn(&mostro_core::message::MessageKind, u64) -> bool, ) -> Result { - use crate::rt::time::{timeout, Duration}; + // This query, unlike the restore itself, has a fallback (the payload + // maximum), so a shorter wait halves the worst-case restore latency + // against a silent daemon. + ask_daemon_with( + sender_keys, + mostro_pubkey, + request_id, + event_json, + label, + is_reply, + crate::rt::time::Duration::from_secs(5), + "restore", + ) + .await +} + +/// [`ask_daemon`] with the reply window and the log category the caller +/// chooses: a request the user waits for, with no fallback, can afford a +/// longer window than the restore's. +#[allow(clippy::too_many_arguments)] +pub(crate) async fn ask_daemon_with( + sender_keys: &nostr_sdk::prelude::Keys, + mostro_pubkey: &nostr_sdk::prelude::PublicKey, + request_id: u64, + event_json: &str, + label: &str, + is_reply: fn(&mostro_core::message::MessageKind, u64) -> bool, + reply_window: crate::rt::time::Duration, + log_category: &str, +) -> Result { + use crate::rt::time::timeout; use nostr_sdk::prelude::{ClientNotification, StreamExt}; let mostro_pubkey = *mostro_pubkey; @@ -9968,12 +9998,9 @@ async fn ask_daemon( // arrive in the gap between subscribe and the first recv. let mut rx = client.notifications(); - // This query, unlike the restore itself, has a fallback (the payload - // maximum), so a shorter wait halves the worst-case restore latency - // against a silent daemon. Shared by the relay-side auto-close and the - // outer wait loop — both started at subscribe below, so the two budgets + // `reply_window` is shared by the relay-side auto-close and the outer + // wait loop — both started at subscribe below, so the two budgets // actually run together. - const REPLY_TIMEOUT: Duration = Duration::from_secs(5); // limit(0): live-only, same rationale as subscribe_daemon_messages — the // reply is published after we subscribe, and we never want a replayed @@ -9996,9 +10023,9 @@ async fn ask_daemon( // back to the payload maximum by design). let close_opts = nostr_sdk::prelude::SubscribeAutoCloseOptions::default() .exit_policy(nostr_sdk::prelude::ReqExitPolicy::WaitDurationAfterEOSE( - REPLY_TIMEOUT, + reply_window, )) - .timeout(Some(REPLY_TIMEOUT)); + .timeout(Some(reply_window)); if let Err(e) = client.subscribe(filter).close_on(close_opts).await { log::warn!("[orders] {label} subscribe failed: {e}"); return Ok(DaemonAnswer::Silent); @@ -10008,10 +10035,10 @@ async fn ask_daemon( let start = crate::rt::time::Instant::now(); publish_event_json(event_json).await?; - crate::api::logging::blog_info("restore", format!("{label} published — waiting for daemon")); + crate::api::logging::blog_info(log_category, format!("{label} published — waiting for daemon")); loop { - let remaining = REPLY_TIMEOUT.saturating_sub(start.elapsed()); + let remaining = reply_window.saturating_sub(start.elapsed()); if remaining.is_zero() { break; } diff --git a/rust/src/api/reputation_transfer.rs b/rust/src/api/reputation_transfer.rs new file mode 100644 index 000000000..04c72f97a --- /dev/null +++ b/rust/src/api/reputation_transfer.rs @@ -0,0 +1,348 @@ +//! Reputation portability (MostroP2P/mostro docs/REPUTATION_PORTABILITY.md, +//! phase 5b): export the reputation earned on the active node, import an +//! attestation earned elsewhere, and authorise moving an exported reputation +//! to a new identity. +//! +//! Both requests act on the identity key, so neither runs in privacy mode, +//! and each goes only to a node whose info event advertises it +//! (`mostro::reputation_support`): a node that predates the actions cannot +//! parse them and would never answer. +//! +//! **Errors** are `anyhow` markers Dart localizes (`core/daemon_errors.dart`): +//! `PrivacyModeEnabled`, `ReputationExportUnsupported`, +//! `ReputationImportUnsupported`, `NoDaemonResponse`, and the daemon's or the +//! local check's `CantDoReason` by name (`UntrustedReputationIssuer`, +//! `ReputationAlreadyImported`, `InvalidReputationAttestation`, …). + +use anyhow::{bail, Result}; +use mostro_core::message::{Action, MessageKind, Payload, ReputationExportRequest}; +use mostro_core::reputation::{ + AttestationError, ReputationAttestation, ReputationRebind, ATTESTATION_LIFETIME_SECS, + REBIND_MAX_LIFETIME_SECS, +}; +use nostr_sdk::prelude::{Keys, PublicKey, Timestamp}; + +use crate::api::orders::{ask_daemon_with, fresh_request_id, DaemonAnswer}; +use crate::db::settings_keys::PENDING_REPUTATION_ATTESTATION; +use crate::db::Storage; +use crate::mostro::reputation_support; + +/// How long to wait for the node's answer. The user is waiting and there is +/// no fallback, so the window is longer than the restore's. +const REPLY_WINDOW_SECS: u64 = 15; + +/// What the active node offers for reputation portability. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReputationSupportInfo { + /// The key the node signs attestations with; `None` when it does not + /// export. + pub issuer: Option, + /// The issuer keys it imports from; `None` when it does not import. + pub import_issuers: Option>, +} + +/// A verified reputation attestation, for display and import. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReputationAttestationInfo { + pub id: String, + /// Key that signed it. + pub issuer: String, + /// Identity it is addressed to. + pub destination: String, + /// The source account, opaque. + pub subject: String, + /// Ratings received on the source. + pub reviews: u32, + /// Their average, with two decimals (`"4.87"`). + pub rating: String, + /// UTC day start of the first completed trade on the source. + pub since: i64, + pub created_at: i64, + pub expiration: i64, + /// The event JSON, as received, to import unchanged. + pub json: String, +} + +impl ReputationAttestationInfo { + fn new(attestation: &ReputationAttestation, json: String) -> Self { + Self { + id: attestation.id.to_hex(), + issuer: attestation.issuer.to_hex(), + destination: attestation.destination.to_hex(), + subject: attestation.subject.clone(), + reviews: attestation.reviews, + rating: attestation.rating_text(), + since: attestation.since as i64, + created_at: attestation.created_at as i64, + expiration: attestation.expiration as i64, + json, + } + } +} + +/// What the active node advertises, as of its last capability fetch. +#[flutter_rust_bridge::frb(sync)] +pub fn get_reputation_support() -> ReputationSupportInfo { + let support = reputation_support::get(&crate::config::active_mostro_pubkey()); + ReputationSupportInfo { + issuer: support.issuer, + import_issuers: support.import_issuers, + } +} + +/// Verify an attestation pasted or received, without sending anything: the +/// event's own rules (signature, kind, tags, clock). Whether the node trusts +/// its issuer is the node's to say on import. +#[flutter_rust_bridge::frb(sync)] +pub fn parse_reputation_attestation(json: String) -> Result { + let attestation = parse(&json)?; + Ok(ReputationAttestationInfo::new(&attestation, json)) +} + +/// Ask the active node to attest the user's reputation there for +/// `destination` (the user's own identity when `None`). The UI confirms the +/// destination with the user first: the request binds the node's account to +/// it. `rebind` is an authorisation from [`sign_reputation_rebind`], needed +/// when the account is bound to another identity. The attestation is +/// verified, kept until imported, and returned. +pub async fn export_reputation( + destination: Option, + rebind: Option, +) -> Result { + let node = crate::config::active_mostro_pubkey(); + let Some(issuer) = reputation_support::get(&node).issuer else { + bail!("ReputationExportUnsupported: the node does not export reputation"); + }; + let identity = identity_keys().await?; + let target = match destination { + Some(hex) => PublicKey::from_hex(hex.trim())?.to_hex(), + None => identity.public_key().to_hex(), + }; + let payload = Payload::ReputationExportRequest(ReputationExportRequest { + destination: target.clone(), + rebind, + }); + let reply = ask( + &node, + &identity, + Action::ExportReputation, + payload, + is_export_reply, + ) + .await?; + let Some(Payload::ReputationAttestation(json)) = reply.payload else { + bail!("InvalidPayload: the node answered without an attestation"); + }; + let attestation = parse(&json)?; + if attestation.issuer.to_hex() != issuer || attestation.destination.to_hex() != target { + bail!("InvalidReputationAttestation: not the node's issuer key, or another identity"); + } + if let Some(db) = crate::db::app_db::db() { + db.set_setting(PENDING_REPUTATION_ATTESTATION, &json) + .await?; + } + Ok(ReputationAttestationInfo::new(&attestation, json)) +} + +/// Import `attestation_json` into the active node. It must name the user's +/// identity and be signed by a key the node advertises it trusts; the node +/// runs the full checks and answers `reputation-imported` or a refusal. +pub async fn import_reputation(attestation_json: String) -> Result { + let node = crate::config::active_mostro_pubkey(); + let Some(trusted) = reputation_support::get(&node).import_issuers else { + bail!("ReputationImportUnsupported: the node does not import reputation"); + }; + let identity = identity_keys().await?; + let attestation = parse(&attestation_json)?; + if attestation.destination != identity.public_key() { + bail!("ReputationIdentityMismatch: it names another identity"); + } + if !trusted.contains(&attestation.issuer.to_hex()) { + bail!("UntrustedReputationIssuer: the node does not trust its issuer"); + } + ask( + &node, + &identity, + Action::ImportReputation, + Payload::ReputationAttestation(attestation_json.clone()), + is_import_reply, + ) + .await?; + if let Some(db) = crate::db::app_db::db() { + if db + .get_setting(PENDING_REPUTATION_ATTESTATION) + .await? + .as_deref() + == Some(attestation_json.as_str()) + { + db.delete_setting(PENDING_REPUTATION_ATTESTATION).await?; + } + } + Ok(ReputationAttestationInfo::new( + &attestation, + attestation_json, + )) +} + +/// The attestation exported last and not imported yet, while still valid. +pub async fn get_pending_reputation_attestation() -> Result> { + let Some(db) = crate::db::app_db::db() else { + return Ok(None); + }; + let Some(json) = db.get_setting(PENDING_REPUTATION_ATTESTATION).await? else { + return Ok(None); + }; + Ok(parse(&json) + .ok() + .map(|attestation| ReputationAttestationInfo::new(&attestation, json))) +} + +/// Sign, with the identity the reputation at `issuer` is bound to now, the +/// authorisation to move that binding to `new_identity` (hex). Pass it as +/// `rebind` to [`export_reputation`] from the new identity, or paste it into +/// lnp2pBot. Valid for an hour. +pub async fn sign_reputation_rebind(issuer: String, new_identity: String) -> Result { + let bound = identity_keys().await?; + let issuer = PublicKey::from_hex(issuer.trim())?; + let new_identity = PublicKey::parse(new_identity.trim())?; + let event = ReputationRebind::build( + &bound, + &issuer, + &new_identity, + Timestamp::now(), + REBIND_MAX_LIFETIME_SECS, + ) + .map_err(|e| anyhow::anyhow!("InvalidReputationRebind: {e}"))?; + Ok(event.as_json()) +} + +/// The identity keys, refusing in privacy mode: there is no identity-bound +/// reputation to export or import into. +async fn identity_keys() -> Result { + if crate::api::reputation::get_privacy_mode() { + bail!("PrivacyModeEnabled: reputation portability needs the identity key"); + } + crate::api::identity::get_active_keys().await +} + +fn parse(json: &str) -> Result { + ReputationAttestation::parse_json(json, Timestamp::now(), ATTESTATION_LIFETIME_SECS) + .map(|(attestation, _)| attestation) + .map_err(refusal) +} + +/// An attestation the local check refuses, named by the `cant-do` reason a +/// destination would answer with. +fn refusal(error: AttestationError) -> anyhow::Error { + anyhow::anyhow!("{:?}: {error}", error.cant_do_reason()) +} + +fn is_export_reply(kind: &MessageKind, request_id: u64) -> bool { + kind.action == Action::ReputationExported && kind.request_id == Some(request_id) +} + +fn is_import_reply(kind: &MessageKind, request_id: u64) -> bool { + kind.action == Action::ReputationImported && kind.request_id == Some(request_id) +} + +/// Send one reputation request from a fresh trade key with the identity +/// proof, and wait for the node's answer to it. +async fn ask( + node: &str, + identity: &Keys, + action: Action, + payload: Payload, + is_reply: fn(&MessageKind, u64) -> bool, +) -> Result { + let mostro_pubkey = PublicKey::from_hex(node)?; + let trade_key_info = crate::api::identity::derive_trade_key().await?; + let trade_keys = crate::api::identity::get_active_trade_keys(trade_key_info.index).await?; + let request_id = fresh_request_id(); + let label = format!("{action:?}"); + let event_json = crate::mostro::actions::reputation_request( + identity, + &trade_keys, + &mostro_pubkey, + request_id, + action, + payload, + ) + .await?; + let answer = ask_daemon_with( + &trade_keys, + &mostro_pubkey, + request_id, + &event_json, + &label, + is_reply, + crate::rt::time::Duration::from_secs(REPLY_WINDOW_SECS), + "reputation", + ) + .await?; + match answer { + DaemonAnswer::Reply(kind, _) => Ok(*kind), + DaemonAnswer::Refused(reason) => bail!("{reason}: refused by the node"), + DaemonAnswer::Silent => bail!(crate::mostro::pending::NO_DAEMON_RESPONSE), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const VECTORS: &str = include_str!("../../tests/fixtures/reputation_v1.json"); + + fn vectors() -> serde_json::Value { + serde_json::from_str(VECTORS).unwrap() + } + + #[test] + fn a_refused_attestation_is_named_by_its_cant_do_reason() { + let v = vectors(); + for case in v["attestation"]["invalid"].as_array().unwrap() { + let name = case["name"].as_str().unwrap(); + if ["identity_mismatch", "untrusted_issuer", "own_issuer_key"].contains(&name) { + continue; + } + let event = nostr_sdk::prelude::Event::from_json(case["event"].to_string()).unwrap(); + let now = Timestamp::from(v["context"]["now"].as_u64().unwrap()); + let error = ReputationAttestation::parse(&event, now, ATTESTATION_LIFETIME_SECS) + .map_err(refusal) + .unwrap_err() + .to_string(); + let expected = match case["reason"].as_str().unwrap() { + "expired_reputation_attestation" => "ExpiredReputationAttestation", + _ => "InvalidReputationAttestation", + }; + assert!(error.starts_with(expected), "{name}: {error}"); + } + } + + #[test] + fn the_replies_are_matched_by_action_and_nonce() { + let exported = MessageKind::new(None, Some(9), None, Action::ReputationExported, None); + assert!(is_export_reply(&exported, 9)); + assert!(!is_export_reply(&exported, 8)); + assert!(!is_import_reply(&exported, 9)); + let imported = MessageKind::new(None, Some(9), None, Action::ReputationImported, None); + assert!(is_import_reply(&imported, 9)); + } + + #[test] + fn the_info_keeps_the_figures_and_the_json() { + let v = vectors(); + let json = v["attestation"]["valid"]["json"] + .as_str() + .unwrap() + .to_string(); + let now = Timestamp::from(v["context"]["now"].as_u64().unwrap()); + let (attestation, _) = + ReputationAttestation::parse_json(&json, now, ATTESTATION_LIFETIME_SECS).unwrap(); + let info = ReputationAttestationInfo::new(&attestation, json.clone()); + assert_eq!(info.reviews, 214); + assert_eq!(info.rating, "4.87"); + assert_eq!(info.since, 1_696_204_800); + assert_eq!(info.json, json); + assert_eq!(info.id, v["attestation"]["valid"]["id"].as_str().unwrap()); + } +} diff --git a/rust/src/db/indexeddb.rs b/rust/src/db/indexeddb.rs index a8df4ab20..caff663a8 100644 --- a/rust/src/db/indexeddb.rs +++ b/rust/src/db/indexeddb.rs @@ -527,6 +527,7 @@ impl Storage for IndexedDbStorage { .any(|prefix| key.starts_with(prefix)) || key == settings_keys::BOND_CLAIM_RETAINED_NODES || key == settings_keys::RESTORE_SNAPSHOT + || key == settings_keys::PENDING_REPUTATION_ATTESTATION }) .collect() }; diff --git a/rust/src/db/mod.rs b/rust/src/db/mod.rs index 6a88e3e9d..1b6a96d8d 100644 --- a/rust/src/db/mod.rs +++ b/rust/src/db/mod.rs @@ -40,6 +40,12 @@ pub mod settings_keys { /// history (#614). pub const RESTORE_SNAPSHOT: &str = "restore_snapshot"; + /// A reputation attestation the identity exported and has not imported + /// yet, as the event JSON it came in (`api::reputation_transfer`). + /// Identity-scoped: it names the identity that requested it, and is of + /// no use to the next one. + pub const PENDING_REPUTATION_ATTESTATION: &str = "pending_reputation_attestation"; + // ── Push notifications (docs/PUSH_NOTIFICATIONS.md §7.1, §8.1) ────────── /// The master toggle, `"true"` / `"false"`; absent reads as enabled. @@ -212,8 +218,8 @@ pub mod settings_keys { pub const TRADE_WIPED_PREFIX: &str = "trade_wiped:"; /// Every per-order key family above. The single identity-scoped keys — - /// [`BOND_CLAIM_RETAINED_NODES`] and [`RESTORE_SNAPSHOT`] — are dropped - /// by name next to them. + /// [`BOND_CLAIM_RETAINED_NODES`], [`RESTORE_SNAPSHOT`] and + /// [`PENDING_REPUTATION_ATTESTATION`] — are dropped by name next to them. /// All of it describes trades of the identity that wrote it, so /// [`super::Storage::clear_identity_data`] drops it with the rows. What /// is left in the store is device preference: the active node, custom diff --git a/rust/src/db/sqlite.rs b/rust/src/db/sqlite.rs index 6eee3fc93..510d7dbeb 100644 --- a/rust/src/db/sqlite.rs +++ b/rust/src/db/sqlite.rs @@ -556,6 +556,7 @@ impl Storage for SqliteStorage { for key in [ settings_keys::BOND_CLAIM_RETAINED_NODES, settings_keys::RESTORE_SNAPSHOT, + settings_keys::PENDING_REPUTATION_ATTESTATION, ] { sqlx::query("DELETE FROM settings WHERE key = ?") .bind(key) @@ -2429,6 +2430,7 @@ mod tests { settings_keys::trade_wiped("order-a"), settings_keys::BOND_CLAIM_RETAINED_NODES.to_string(), settings_keys::RESTORE_SNAPSHOT.to_string(), + settings_keys::PENDING_REPUTATION_ATTESTATION.to_string(), ] { storage.set_setting(&key, "1").await.unwrap(); } @@ -2462,6 +2464,7 @@ mod tests { settings_keys::trade_wiped("order-a"), settings_keys::BOND_CLAIM_RETAINED_NODES.to_string(), settings_keys::RESTORE_SNAPSHOT.to_string(), + settings_keys::PENDING_REPUTATION_ATTESTATION.to_string(), ] { assert_eq!( storage.get_setting(&key).await.unwrap(), diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 19b6a5329..47fa9e705 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -52,7 +52,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1351531358; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -3555436; // Section: executor @@ -3086,6 +3086,47 @@ fn wire__crate__api__identity__export_encrypted_backup_impl( }, ) } +fn wire__crate__api__reputation_transfer__export_reputation_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "export_reputation", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_destination = >::sse_decode(&mut deserializer); + let api_rebind = >::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::reputation_transfer::export_reputation( + api_destination, + api_rebind, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__nostr__fetch_exchange_rate_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3832,6 +3873,43 @@ fn wire__crate__api__orders__get_orders_impl( }, ) } +fn wire__crate__api__reputation_transfer__get_pending_reputation_attestation_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_pending_reputation_attestation", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::reputation_transfer::get_pending_reputation_attestation() + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__reputation__get_privacy_mode_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -3972,6 +4050,36 @@ fn wire__crate__api__nostr__get_relays_impl( }, ) } +fn wire__crate__api__reputation_transfer__get_reputation_support_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_reputation_support", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + deserializer.end(); + transform_result_sse::<_, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::reputation_transfer::get_reputation_support())?; + Ok(output_ok) + })()) + }, + ) +} fn wire__crate__api__settings__get_settings_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -4382,6 +4490,45 @@ fn wire__crate__api__identity__import_from_nsec_impl( }, ) } +fn wire__crate__api__reputation_transfer__import_reputation_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "import_reputation", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_attestation_json = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::reputation_transfer::import_reputation( + api_attestation_json, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__init_db_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -5644,6 +5791,39 @@ fn wire__crate__api__orders__order_filters_default_impl( }, ) } +fn wire__crate__api__reputation_transfer__parse_reputation_attestation_impl( + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "parse_reputation_attestation", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_json = ::sse_decode(&mut deserializer); + deserializer.end(); + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = + crate::api::reputation_transfer::parse_reputation_attestation(api_json)?; + Ok(output_ok) + })(), + ) + }, + ) +} fn wire__crate__api__nwc__pay_invoice_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -6858,6 +7038,47 @@ fn wire__crate__api__settings__set_theme_impl( }, ) } +fn wire__crate__api__reputation_transfer__sign_reputation_rebind_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "sign_reputation_rebind", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_issuer = ::sse_decode(&mut deserializer); + let api_new_identity = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::reputation_transfer::sign_reputation_rebind( + api_issuer, + api_new_identity, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__disputes__solver_role_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -8982,6 +9203,21 @@ impl SseDecode for Option { } } +impl SseDecode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some( + ::sse_decode( + deserializer, + ), + ); + } else { + return None; + } + } +} + impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -9413,6 +9649,46 @@ impl SseDecode for crate::api::types::RelayStatus { } } +impl SseDecode for crate::api::reputation_transfer::ReputationAttestationInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_issuer = ::sse_decode(deserializer); + let mut var_destination = ::sse_decode(deserializer); + let mut var_subject = ::sse_decode(deserializer); + let mut var_reviews = ::sse_decode(deserializer); + let mut var_rating = ::sse_decode(deserializer); + let mut var_since = ::sse_decode(deserializer); + let mut var_createdAt = ::sse_decode(deserializer); + let mut var_expiration = ::sse_decode(deserializer); + let mut var_json = ::sse_decode(deserializer); + return crate::api::reputation_transfer::ReputationAttestationInfo { + id: var_id, + issuer: var_issuer, + destination: var_destination, + subject: var_subject, + reviews: var_reviews, + rating: var_rating, + since: var_since, + created_at: var_createdAt, + expiration: var_expiration, + json: var_json, + }; + } +} + +impl SseDecode for crate::api::reputation_transfer::ReputationSupportInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_issuer = >::sse_decode(deserializer); + let mut var_importIssuers = >>::sse_decode(deserializer); + return crate::api::reputation_transfer::ReputationSupportInfo { + issuer: var_issuer, + import_issuers: var_importIssuers, + }; + } +} + impl SseDecode for crate::api::types::RestoreProgress { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -9923,304 +10199,328 @@ fn pde_ffi_dispatcher_primary_impl( rust_vec_len, data_len, ), - 67 => wire__crate__api__nostr__fetch_exchange_rate_impl(port, ptr, rust_vec_len, data_len), - 68 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( + 67 => wire__crate__api__reputation_transfer__export_reputation_impl( port, ptr, rust_vec_len, data_len, ), - 69 => wire__crate__api__node_stats__fetch_mostro_node_stats_impl( + 68 => wire__crate__api__nostr__fetch_exchange_rate_impl(port, ptr, rust_vec_len, data_len), + 69 => wire__crate__api__nostr__fetch_mostro_instance_tags_impl( port, ptr, rust_vec_len, data_len, ), - 70 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), - 71 => wire__crate__api__identity__funds_at_risk_impl(port, ptr, rust_vec_len, data_len), - 72 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), - 73 => wire__crate__api__messages__get_attachment_status_impl( + 70 => wire__crate__api__node_stats__fetch_mostro_node_stats_impl( port, ptr, rust_vec_len, data_len, ), - 74 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), - 75 => wire__crate__api__bond__get_bond_claim_impl(port, ptr, rust_vec_len, data_len), - 76 => wire__crate__api__bond__get_bond_claim_from_impl(port, ptr, rust_vec_len, data_len), - 77 => wire__crate__api__bond__get_bond_policy_impl(port, ptr, rust_vec_len, data_len), - 78 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), - 79 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), - 80 => wire__crate__api__escrow__get_escrow_mode_impl(port, ptr, rust_vec_len, data_len), - 81 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), - 82 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), - 83 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), - 84 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), - 85 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), - 86 => wire__crate__api__orders__get_order_book_snapshot_impl( + 71 => wire__crate__api__nostr__flush_message_queue_impl(port, ptr, rust_vec_len, data_len), + 72 => wire__crate__api__identity__funds_at_risk_impl(port, ptr, rust_vec_len, data_len), + 73 => wire__crate__api__get_app_version_impl(port, ptr, rust_vec_len, data_len), + 74 => wire__crate__api__messages__get_attachment_status_impl( port, ptr, rust_vec_len, data_len, ), - 87 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), - 88 => { + 75 => wire__crate__api__nwc__get_balance_impl(port, ptr, rust_vec_len, data_len), + 76 => wire__crate__api__bond__get_bond_claim_impl(port, ptr, rust_vec_len, data_len), + 77 => wire__crate__api__bond__get_bond_claim_from_impl(port, ptr, rust_vec_len, data_len), + 78 => wire__crate__api__bond__get_bond_policy_impl(port, ptr, rust_vec_len, data_len), + 79 => wire__crate__api__nostr__get_connection_state_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__crate__api__disputes__get_dispute_impl(port, ptr, rust_vec_len, data_len), + 81 => wire__crate__api__escrow__get_escrow_mode_impl(port, ptr, rust_vec_len, data_len), + 82 => wire__crate__api__identity__get_identity_impl(port, ptr, rust_vec_len, data_len), + 83 => wire__crate__api__messages__get_messages_impl(port, ptr, rust_vec_len, data_len), + 84 => wire__crate__api__settings__get_mostro_pubkey_impl(port, ptr, rust_vec_len, data_len), + 85 => wire__crate__api__identity__get_nym_identity_impl(port, ptr, rust_vec_len, data_len), + 86 => wire__crate__api__orders__get_order_impl(port, ptr, rust_vec_len, data_len), + 87 => wire__crate__api__orders__get_order_book_snapshot_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 88 => wire__crate__api__orders__get_orders_impl(port, ptr, rust_vec_len, data_len), + 89 => wire__crate__api__reputation_transfer__get_pending_reputation_attestation_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 90 => { wire__crate__api__reputation__get_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 89 => wire__crate__api__push__get_push_status_impl(port, ptr, rust_vec_len, data_len), - 90 => wire__crate__api__reputation__get_rating_for_trade_impl( + 91 => wire__crate__api__push__get_push_status_impl(port, ptr, rust_vec_len, data_len), + 92 => wire__crate__api__reputation__get_rating_for_trade_impl( port, ptr, rust_vec_len, data_len, ), - 91 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), - 92 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), - 93 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), - 94 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), - 95 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), - 96 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), - 97 => wire__crate__api__disputes__handle_admin_canceled_impl( + 93 => wire__crate__api__nostr__get_relays_impl(port, ptr, rust_vec_len, data_len), + 95 => wire__crate__api__settings__get_settings_impl(port, ptr, rust_vec_len, data_len), + 96 => wire__crate__api__identity__get_trade_key_impl(port, ptr, rust_vec_len, data_len), + 97 => wire__crate__api__orders__get_trade_role_impl(port, ptr, rust_vec_len, data_len), + 98 => wire__crate__api__messages__get_unread_count_impl(port, ptr, rust_vec_len, data_len), + 99 => wire__crate__api__nwc__get_wallet_impl(port, ptr, rust_vec_len, data_len), + 100 => wire__crate__api__disputes__handle_admin_canceled_impl( port, ptr, rust_vec_len, data_len, ), - 98 => { + 101 => { wire__crate__api__disputes__handle_admin_settled_impl(port, ptr, rust_vec_len, data_len) } - 99 => wire__crate__api__disputes__handle_admin_took_dispute_impl( + 102 => wire__crate__api__disputes__handle_admin_took_dispute_impl( port, ptr, rust_vec_len, data_len, ), - 100 => wire__crate__api__reputation__handle_rating_received_impl( + 103 => wire__crate__api__reputation__handle_rating_received_impl( port, ptr, rust_vec_len, data_len, ), - 101 => { + 104 => { wire__crate__api__identity__import_from_mnemonic_impl(port, ptr, rust_vec_len, data_len) } - 102 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), - 103 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), - 104 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), - 105 => { + 105 => wire__crate__api__identity__import_from_nsec_impl(port, ptr, rust_vec_len, data_len), + 106 => wire__crate__api__reputation_transfer__import_reputation_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 107 => wire__crate__api__init_db_impl(port, ptr, rust_vec_len, data_len), + 108 => wire__crate__api__nostr__initialize_impl(port, ptr, rust_vec_len, data_len), + 109 => { wire__crate__api__logging__install_log_bridge_impl(port, ptr, rust_vec_len, data_len) } - 106 => wire__crate__api__bond__list_bond_claims_impl(port, ptr, rust_vec_len, data_len), - 107 => wire__crate__api__nodes__list_mostro_nodes_impl(port, ptr, rust_vec_len, data_len), - 108 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), - 109 => wire__crate__api__identity__load_identity_from_mnemonic_impl( + 110 => wire__crate__api__bond__list_bond_claims_impl(port, ptr, rust_vec_len, data_len), + 111 => wire__crate__api__nodes__list_mostro_nodes_impl(port, ptr, rust_vec_len, data_len), + 112 => wire__crate__api__orders__list_trades_impl(port, ptr, rust_vec_len, data_len), + 113 => wire__crate__api__identity__load_identity_from_mnemonic_impl( port, ptr, rust_vec_len, data_len, ), - 110 => wire__crate__api__cashu__lock_escrow_impl(port, ptr, rust_vec_len, data_len), - 111 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), - 112 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), - 113 => { + 114 => wire__crate__api__cashu__lock_escrow_impl(port, ptr, rust_vec_len, data_len), + 115 => wire__crate__api__nwc__make_invoice_impl(port, ptr, rust_vec_len, data_len), + 116 => wire__crate__api__messages__mark_as_read_impl(port, ptr, rust_vec_len, data_len), + 117 => { wire__crate__api__nodes__node_metadata_default_impl(port, ptr, rust_vec_len, data_len) } - 114 => { + 118 => { wire__crate__api__messages__on_any_new_message_impl(port, ptr, rust_vec_len, data_len) } - 115 => wire__crate__api__messages__on_attachment_progress_impl( + 119 => wire__crate__api__messages__on_attachment_progress_impl( port, ptr, rust_vec_len, data_len, ), - 116 => { + 120 => { wire__crate__api__bond__on_bond_claim_updated_impl(port, ptr, rust_vec_len, data_len) } - 117 => wire__crate__api__bond__on_bond_slashed_impl(port, ptr, rust_vec_len, data_len), - 118 => { + 121 => wire__crate__api__bond__on_bond_slashed_impl(port, ptr, rust_vec_len, data_len), + 122 => { wire__crate__api__cashu__on_cashu_wallet_changed_impl(port, ptr, rust_vec_len, data_len) } - 119 => wire__crate__api__nostr__on_connection_state_changed_impl( + 123 => wire__crate__api__nostr__on_connection_state_changed_impl( port, ptr, rust_vec_len, data_len, ), - 120 => { + 124 => { wire__crate__api__disputes__on_dispute_updated_impl(port, ptr, rust_vec_len, data_len) } - 121 => { + 125 => { wire__crate__api__escrow__on_escrow_mode_changed_impl(port, ptr, rust_vec_len, data_len) } - 122 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), - 123 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), - 124 => wire__crate__api__orders__on_order_deltas_impl(port, ptr, rust_vec_len, data_len), - 125 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), - 126 => { + 126 => wire__crate__api__logging__on_log_entry_impl(port, ptr, rust_vec_len, data_len), + 127 => wire__crate__api__messages__on_new_message_impl(port, ptr, rust_vec_len, data_len), + 128 => wire__crate__api__orders__on_order_deltas_impl(port, ptr, rust_vec_len, data_len), + 129 => wire__crate__api__orders__on_orders_updated_impl(port, ptr, rust_vec_len, data_len), + 130 => { wire__crate__api__push__on_push_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 127 => { + 131 => { wire__crate__api__reputation__on_rating_received_impl(port, ptr, rust_vec_len, data_len) } - 128 => { + 132 => { wire__crate__api__nostr__on_relay_auto_synced_impl(port, ptr, rust_vec_len, data_len) } - 129 => { + 133 => { wire__crate__api__nostr__on_relay_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 130 => wire__crate__api__restore_progress__on_restore_progress_impl( + 134 => wire__crate__api__restore_progress__on_restore_progress_impl( port, ptr, rust_vec_len, data_len, ), - 131 => { + 135 => { wire__crate__api__settings__on_settings_changed_impl(port, ptr, rust_vec_len, data_len) } - 132 => wire__crate__api__identity__on_trade_key_index_changed_impl( + 136 => wire__crate__api__identity__on_trade_key_index_changed_impl( port, ptr, rust_vec_len, data_len, ), - 133 => { + 137 => { wire__crate__api__trade_touch__on_trade_touched_impl(port, ptr, rust_vec_len, data_len) } - 134 => wire__crate__api__orders__on_trade_updated_impl(port, ptr, rust_vec_len, data_len), - 135 => wire__crate__api__messages__on_unread_count_changed_impl( + 138 => wire__crate__api__orders__on_trade_updated_impl(port, ptr, rust_vec_len, data_len), + 139 => wire__crate__api__messages__on_unread_count_changed_impl( port, ptr, rust_vec_len, data_len, ), - 136 => { + 140 => { wire__crate__api__nwc__on_wallet_status_changed_impl(port, ptr, rust_vec_len, data_len) } - 137 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), - 138 => { + 141 => wire__crate__api__disputes__open_dispute_impl(port, ptr, rust_vec_len, data_len), + 142 => { wire__crate__api__orders__order_filters_default_impl(port, ptr, rust_vec_len, data_len) } - 139 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), - 140 => { + 144 => wire__crate__api__nwc__pay_invoice_impl(port, ptr, rust_vec_len, data_len), + 145 => { wire__crate__api__types__push_platform_as_wire_impl(port, ptr, rust_vec_len, data_len) } - 141 => { + 146 => { wire__crate__api__types__push_platform_from_wire_impl(port, ptr, rust_vec_len, data_len) } - 142 => wire__crate__api__logging__recent_logs_impl(port, ptr, rust_vec_len, data_len), - 143 => wire__crate__api__push__reconcile_push_impl(port, ptr, rust_vec_len, data_len), - 144 => wire__crate__api__orders__recover_trades_impl(port, ptr, rust_vec_len, data_len), - 145 => wire__crate__api__node_stats__refresh_mostro_node_info_cache_impl( + 147 => wire__crate__api__logging__recent_logs_impl(port, ptr, rust_vec_len, data_len), + 148 => wire__crate__api__push__reconcile_push_impl(port, ptr, rust_vec_len, data_len), + 149 => wire__crate__api__orders__recover_trades_impl(port, ptr, rust_vec_len, data_len), + 150 => wire__crate__api__node_stats__refresh_mostro_node_info_cache_impl( port, ptr, rust_vec_len, data_len, ), - 146 => wire__crate__api__nodes__refresh_mostro_node_metadata_impl( + 151 => wire__crate__api__nodes__refresh_mostro_node_metadata_impl( port, ptr, rust_vec_len, data_len, ), - 147 => wire__crate__api__settings__rehydrate_active_mostro_node_impl( + 152 => wire__crate__api__settings__rehydrate_active_mostro_node_impl( port, ptr, rust_vec_len, data_len, ), - 148 => wire__crate__api__escrow__rehydrate_escrow_overrides_impl( + 153 => wire__crate__api__escrow__rehydrate_escrow_overrides_impl( port, ptr, rust_vec_len, data_len, ), - 149 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), - 150 => wire__crate__api__nodes__remove_custom_mostro_node_impl( + 154 => wire__crate__api__orders__release_order_impl(port, ptr, rust_vec_len, data_len), + 155 => wire__crate__api__nodes__remove_custom_mostro_node_impl( port, ptr, rust_vec_len, data_len, ), - 151 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), - 152 => wire__crate__api__orders__request_bond_invoice_again_impl( + 156 => wire__crate__api__nostr__remove_relay_impl(port, ptr, rust_vec_len, data_len), + 157 => wire__crate__api__orders__request_bond_invoice_again_impl( port, ptr, rust_vec_len, data_len, ), - 153 => wire__crate__api__orders__restart_orders_subscription_impl( + 158 => wire__crate__api__orders__restart_orders_subscription_impl( port, ptr, rust_vec_len, data_len, ), - 154 => wire__crate__api__nostr__resync_impl(port, ptr, rust_vec_len, data_len), - 155 => { + 159 => wire__crate__api__nostr__resync_impl(port, ptr, rust_vec_len, data_len), + 160 => { wire__crate__api__disputes__send_dispute_file_impl(port, ptr, rust_vec_len, data_len) } - 156 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), - 157 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), - 158 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), - 159 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), - 160 => wire__crate__api__settings__set_active_mostro_node_impl( + 161 => wire__crate__api__orders__send_fiat_sent_impl(port, ptr, rust_vec_len, data_len), + 162 => wire__crate__api__messages__send_file_impl(port, ptr, rust_vec_len, data_len), + 163 => wire__crate__api__orders__send_invoice_impl(port, ptr, rust_vec_len, data_len), + 164 => wire__crate__api__messages__send_message_impl(port, ptr, rust_vec_len, data_len), + 165 => wire__crate__api__settings__set_active_mostro_node_impl( port, ptr, rust_vec_len, data_len, ), - 161 => wire__crate__api__escrow__set_cashu_mint_url_override_impl( + 166 => wire__crate__api__escrow__set_cashu_mint_url_override_impl( port, ptr, rust_vec_len, data_len, ), - 162 => wire__crate__api__settings__set_default_fiat_code_impl( + 167 => wire__crate__api__settings__set_default_fiat_code_impl( port, ptr, rust_vec_len, data_len, ), - 163 => wire__crate__api__settings__set_default_lightning_address_impl( + 168 => wire__crate__api__settings__set_default_lightning_address_impl( port, ptr, rust_vec_len, data_len, ), - 164 => wire__crate__api__escrow__set_escrow_mode_override_impl( + 169 => wire__crate__api__escrow__set_escrow_mode_override_impl( port, ptr, rust_vec_len, data_len, ), - 165 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), - 166 => { + 170 => wire__crate__api__settings__set_language_impl(port, ptr, rust_vec_len, data_len), + 171 => { wire__crate__api__settings__set_logging_enabled_impl(port, ptr, rust_vec_len, data_len) } - 167 => { + 172 => { wire__crate__api__reputation__set_privacy_mode_impl(port, ptr, rust_vec_len, data_len) } - 168 => wire__crate__api__push__set_push_enabled_impl(port, ptr, rust_vec_len, data_len), - 169 => wire__crate__api__push__set_push_token_impl(port, ptr, rust_vec_len, data_len), - 170 => wire__crate__api__settings__set_test_order_expiry_impl( + 173 => wire__crate__api__push__set_push_enabled_impl(port, ptr, rust_vec_len, data_len), + 174 => wire__crate__api__push__set_push_token_impl(port, ptr, rust_vec_len, data_len), + 175 => wire__crate__api__settings__set_test_order_expiry_impl( port, ptr, rust_vec_len, data_len, ), - 171 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), - 172 => wire__crate__api__disputes__solver_role_impl(port, ptr, rust_vec_len, data_len), - 173 => wire__crate__api__bond__submit_bond_payout_invoice_impl( + 176 => wire__crate__api__settings__set_theme_impl(port, ptr, rust_vec_len, data_len), + 177 => wire__crate__api__reputation_transfer__sign_reputation_rebind_impl( port, ptr, rust_vec_len, data_len, ), - 174 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), - 175 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), - 176 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), - 177 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), - 178 => { + 178 => wire__crate__api__disputes__solver_role_impl(port, ptr, rust_vec_len, data_len), + 179 => wire__crate__api__bond__submit_bond_payout_invoice_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 180 => wire__crate__api__disputes__submit_evidence_impl(port, ptr, rust_vec_len, data_len), + 181 => wire__crate__api__reputation__submit_rating_impl(port, ptr, rust_vec_len, data_len), + 182 => wire__crate__api__orders__subscribe_orders_impl(port, ptr, rust_vec_len, data_len), + 183 => wire__crate__api__orders__take_order_impl(port, ptr, rust_vec_len, data_len), + 184 => { wire__crate__api__invoice__trade_step_started_at_impl(port, ptr, rust_vec_len, data_len) } _ => unreachable!(), @@ -10235,6 +10535,16 @@ fn pde_ffi_dispatcher_sync_impl( ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { // Codec=Pde (Serialization + dispatch), see doc to use other codecs match func_id { + 94 => wire__crate__api__reputation_transfer__get_reputation_support_impl( + ptr, + rust_vec_len, + data_len, + ), + 143 => wire__crate__api__reputation_transfer__parse_reputation_attestation_impl( + ptr, + rust_vec_len, + data_len, + ), _ => unreachable!(), } } @@ -12014,6 +12324,56 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::reputation_transfer::ReputationAttestationInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.issuer.into_into_dart().into_dart(), + self.destination.into_into_dart().into_dart(), + self.subject.into_into_dart().into_dart(), + self.reviews.into_into_dart().into_dart(), + self.rating.into_into_dart().into_dart(), + self.since.into_into_dart().into_dart(), + self.created_at.into_into_dart().into_dart(), + self.expiration.into_into_dart().into_dart(), + self.json.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::reputation_transfer::ReputationAttestationInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::reputation_transfer::ReputationAttestationInfo +{ + fn into_into_dart(self) -> crate::api::reputation_transfer::ReputationAttestationInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::reputation_transfer::ReputationSupportInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.issuer.into_into_dart().into_dart(), + self.import_issuers.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::reputation_transfer::ReputationSupportInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::reputation_transfer::ReputationSupportInfo +{ + fn into_into_dart(self) -> crate::api::reputation_transfer::ReputationSupportInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::types::RestoreProgress { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { match self { @@ -13882,6 +14242,18 @@ impl SseEncode for Option { } } +impl SseEncode for Option { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + ::sse_encode( + value, serializer, + ); + } + } +} + impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -14243,6 +14615,30 @@ impl SseEncode for crate::api::types::RelayStatus { } } +impl SseEncode for crate::api::reputation_transfer::ReputationAttestationInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.issuer, serializer); + ::sse_encode(self.destination, serializer); + ::sse_encode(self.subject, serializer); + ::sse_encode(self.reviews, serializer); + ::sse_encode(self.rating, serializer); + ::sse_encode(self.since, serializer); + ::sse_encode(self.created_at, serializer); + ::sse_encode(self.expiration, serializer); + ::sse_encode(self.json, serializer); + } +} + +impl SseEncode for crate::api::reputation_transfer::ReputationSupportInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.issuer, serializer); + >>::sse_encode(self.import_issuers, serializer); + } +} + impl SseEncode for crate::api::types::RestoreProgress { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { diff --git a/rust/src/mostro/actions.rs b/rust/src/mostro/actions.rs index c97d1f6fa..b87de72b3 100644 --- a/rust/src/mostro/actions.rs +++ b/rust/src/mostro/actions.rs @@ -589,6 +589,23 @@ pub async fn own_orders( wrap_message_first_contact(identity_keys, trade_keys, mostro_pubkey, &msg).await } +/// Build a reputation request (`export-reputation` or `import-reputation`, +/// MostroP2P/protocol reputation_transfer.md): the `order` wrapper with no +/// id. Both act on the identity, so `identity_keys` must be the real +/// identity — the daemon refuses a request without an identity proof. +/// `request_id` is the nonce the daemon echoes in its reply. +pub async fn reputation_request( + identity_keys: &Keys, + trade_keys: &Keys, + mostro_pubkey: &PublicKey, + request_id: u64, + action: Action, + payload: Payload, +) -> Result { + let msg = Message::new_order(None, Some(request_id), None, action, Some(payload)); + wrap_message_first_contact(identity_keys, trade_keys, mostro_pubkey, &msg).await +} + #[cfg(test)] mod tests { use super::*; @@ -821,6 +838,57 @@ mod tests { assert!(matches!(kind.action, Action::NewOrder)); } + /// A reputation request reaches the node in the `order` wrapper with no + /// id, its nonce and payload intact, and the identity proven (the daemon + /// refuses it without the proof). + #[tokio::test] + async fn a_reputation_request_carries_the_identity_proof_and_its_nonce() { + use mostro_core::message::ReputationExportRequest; + let identity_keys = Keys::generate(); + let trade_keys = Keys::generate(); + let mostro_keys = Keys::generate(); + let _pow = crate::mostro::pow::test_support::lock_pow(); + crate::mostro::pow::set_pows(&mostro_keys.public_key().to_hex(), 0, None); + crate::mostro::protocol_version::set_protocol_version( + &mostro_keys.public_key().to_hex(), + Some(2), + ); + let destination = identity_keys.public_key().to_hex(); + let json = reputation_request( + &identity_keys, + &trade_keys, + &mostro_keys.public_key(), + 77, + Action::ExportReputation, + Payload::ReputationExportRequest(ReputationExportRequest { + destination: destination.clone(), + rebind: None, + }), + ) + .await + .unwrap(); + + let event = Event::from_json(&json).unwrap(); + assert_eq!(event.pubkey, trade_keys.public_key(), "authored by the trade key"); + let unwrapped = transport::unwrap_mostro_message(&mostro_keys, &event) + .await + .unwrap() + .expect("message must decrypt for the recipient"); + assert_eq!(unwrapped.identity, identity_keys.public_key()); + assert!(matches!(unwrapped.message, Message::Order(_))); + let kind = unwrapped.message.get_inner_message_kind(); + assert_eq!(kind.request_id, Some(77)); + assert_eq!(kind.id, None); + assert_eq!(kind.action, Action::ExportReputation); + assert!(kind.verify()); + match kind.get_payload() { + Some(Payload::ReputationExportRequest(request)) => { + assert_eq!(request.destination, destination) + } + other => panic!("unexpected payload {other:?}"), + } + } + /// The outgoing take messages must carry the caller's request_id — the /// correlation nonce the daemon echoes in its reply (add-invoice, /// pay-invoice, pay-bond-invoice, or CantDo) that `take_order` relies on diff --git a/rust/src/mostro/mod.rs b/rust/src/mostro/mod.rs index 72e98e71f..d6a7f29f8 100644 --- a/rust/src/mostro/mod.rs +++ b/rust/src/mostro/mod.rs @@ -12,6 +12,7 @@ pub mod push; pub mod rates; pub(crate) mod reputation; pub mod restore_history; +pub mod reputation_support; pub mod serbero; pub mod session; pub mod trade_index; diff --git a/rust/src/mostro/reputation_support.rs b/rust/src/mostro/reputation_support.rs new file mode 100644 index 000000000..667884c2e --- /dev/null +++ b/rust/src/mostro/reputation_support.rs @@ -0,0 +1,113 @@ +//! What a node offers for reputation portability (MostroP2P/protocol +//! reputation_transfer.md, "Discovery"): `reputation_issuer` when it exports, +//! `reputation_import_issuers` when it imports. A client sends +//! `export-reputation` / `import-reputation` only to a node that advertises +//! them: one that predates the actions cannot parse them and never answers. +//! +//! Kept per node from its capability fetch; a fetch without the tags, or no +//! info event at all, retracts an older answer. + +use std::collections::HashMap; +use std::sync::RwLock; + +const ISSUER_TAG: &str = "reputation_issuer"; +const IMPORT_TAG: &str = "reputation_import_issuers"; + +/// A node's reputation capabilities, from its info event. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct ReputationSupport { + /// The key the node signs attestations with; `None` when it does not + /// export. + pub issuer: Option, + /// The issuer keys the node imports from; `None` when it does not import, + /// empty when it imports from nobody yet. + pub import_issuers: Option>, +} + +static LIVE: RwLock>> = RwLock::new(None); + +fn is_pubkey(value: &str) -> bool { + value.len() == 64 && value.bytes().all(|b| b.is_ascii_hexdigit()) +} + +/// The reputation capabilities an info event's tags announce. Values that +/// are not public keys are dropped. +pub(crate) fn parse_tags(tags: &[Vec]) -> ReputationSupport { + let find = |name: &str| { + tags.iter() + .find(|t| t.first().map(String::as_str) == Some(name)) + }; + let issuer = find(ISSUER_TAG) + .and_then(|t| t.get(1)) + .map(|v| v.trim().to_lowercase()) + .filter(|v| is_pubkey(v)); + let import_issuers = find(IMPORT_TAG).map(|t| { + t.iter() + .skip(1) + .map(|v| v.trim().to_lowercase()) + .filter(|v| is_pubkey(v)) + .collect() + }); + ReputationSupport { + issuer, + import_issuers, + } +} + +/// Record what `node`'s capability fetch announced. +pub(crate) fn set_from_tags(node: &str, tags: &[Vec]) { + LIVE.write() + .unwrap_or_else(|e| e.into_inner()) + .get_or_insert_with(HashMap::new) + .insert(node.to_lowercase(), parse_tags(tags)); +} + +/// What `node` offers, as of its last capability fetch; nothing when it was +/// never fetched. +pub(crate) fn get(node: &str) -> ReputationSupport { + LIVE.read() + .unwrap_or_else(|e| e.into_inner()) + .as_ref() + .and_then(|live| live.get(&node.to_lowercase()).cloned()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn tags(raw: &[&[&str]]) -> Vec> { + raw.iter() + .map(|t| t.iter().map(|v| v.to_string()).collect()) + .collect() + } + + #[test] + fn reads_both_tags_and_drops_values_that_are_not_keys() { + let a = "a".repeat(64); + let b = "B".repeat(64); + let support = parse_tags(&tags(&[ + &["reputation_issuer", &a], + &["reputation_import_issuers", &a, "nope", &b], + ])); + assert_eq!(support.issuer, Some(a.clone())); + assert_eq!(support.import_issuers, Some(vec![a, "b".repeat(64)])); + } + + #[test] + fn an_empty_trust_list_still_means_the_node_imports() { + let support = parse_tags(&tags(&[&["reputation_import_issuers"]])); + assert_eq!(support.import_issuers, Some(vec![])); + assert_eq!(support.issuer, None); + } + + #[test] + fn a_fetch_without_the_tags_retracts_an_older_answer() { + let node = "c".repeat(64); + set_from_tags(&node, &tags(&[&["reputation_issuer", &"d".repeat(64)]])); + assert!(get(&node).issuer.is_some()); + set_from_tags(&node, &[]); + assert_eq!(get(&node), ReputationSupport::default()); + assert_eq!(get(&"e".repeat(64)), ReputationSupport::default()); + } +} diff --git a/rust/tests/fixtures/reputation_v1.json b/rust/tests/fixtures/reputation_v1.json new file mode 100644 index 000000000..5eac30fe3 --- /dev/null +++ b/rust/tests/fixtures/reputation_v1.json @@ -0,0 +1,1979 @@ +{ + "attestation": { + "accepted": [ + { + "description": "`created_at` is exactly 300 seconds after `now`.", + "event": { + "content": "", + "created_at": 1790903100, + "id": "0c5ad1dc4fb7c8712e791e5381eacbf22d881fdaf4e3a0f8436ba1ff4a35b2e7", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "27e3be41e8a0dc396aab60a7203e3722eacb3b54f0a1587629a3994659ab0e1b0503d8ba293cb94f0063e366a852f2355ab6af6899210e6c25d869223a3cfd4b", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "5" + ], + [ + "rating", + "1.00" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1790989500" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "created_at_skew_limit" + }, + { + "description": "`expiration` is exactly 300 seconds before `now`.", + "event": { + "content": "", + "created_at": 1790816100, + "id": "287811077ae87e24be5af7f79255e66d5d42c5cdba4472f4b3f879c5f76690c8", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "67bc3c47b031f14e8a486b55409522f635a8eb0f567829d4ad1f4217009af917d3e81c26b8a1f90eca087acc2d6f138514ef5b932c7cb296d0090a3a114a481e", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "5" + ], + [ + "rating", + "1.00" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1790902500" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "expiration_skew_limit" + }, + { + "description": "`expiration - created_at` is exactly the 7-day cap; `reviews` and `rating` sit at their floors.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "d0c47d5a8269c40289a0d20cda55eedbd44d44c7d9e5ae4f8a90a8b64d50b6d0", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "0d65a625912993ec696754c8a8b948a3fd395e3a2955b5292a3d34e713f101face9a93984441aa1a08ae400d264a1fcffd5f2447e6698453caf6b9981bd387a7", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "5" + ], + [ + "rating", + "1.00" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "lifetime_at_cap" + }, + { + "description": "Carries a tag the rules do not name, which a receiver ignores.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "9cef32d9580e29c3132cea5e2d830e8662f87e890f06f8358bd85be631f6fc3f", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "76e7749cce406fd52df59695f767738db9b1c742e3bb1434d4e83db06b93eb160019e58f9845e3d1ce050c9c5f031755987dfa0a1b1f2d518f03f40632bfb9d5", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "5" + ], + [ + "rating", + "1.00" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ], + [ + "client", + "lnp2pbot" + ] + ] + }, + "name": "unknown_tag_ignored" + } + ], + "invalid": [ + { + "description": "Kind 38384 instead of 38388.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "3855b73587e7bc93af7114c7e4906fa523ee0acbf022f3153abda58901ce6b08", + "kind": 38384, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "aa82e118f4753424cc045b2aad18817d1899f8413404b70a76ee26110de33ae295d9e396a92f53d6cd0a52d3ff23e493da72c5aada12fbae145a2e2d45b626e8", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "wrong_kind", + "reason": "invalid_reputation_attestation" + }, + { + "description": "No `z` tag.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "ed4f92c3f165d5c2186b020d5985619b1563d726a33192e3480b3a1a2b768a50", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "6295e21476f45530c443073f9f2f1148f5a27cf2fb2f59fff04148d6997cc602d0233e81021429e470f983fa774ba3adcee77da17c68025c1469ba54b6e97fa3", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ] + ] + }, + "name": "missing_z", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`z` is `reputation-rebind`: a rebind authorisation is never an attestation.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "6658c5a1e190800006e1d03b935e8c7cdf1f2df0e1403efb96b0247a15dc27f4", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "9d955183778a6f4bfcf1bbeed6dd287281d85c039f6f9c2fd44dd0dd420d8c5b9abda31cee681b831fd99ae509a3b515c9335d3db4b3fde577d0e903e9f44085", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "rebind_z", + "reason": "invalid_reputation_attestation" + }, + { + "description": "No `p` tag.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "2819c37d15fdd21d6fb6281af47f36f56b5de4670e67b04e671bed709757772f", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "67ce56586ae1e7f992e527c86667848ef5fbf753604786a7416f68f6bc9e5c821365afb2f716b9838d8be28a25400863daf8a6fdf777831464ad81d161a83562", + "tags": [ + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "missing_p", + "reason": "invalid_reputation_attestation" + }, + { + "description": "No `since` tag.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "1e9d01a04593fd3ca3b4c9af2d1d59e22eef1aaf42a8e817f2116167e13fdafd", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "2f278e9e821f346f75f7f385e7b9df8cb381fe5dd13101e8604f5ede8015f70f51150b7d568663201cbdd40704741d921ff8fd838e77c6eb12cc5c22ea264705", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "missing_since", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`reviews` appears twice.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "66ad4c648d7be9aa41ee9658e0f917e8d7b1b34fc01d409b71873d051f8aa1c7", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "b82e6cfee17562ad41253a59db418605ad388014b5aaa24eb5d0cea6fc80afba265f551de6725ed01c5c713055ae825a7c5cd6d29fc20402784f23b0f31b2035", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ], + [ + "reviews", + "214" + ] + ] + }, + "name": "repeated_reviews", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`p` is not lowercase hex.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "f66d94844cc05f5ecc6c0426a1efd54586ee7cf6d7be679e20d8fa2bfba5bbfd", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "e938cf76da44d3dfe9334fad6a1ede3e8c0aa311c714d2ad82e289d1d23e8356d1d94281448fb0e23547acec706c8e01ee49813e8888c8946f93149b53727d9b", + "tags": [ + [ + "p", + "2CAA699EC98B1AFE69EB3ED919FE2F3E06E6C3E932C290B4B520D1DD351ED590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "uppercase_p", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`subject` is empty.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "106a9c3a2e991c282531c29cdb5ff33eefd216948730ff64a2b3e5f05cad35c6", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "72ffd168336a6ce481d4d716b316518712b72740d1cfd6791b270e49be2a0007a9e5b0ef132e2aaf3cbf238fcd63597025172910cc72d7a65769e2bef622a262", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "subject_empty", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`subject` holds a character outside `[0-9A-Za-z_-]`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "320283bbd4d11114e636a17d518cdbea833604f6a9d6e060e1959ab491c8b393", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "37151a39de83f05fba0d5b9a49bf9810d2a6435a8684751b24619fe66d7d0e7c73643bc46b791600e336f2e7c79ac4f8cc56109cb6cc27598b7693515160ffd6", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "user 42" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "subject_bad_characters", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`subject` is 65 characters long.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "03c3f5d175e75563d5d7c29a6fa60cc26e15486dd792baff902d722b1bf9d07d", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "ef61e81805edd4834bd27212d05fa1dbb980f052dc03be782a968ce363ede62a03bc4bbdd91f61c232d7e32d51d2e29801b173c176182025757c2a3abde3ae28", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "subject_too_long", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`rating` is 5.01.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "88ef37fc95777ec73a365315c70b774195dc58f71bbdbf314d073a3e38bceb62", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "76bc6a508b851d8f5355a1b8ff7d77b05b78d4bc7b90caa665737d4e93dcd8effea0e6f816de5a94ea9446a0c5ee22015e0d638ab5baf3566a9c199c5908ab48", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "5.01" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "rating_above_range", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`rating` is 0.99.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "1b846d7207afd51502b8d721a658fa0bc0fd98e6e7b19fe797e6a13a5b318c70", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "e043ce66671e26b9385d243dd7338c9bc39920d7c591b651d0bdb60fecaec285d5809117c379950c00da4c116d99d375b4e9586258fc0253b9606cbbf7a66736", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "0.99" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "rating_below_range", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`rating` has one decimal.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "69fd7f4b2a61a2e1962d474d59b8d00cab3ce1d7889202b5b936ef9562f41711", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "6b813bf76d852973f1b4c76f775910fe7b73e491d6e6d30ea2ccbc7febb7fef4f80bc4eed59a2329927d2347d794d10c42eb34109e69d0d51be80913c0de1b1d", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.8" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "rating_one_decimal", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`rating` has three decimals.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "b8632d2476f59687793495be09d6ce73376e1de92423d8967ae489eec9e319b8", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "838f1fbc2d0015b56fa83bd31e3d6e290f297a92074bab5ac3579538a7d9855d4e5a13179aec9d49720c63a94ca4a38454d4d113a73fb0cb2d15aa68795a87ff", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.870" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "rating_three_decimals", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`rating` is `4,87`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "aa7fb5d1b8ccfb833d97eebaad2133226f6ec6a179b57113da7f6aad6e07b4dc", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "44bc8455ef42f9b755e6a9050f4c182631d46b47ec5ae4420433f232036ac376fa4e81f85d3e2da03bef639edf91cbc2efc291df053da4d3d33839ead9d4a17f", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4,87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "rating_not_a_number", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`reviews` is 4.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "4d3a104bca2248cbbe2d6559b489abdf75a306b47ecedaf89f2f6ebfb8fd92c7", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "f0b0964179284917fee9f3830e3d711a9cccdb05881134e1862393b7a2ba2f1ffe04075546c3ad766aafc94cd0553f5ccdeb38c5f0f9f5fa1a8f1b39a70ad1dd", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "4" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "reviews_below_floor", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`reviews` is written `0214`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "d3ac0dba0eb9d509484e8fa13d19bfc86551169291f452a5eee88cd5829e2884", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "53967be757bb5e306a177b12bc3adb7506bbf1630fa0772afa5365361577ba487cfbfc892391d73c1d87c8b91a54f1bbb706830d76aeed1d11f54161881919ce", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "0214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "reviews_leading_zero", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`reviews` is written `+214`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "804d5678024cb63511037595a7f6f6dd37c8b58adfef801c5cea5f86a7abcbb2", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "9faa38a38e8a5aff5bb3b00b4cdea5603e769db7a3810189644c55d82ae5f2997f40820253a4e18337ffc903e8c4c8a5e5a77b78299d3c52c8acbed867aeb062", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "+214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "reviews_signed", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`reviews` is 4294967296.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "129be843748c6b8401c575b3adf3e01873e6e8c7e843a4f8f9957a3200d75764", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "226811dad2447cbb31b06e659c4719b05d003be512420e80226082461b075d8d5ea41d956698a438afa2f8f19b095e1ed09da99b0e2f9dc0b6ab3cfdc9c2a7dc", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "4294967296" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "reviews_overflow", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`since` is not a multiple of 86400.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "1ee8c88c50552e4e2fda423f912242aa221e4f5d8c55e0c1cc09031db9e818cf", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "bd92e9b7ce159fa95ba1ee21e4a61fbbd41c5bcf30d5e41799806a56cd2715a9a11029ec0e9bb294489075dd7647380b3fb72e16b1f08730d99665920c69662c", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204801" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "since_not_day_aligned", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`since` is 2019-12-31.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "861365acaf65c45c0ae06557f217027ca4beb2cd9fe2dda87843cc26bce1c615", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "e15d2b81e0075288454889c65e5974445a5ce2e686e697b65142ce6cec3d3912ade83cfdc9665a62d913b8deb0053c2a59b58e1bfd3d4c4cc2f1900634bd0476", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1577750400" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "since_before_2020", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`since` is the day after `created_at`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "744e2256f3914b742fc24ce44c3699d15f340c6908f14cc70dba621f963235fb", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "bce9d53ddbc4c85fd4318c81300fb867babe71bdfe904b42ef9696ab0f07f7e6e98a1b36dba015a774503c313002341f2224697b8e24e1b3c4b3be46ee55a4e6", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1790985600" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "since_after_created_at", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`expiration` equals `created_at`.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "53e33cb0fbbc140d7b31795c1ffc41cddbfb3e543abc986d4c4f1c51cf1a5b17", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "f453c34248cd5f56117d2fc8219007f9c5664ab2dc523e6729a3cf1e2ae1b2602cb10f7b0e057132cdef18a8a69a16753ce12d8d998f25a1778aab220bb22684", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1790899200" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "expiration_not_after_created_at", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`expiration - created_at` is one second over the 7-day cap.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "47d288ad1c94dec85b64a5119e9a8fd7fb77a944c29bc010909cd1d361bb5d48", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "fa2bb9c338932e359c9cf7c0d522240a979eaf2868dafcd625ab92d58ffc1e242bc02625f6966e734fac8f8ffa622cc91b7e7083602e9a97008b42065e2a9ce2", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504001" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "lifetime_over_cap", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`created_at` is 301 seconds after `now`.", + "event": { + "content": "", + "created_at": 1790903101, + "id": "228842f0664f236b8229dd5a76f444847a2ffd62eb03302c19c7f26f5df31e67", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "4741eed59ce3b24dee6ded8032f5c0f741cfe687926d513a0018b3d855cba9fc7be692456bbaa9d9c6d14cafe3cae9346c792fbc8155915a215d552e5ab6ecf8", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1790989501" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "created_in_the_future", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`expiration` is 301 seconds before `now`.", + "event": { + "content": "", + "created_at": 1790816099, + "id": "7df85be68a2679b3ca3511d0d4aea9609c05e2fe91d21beaa0f5ef0b628f5a3a", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "4a200b727f1ee13c6667c4d76148cceb660139614cee3514d3349d251a774405ee71c06074b8e5885e1b5ca6a6720f1b0b576d28eb315d808b7d4e5f8830748d", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1790902499" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "expired", + "reason": "expired_reputation_attestation" + }, + { + "description": "Last byte of `sig` flipped.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "a52ccf54b31581681c681b9fe939a8742705c7af231ef3a7e33b9c86d3d6a78a", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "583660a36bd95ed4bd85cab708ccce531031c5fce2194483a002eb126c3741f068d4684f2f849e056dadfa1972513010cdc05c254a1ba60ea279af20a9d418ee", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "mauled_signature", + "reason": "invalid_reputation_attestation" + }, + { + "description": "`content` changed after signing, so `id` no longer matches.", + "event": { + "content": "x", + "created_at": 1790899200, + "id": "a52ccf54b31581681c681b9fe939a8742705c7af231ef3a7e33b9c86d3d6a78a", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "583660a36bd95ed4bd85cab708ccce531031c5fce2194483a002eb126c3741f068d4684f2f849e056dadfa1972513010cdc05c254a1ba60ea279af20a9d418ef", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "id_mismatch", + "reason": "invalid_reputation_attestation" + }, + { + "description": "Well formed, but names another identity than the one the request proves.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "5e8f9fc5f86eb530aa48719ef9ddcb10ea504c8c581ec6af8d3b7606c775ea30", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "bb106619d4ad4440e00b011d74c1b64403e89a2d83b76be25da3f2f594498c9b0b512d1bd58e0af55a988cc2eac9789fd020e3c20d2d328ffa1e952dee89b39f", + "tags": [ + [ + "p", + "708fe596e895c30458deedf0321a53bb640cb5c3e38e4660f652f2b2e5c2feab" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "identity_mismatch", + "reason": "reputation_identity_mismatch" + }, + { + "description": "Well formed, but signed by a key in no trust-list entry.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "560e44fb6ab80926713d3564a982d3abe8c008f569dacc07d569837f80c00906", + "kind": 38388, + "pubkey": "b8e18d18d33cbaf837fda291f9f78e974b92ca83865a61f69a38fabba99846df", + "sig": "837a7c2f2ad754cdec60f88e02332cb5df769306001eff50405b1ac99248c988fa214f3cfcc92000be0becad694a2c571bc4205109b4a13ed843f295eb2eed55", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "untrusted_issuer", + "reason": "untrusted_reputation_issuer" + }, + { + "description": "Well formed, but signed by the destination's own issuer key, which its trust list also holds.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "9a0afe9dd7b422a495bcc15df86016b091f6dfbc8ea26c64ef52b35bad11b2a0", + "kind": 38388, + "pubkey": "4aaed1ec5ce1e8e04e2e97183fe1eea0569adfa3679c20d3479a7da54e348fd5", + "sig": "fdeeb8a30266e04a9532869245b5c82804354e407c623248dae719c58932da90521c4f5dffff120e64170dc8c9535821237653b99f416f3d9d8f704f34ae5190", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "own_issuer_key", + "reason": "untrusted_reputation_issuer" + } + ], + "valid": { + "description": "Signed by the trusted issuer for the proven identity, inside its lifetime.", + "event": { + "content": "", + "created_at": 1790899200, + "id": "a52ccf54b31581681c681b9fe939a8742705c7af231ef3a7e33b9c86d3d6a78a", + "kind": 38388, + "pubkey": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "sig": "583660a36bd95ed4bd85cab708ccce531031c5fce2194483a002eb126c3741f068d4684f2f849e056dadfa1972513010cdc05c254a1ba60ea279af20a9d418ef", + "tags": [ + [ + "p", + "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590" + ], + [ + "subject", + "64f1c9f4e3a2b1c0d9e8f7a6" + ], + [ + "reviews", + "214" + ], + [ + "rating", + "4.87" + ], + [ + "since", + "1696204800" + ], + [ + "expiration", + "1791504000" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "expect": { + "created_at": 1790899200, + "destination": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "expiration": 1791504000, + "issuer_key": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "issuer_name": "issuer-a", + "rating": "4.87", + "reviews": 214, + "since": 1696204800, + "subject": "64f1c9f4e3a2b1c0d9e8f7a6" + }, + "id": "a52ccf54b31581681c681b9fe939a8742705c7af231ef3a7e33b9c86d3d6a78a", + "json": "{\"id\":\"a52ccf54b31581681c681b9fe939a8742705c7af231ef3a7e33b9c86d3d6a78a\",\"pubkey\":\"fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21\",\"created_at\":1790899200,\"kind\":38388,\"tags\":[[\"p\",\"2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590\"],[\"subject\",\"64f1c9f4e3a2b1c0d9e8f7a6\"],[\"reviews\",\"214\"],[\"rating\",\"4.87\"],[\"since\",\"1696204800\"],[\"expiration\",\"1791504000\"],[\"z\",\"reputation-attestation\"]],\"content\":\"\",\"sig\":\"583660a36bd95ed4bd85cab708ccce531031c5fce2194483a002eb126c3741f068d4684f2f849e056dadfa1972513010cdc05c254a1ba60ea279af20a9d418ef\"}" + } + }, + "context": { + "clock_skew": 300, + "max_lifetime": 604800, + "now": 1790902800, + "own_issuer_key": "4aaed1ec5ce1e8e04e2e97183fe1eea0569adfa3679c20d3479a7da54e348fd5", + "proven_identity": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "trust_list": [ + { + "keys": [ + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + "name": "issuer-a" + }, + { + "keys": [ + "4aaed1ec5ce1e8e04e2e97183fe1eea0569adfa3679c20d3479a7da54e348fd5" + ], + "name": "self" + } + ] + }, + "description": "Test vectors for the reputation attestation (kind 38388). See reputation_vectors.md.", + "merge": { + "cases": [ + { + "after": { + "created_at": 1696204800, + "last_rating": 5, + "max_rating": 5, + "min_rating": 4, + "native_created_at": 1790047545, + "native_rating_sum": 9.0, + "seeded_rating_sum": 1042.18, + "seeded_reviews": 214, + "total_rating": 4.866574074074074, + "total_reviews": 216 + }, + "before": { + "created_at": 1790047545, + "last_rating": 5, + "max_rating": 5, + "min_rating": 4, + "native_created_at": 1790047545, + "native_rating_sum": 9.0, + "seeded_rating_sum": 0.0, + "seeded_reviews": 0, + "total_rating": 4.5, + "total_reviews": 2 + }, + "description": "The plan's worked example: 2 native ratings averaging 4.5, 10 days on the instance.", + "import": { + "rating": "4.87", + "reviews": 214, + "since": 1696204800 + }, + "name": "existing_reputation", + "reverted": { + "created_at": 1790047545, + "last_rating": 5, + "max_rating": 5, + "min_rating": 4, + "native_created_at": 1790047545, + "native_rating_sum": 9.0, + "seeded_rating_sum": 0.0, + "seeded_reviews": 0, + "total_rating": 4.5, + "total_reviews": 2 + } + }, + { + "after": { + "created_at": 1696204800, + "last_rating": 5, + "max_rating": 5, + "min_rating": 5, + "native_created_at": 1790047545, + "native_rating_sum": 0.0, + "seeded_rating_sum": 1042.18, + "seeded_reviews": 214, + "total_rating": 4.87, + "total_reviews": 214 + }, + "before": { + "created_at": 1790047545, + "last_rating": 0, + "max_rating": 0, + "min_rating": 0, + "native_created_at": 1790047545, + "native_rating_sum": 0.0, + "seeded_rating_sum": 0.0, + "seeded_reviews": 0, + "total_rating": 0.0, + "total_reviews": 0 + }, + "description": "No rating at all: the import also sets the rating extrema, and reverting clears them.", + "import": { + "rating": "4.87", + "reviews": 214, + "since": 1696204800 + }, + "name": "no_reputation", + "reverted": { + "created_at": 1790047545, + "last_rating": 0, + "max_rating": 0, + "min_rating": 0, + "native_created_at": 1790047545, + "native_rating_sum": 0.0, + "seeded_rating_sum": 0.0, + "seeded_reviews": 0, + "total_rating": 0.0, + "total_reviews": 0 + } + } + ], + "tolerance": 1e-9 + }, + "rating_rounding": [ + { + "average": 4.866574074074074, + "rating": "4.87" + }, + { + "average": 4.125, + "rating": "4.13" + }, + { + "average": 4.895, + "rating": "4.89" + }, + { + "average": 4.875, + "rating": "4.88" + }, + { + "average": 4.005, + "rating": "4.01" + }, + { + "average": 0.9, + "rating": "1.00" + }, + { + "average": 5.0, + "rating": "5.00" + }, + { + "average": 5.2, + "rating": "5.00" + } + ], + "rebind": { + "context": { + "bound_identity": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "clock_skew": 300, + "issuer_key": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "now": 1790902800 + }, + "invalid": [ + { + "description": "Valid signature, but not by the identity the account is bound to.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "33707347bdc31326b9008ac448d80be1c3dec82ca46a44ee9fbf49f33223aeb0", + "kind": 38388, + "pubkey": "708fe596e895c30458deedf0321a53bb640cb5c3e38e4660f652f2b2e5c2feab", + "sig": "559f977ba64b731943d471f650bec739051b2fc974397e1c4b2a917e93c3f8267e5c8474c6c1918367f61c8054426fb1efb0bc6d636d91359c751dba50d26918", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790902800" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "signed_by_other_identity", + "reason": "invalid_reputation_rebind" + }, + { + "description": "`issuer` names another issuer, so it cannot be replayed here.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "08ff8d0ec16eea59a8434408173714694f1c26311a235259712335dfaa790906", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "38574c451edc4451f3f96e1bb44ab27aecb81d5e5997ea2aef2860c0130cb61e21ad704602ac1afb79081082ceb2755c42653793d187a2bbf28c852c055b334e", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "b8e18d18d33cbaf837fda291f9f78e974b92ca83865a61f69a38fabba99846df" + ], + [ + "expiration", + "1790902800" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "other_issuer", + "reason": "invalid_reputation_rebind" + }, + { + "description": "`expiration - created_at` is 3601 seconds.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "421192787ffa71946c75cd7120b8a135ecf456c538ea3d4eecc83e5cfc55570a", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "0e22e1673d7dc895f89735a0778e8541764f82fc9d1f27fc386a751434c229700143ee10e00be05f183c93559172d9e7963cf3dc882260e33a22b5e9416179c3", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790905801" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "lifetime_over_an_hour", + "reason": "invalid_reputation_rebind" + }, + { + "description": "`expiration` is 301 seconds before `now`.", + "event": { + "content": "", + "created_at": 1790901600, + "id": "5e35ad33b4e8bb066245f79c2739288a6c3059d1cee814701dd8ce72e0755c17", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "381455289ba9c4e24c84537769de75372b80d0cf038fa536e61fd3e54c3357ba5ba9358e4b2044e5fa84e1617a85ff2cb1ef4c639c5f8e5542fd1a3f7eecff5b", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790902499" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "expired", + "reason": "invalid_reputation_rebind" + }, + { + "description": "`z` is `reputation-attestation`.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "67bc39dab3070c83110bf393aefbb52d4352e10f1947c43cc1bc83a8b3ca56c5", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "bce4cb7a2ea1bba4d170fc11181e560c332e67dbd476d8d2dfb4f506b0b3d15633269a87ef5849cabb4de7308d4f5aa68962c5b327bb806b779732033f49f48c", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790902800" + ], + [ + "z", + "reputation-attestation" + ] + ] + }, + "name": "attestation_z", + "reason": "invalid_reputation_rebind" + }, + { + "description": "No `issuer` tag.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "f3bbb1578b5dc39a9f4d8530145b00192af41083267893c07158a86f2abacf8f", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "49d43af28be4601fd9d192489e298b8c6fcf244de0a0538c8f5ac99b9225731d60589b0f02fa702b9da6d300aa1ebdb1c6a1567e0ce5df610ae0006f0c3270e9", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "expiration", + "1790902800" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "missing_issuer", + "reason": "invalid_reputation_rebind" + }, + { + "description": "`created_at` changed after signing.", + "event": { + "content": "", + "created_at": 1790902201, + "id": "6da754add522f76fa0e515a7a1cf9c577f3cee2e9b5a022211cb5803f9d7bf82", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "09bb6711aada783d032a25766f5782f70905ed8c24bc8f80d0ebb35d8223e3bdc432166ee0a2b0c37d1d4de0c7502647b5464a917d16b8079ec0536e12c436bf", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790905800" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "name": "id_mismatch", + "reason": "invalid_reputation_rebind" + } + ], + "valid": { + "description": "Signed by the bound identity, for this issuer, inside its hour.", + "event": { + "content": "", + "created_at": 1790902200, + "id": "6da754add522f76fa0e515a7a1cf9c577f3cee2e9b5a022211cb5803f9d7bf82", + "kind": 38388, + "pubkey": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "sig": "09bb6711aada783d032a25766f5782f70905ed8c24bc8f80d0ebb35d8223e3bdc432166ee0a2b0c37d1d4de0c7502647b5464a917d16b8079ec0536e12c436bf", + "tags": [ + [ + "p", + "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + ], + [ + "issuer", + "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21" + ], + [ + "expiration", + "1790905800" + ], + [ + "z", + "reputation-rebind" + ] + ] + }, + "expect": { + "bound_identity": "2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590", + "created_at": 1790902200, + "expiration": 1790905800, + "issuer": "fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21", + "new_identity": "a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d" + }, + "json": "{\"id\":\"6da754add522f76fa0e515a7a1cf9c577f3cee2e9b5a022211cb5803f9d7bf82\",\"pubkey\":\"2caa699ec98b1afe69eb3ed919fe2f3e06e6c3e932c290b4b520d1dd351ed590\",\"created_at\":1790902200,\"kind\":38388,\"tags\":[[\"p\",\"a3362049acac57a043dcafc35229c563c764bc22b84a18698a71c2e8e0fd706d\"],[\"issuer\",\"fd85e22382d4720a042cc000e0fa24ea12781c2321fe47fb53d413c57596eb21\"],[\"expiration\",\"1790905800\"],[\"z\",\"reputation-rebind\"]],\"content\":\"\",\"sig\":\"09bb6711aada783d032a25766f5782f70905ed8c24bc8f80d0ebb35d8223e3bdc432166ee0a2b0c37d1d4de0c7502647b5464a917d16b8079ec0536e12c436bf\"}" + } + }, + "secret_keys": { + "destination-issuer": "cae35bead45bc66452742ef609768807e600a48c47dba638c95326c223bc6a83", + "identity": "2aa2122269ed35a5cd916ff66f53d57eaac7a0339413c1b4e125af2bcfdde187", + "issuer-a": "00002be611259ef49554a87dc387a8e6471e759ac055af006e859afc40b7a0cd", + "issuer-b": "fb7f417bbe175dcd1d34d8968a3a0975caf25e94910c90ff26c4695e9586e8da", + "new-identity": "2efa1f1c0415d65fe56d12393634a4ee9e875ec6afd9a5a8f987aedc5d593b8b", + "other-identity": "1e0a216d75a09b8306efd7d645265c247a436d5ce20e35ceb7488527d1dc52a1" + }, + "version": 1 +} diff --git a/test/features/reputation/import_reputation_screen_test.dart b/test/features/reputation/import_reputation_screen_test.dart new file mode 100644 index 000000000..f5898aede --- /dev/null +++ b/test/features/reputation/import_reputation_screen_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:mostro/features/reputation/reputation_api.dart'; +import 'package:mostro/features/reputation/screens/import_reputation_screen.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:mostro/src/rust/api/reputation_transfer.dart'; + +const _json = '{"id":"a","kind":38388}'; + +ReputationAttestationInfo _info() => ReputationAttestationInfo( + id: 'a', + issuer: 'b' * 64, + destination: 'c' * 64, + subject: 'acct', + reviews: 214, + rating: '4.87', + since: 1696204800, + createdAt: 1790899200, + expiration: 1791504000, + json: _json, +); + +/// Stands in for the Rust core: parses only [_json], imports or refuses. +class _FakeApi extends ReputationApi { + final List imported = []; + Object? refuse; + + @override + ReputationAttestationInfo parse(String json) { + if (json != _json) throw Exception('InvalidReputationAttestation: bad'); + return _info(); + } + + @override + Future import(String json) async { + if (refuse != null) throw refuse!; + imported.add(json); + return _info(); + } + + @override + Future pending() async => null; +} + +Future<_FakeApi> _pump(WidgetTester tester) async { + final api = _FakeApi(); + await tester.pumpWidget( + ProviderScope( + overrides: [reputationApiProvider.overrideWithValue(api)], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: ImportReputationScreen(), + ), + ), + ); + await tester.pumpAndSettle(); + return api; +} + +Future _tap(WidgetTester tester, String text) async { + await tester.ensureVisible(find.text(text)); + await tester.tap(find.text(text)); + await tester.pumpAndSettle(); +} + +void main() { + testWidgets('shows the figures of a pasted attestation and imports it', ( + tester, + ) async { + final api = await _pump(tester); + await tester.enterText(find.byType(TextField), 'from the bot:\n$_json'); + await _tap(tester, 'Check'); + expect( + find.textContaining('214 ratings received, 4.87 on average'), + findsOneWidget, + ); + await _tap(tester, 'Import into this Mostro'); + expect(api.imported, [_json]); + expect(find.text('Your reputation was imported.'), findsOneWidget); + }); + + testWidgets('says why an attestation is refused', (tester) async { + final api = await _pump(tester); + await tester.enterText(find.byType(TextField), 'nothing useful'); + await _tap(tester, 'Check'); + expect(find.textContaining('not a valid reputation'), findsOneWidget); + + api.refuse = Exception('ReputationAlreadyImported: refused by the node'); + await tester.enterText(find.byType(TextField), _json); + await _tap(tester, 'Check'); + await _tap(tester, 'Import into this Mostro'); + expect(find.textContaining('already imported'), findsOneWidget); + + api.refuse = Exception('PrivacyModeEnabled: needs the identity key'); + await _tap(tester, 'Import into this Mostro'); + expect(find.textContaining('full privacy mode'), findsOneWidget); + }); +} diff --git a/test/features/reputation/lnp2pbot_test.dart b/test/features/reputation/lnp2pbot_test.dart new file mode 100644 index 000000000..899f47ffd --- /dev/null +++ b/test/features/reputation/lnp2pbot_test.dart @@ -0,0 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro/features/reputation/lnp2pbot.dart'; + +void main() { + test('the export link carries the identity in 43 base64url characters', () { + const identity = + 'ab1db593a4d1196eb07335fdc702a314712ed29c631ceaebd84708a17d3bc2f7'; + final uri = lnp2pbotExportUri(identity)!; + expect(uri.toString(), startsWith('https://t.me/lnp2pbot?start=rep_')); + final start = uri.queryParameters['start']!; + expect(start.length, lessThanOrEqualTo(64)); + final encoded = start.substring(4); + expect(encoded, hasLength(43)); + final bytes = base64Url.decode('$encoded='); + expect( + bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(), + identity, + ); + expect(lnp2pbotExportUri('npub1nope'), isNull); + }); + + test('finds the attestation inside pasted text', () { + final vectors = jsonDecode( + File('rust/tests/fixtures/reputation_v1.json').readAsStringSync(), + ); + final json = vectors['attestation']['valid']['json'] as String; + expect(extractAttestationJson(json), json); + expect(extractAttestationJson('from the bot:\n$json\n'), json); + expect(extractAttestationJson('nothing'), isNull); + expect(extractAttestationJson('{broken'), isNull); + }); +} diff --git a/test/features/reputation/npub_test.dart b/test/features/reputation/npub_test.dart new file mode 100644 index 000000000..9b455333b --- /dev/null +++ b/test/features/reputation/npub_test.dart @@ -0,0 +1,14 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mostro/features/reputation/npub.dart'; + +void main() { + test('encodes the NIP-19 example key', () { + expect( + hexToNpub( + '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e', + ), + 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg', + ); + expect(hexToNpub('nope'), isNull); + }); +} diff --git a/test/features/reputation/reputation_screen_test.dart b/test/features/reputation/reputation_screen_test.dart new file mode 100644 index 000000000..257dd28a6 --- /dev/null +++ b/test/features/reputation/reputation_screen_test.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:mostro/features/account/providers/privacy_mode_provider.dart'; +import 'package:mostro/features/reputation/reputation_api.dart'; +import 'package:mostro/features/reputation/screens/reputation_screen.dart'; +import 'package:mostro/l10n/app_localizations.dart'; +import 'package:mostro/src/rust/api/reputation_transfer.dart'; + +const _identity = + '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'; +const _npub = 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'; + +ReputationAttestationInfo _info() => ReputationAttestationInfo( + id: 'a', + issuer: 'b' * 64, + destination: _identity, + subject: 'acct', + reviews: 5, + rating: '4.40', + since: 1696204800, + createdAt: 1790899200, + expiration: 1791504000, + json: '{"kind":38388}', +); + +class _FakeApi extends ReputationApi { + final String? issuer; + int exports = 0; + Object? refuse; + + _FakeApi(this.issuer); + + @override + ReputationSupportInfo support() => + ReputationSupportInfo(issuer: issuer, importIssuers: const []); + + @override + Future identity() async => _identity; + + @override + Future export({ + String? destination, + String? rebind, + }) async { + exports++; + if (refuse != null) throw refuse!; + return _info(); + } + + @override + Future signRebind({ + required String issuer, + required String newIdentity, + }) async { + if (!newIdentity.startsWith('npub1')) throw Exception('invalid key'); + return 'rebind:$issuer:$newIdentity'; + } +} + +class _Privacy extends PrivacyModeNotifier { + _Privacy(bool on) : super() { + state = on; + } +} + +Future<_FakeApi> _pump( + WidgetTester tester, { + String? issuer, + bool privacy = false, +}) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final api = _FakeApi(issuer); + await tester.pumpWidget( + ProviderScope( + overrides: [ + reputationApiProvider.overrideWithValue(api), + privacyModeProvider.overrideWith((ref) => _Privacy(privacy)), + ], + child: const MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: ReputationScreen(), + ), + ), + ); + await tester.pumpAndSettle(); + return api; +} + +void main() { + testWidgets('export asks to confirm the npub before binding it', ( + tester, + ) async { + final api = await _pump(tester, issuer: 'b' * 64); + await tester.tap(find.widgetWithText(FilledButton, 'Export my reputation')); + await tester.pumpAndSettle(); + expect(find.textContaining(_npub), findsOneWidget); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(api.exports, 0); + + await tester.tap(find.widgetWithText(FilledButton, 'Export my reputation')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Export')); + await tester.pumpAndSettle(); + expect(api.exports, 1); + expect(find.textContaining('Your reputation is ready'), findsOneWidget); + }); + + testWidgets('a node that does not export says so', (tester) async { + await _pump(tester); + expect( + find.text('This Mostro does not export reputation.'), + findsOneWidget, + ); + expect( + find.widgetWithText(FilledButton, 'Export my reputation'), + findsNothing, + ); + }); + + testWidgets('shows the node refusal of an export', (tester) async { + final api = await _pump(tester, issuer: 'b' * 64); + api.refuse = Exception( + 'NotEligibleForReputationExport: refused by the node', + ); + await tester.tap(find.widgetWithText(FilledButton, 'Export my reputation')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Export')); + await tester.pumpAndSettle(); + expect(find.textContaining('at least 10 completed trades'), findsOneWidget); + }); + + testWidgets('signs a rebind authorisation for a valid identity only', ( + tester, + ) async { + await _pump(tester, issuer: 'b' * 64); + await tester.enterText(find.byType(TextField), 'nope'); + await tester.tap(find.text('Sign authorization')); + await tester.pumpAndSettle(); + expect(find.text('That is not a valid npub.'), findsOneWidget); + await tester.enterText(find.byType(TextField), _npub); + await tester.tap(find.text('Sign authorization')); + await tester.pumpAndSettle(); + expect(find.text('rebind:${'b' * 64}:$_npub'), findsOneWidget); + }); + + testWidgets('privacy mode turns both directions off', (tester) async { + await _pump(tester, issuer: 'b' * 64, privacy: true); + expect(find.textContaining('Turn off full privacy mode'), findsOneWidget); + final export = tester.widget( + find.widgetWithText(FilledButton, 'Export my reputation'), + ); + expect(export.onPressed, isNull); + }); +} diff --git a/test/features/settings/screens/goldens/settings_10a_settings_dark.png b/test/features/settings/screens/goldens/settings_10a_settings_dark.png index 09f5d09ae..c867776c6 100644 Binary files a/test/features/settings/screens/goldens/settings_10a_settings_dark.png and b/test/features/settings/screens/goldens/settings_10a_settings_dark.png differ diff --git a/test/features/settings/screens/goldens/settings_10a_settings_light.png b/test/features/settings/screens/goldens/settings_10a_settings_light.png index bfa28c4af..016d8673e 100644 Binary files a/test/features/settings/screens/goldens/settings_10a_settings_light.png and b/test/features/settings/screens/goldens/settings_10a_settings_light.png differ