Skip to content

Commit fe4fe4a

Browse files
authored
feat(about): show anti-abuse bond policy from node info event (#617)
* feat(about): show anti-abuse bond policy from node info event - Add an Anti-abuse bond section to the About screen, parsed from the kind-38385 info event - Hide it on legacy nodes, show status only when disabled, full parameters when enabled - Add localized labels, per-row help dialogs and a concept explanation in en/es/it/de/fr * Avoid rendering missing bond fields as real values
1 parent 741535d commit fe4fe4a

6 files changed

Lines changed: 338 additions & 5 deletions

File tree

lib/features/settings/about_screen.dart

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ class AboutScreen extends ConsumerWidget {
313313
),
314314
const SizedBox(height: 20),
315315

316+
// Anti-abuse bond Section (hidden when the node doesn't advertise it)
317+
_buildAntiAbuseBondSection(context, instance),
318+
316319
// Technical Details Section
317320
Text(
318321
S.of(context)!.technicalDetails,
@@ -501,6 +504,146 @@ class AboutScreen extends ConsumerWidget {
501504
);
502505
}
503506

507+
/// Anti-abuse bond section. Hidden entirely for legacy daemons that don't
508+
/// advertise the policy ([BondPolicy.unsupported]); shows only a status row
509+
/// when disabled, and the full parameter set when enabled.
510+
Widget _buildAntiAbuseBondSection(
511+
BuildContext context, MostroInstance instance) {
512+
if (instance.bondPolicy == BondPolicy.unsupported) {
513+
return const SizedBox.shrink();
514+
}
515+
final s = S.of(context)!;
516+
final enabled = instance.bondPolicy == BondPolicy.enabled;
517+
final children = <Widget>[
518+
_buildSectionHeaderWithInfo(
519+
context,
520+
s.antiAbuseBond,
521+
s.antiAbuseBondExplanation,
522+
),
523+
const SizedBox(height: 12),
524+
_buildInfoRowWithDialog(
525+
context,
526+
s.bondStatusLabel,
527+
enabled ? s.bondEnabled : s.bondDisabled,
528+
s.bondStatusExplanation,
529+
),
530+
];
531+
if (enabled) {
532+
children.addAll([
533+
const SizedBox(height: 16),
534+
_buildInfoRowWithDialog(
535+
context,
536+
s.bondAppliesToLabel,
537+
_bondAppliesToValue(s, instance.bondApplyTo),
538+
s.bondAppliesToExplanation,
539+
),
540+
const SizedBox(height: 16),
541+
_buildInfoRowWithDialog(
542+
context,
543+
s.bondAmountLabel,
544+
_bondAmountValue(s, instance),
545+
s.bondAmountExplanation,
546+
),
547+
const SizedBox(height: 16),
548+
_buildInfoRowWithDialog(
549+
context,
550+
s.bondSlashOnTimeoutLabel,
551+
instance.bondSlashOnWaitingTimeout == null
552+
? s.notAvailableShort
553+
: (instance.bondSlashOnWaitingTimeout! ? s.yes : s.no),
554+
s.bondSlashOnTimeoutExplanation,
555+
),
556+
const SizedBox(height: 16),
557+
_buildInfoRowWithDialog(
558+
context,
559+
s.bondNodeShareLabel,
560+
_formatBondPercent(s, instance.bondSlashNodeSharePct),
561+
s.bondNodeShareExplanation,
562+
),
563+
const SizedBox(height: 16),
564+
_buildInfoRowWithDialog(
565+
context,
566+
s.bondClaimWindowLabel,
567+
instance.bondPayoutClaimWindowDays == null
568+
? s.notAvailableShort
569+
: s.bondClaimWindowValue(instance.bondPayoutClaimWindowDays!),
570+
s.bondClaimWindowExplanation,
571+
),
572+
]);
573+
}
574+
children.add(const SizedBox(height: 20));
575+
return Column(
576+
crossAxisAlignment: CrossAxisAlignment.start,
577+
children: children,
578+
);
579+
}
580+
581+
/// Section header matching the plain headers but with a tappable info icon
582+
/// that opens the concept explanation dialog.
583+
Widget _buildSectionHeaderWithInfo(
584+
BuildContext context, String title, String explanation) {
585+
return Row(
586+
children: [
587+
Text(
588+
title,
589+
style: const TextStyle(
590+
color: AppTheme.activeColor,
591+
fontSize: 16,
592+
fontWeight: FontWeight.w600,
593+
),
594+
),
595+
const SizedBox(width: 6),
596+
InkWell(
597+
onTap: () => _showInfoDialog(context, title, explanation),
598+
borderRadius: BorderRadius.circular(12),
599+
child: const Padding(
600+
padding: EdgeInsets.all(8.0),
601+
child: Icon(
602+
Icons.info_outline,
603+
size: 16,
604+
color: AppTheme.activeColor,
605+
),
606+
),
607+
),
608+
],
609+
);
610+
}
611+
612+
String _bondAppliesToValue(S s, BondApplyTo? applyTo) {
613+
switch (applyTo) {
614+
case BondApplyTo.take:
615+
return s.bondAppliesToTakers;
616+
case BondApplyTo.make:
617+
return s.bondAppliesToMakers;
618+
case BondApplyTo.both:
619+
return s.bondAppliesToBoth;
620+
case null:
621+
return s.notAvailableShort;
622+
}
623+
}
624+
625+
String _bondAmountValue(S s, MostroInstance instance) {
626+
final base = instance.bondBaseAmountSats;
627+
if (instance.bondAmountPct == null || base == null) {
628+
return s.notAvailableShort;
629+
}
630+
final percent = _formatPercentNumber(s, instance.bondAmountPct);
631+
final min = NumberFormat.decimalPattern().format(base);
632+
return s.bondAmountValue(percent, min);
633+
}
634+
635+
/// Formats a `[0,1]` fraction as a percent number without trailing zeros,
636+
/// e.g. 0.5 -> "50", 0.015 -> "1.5". Returns the localized "N/A" for null.
637+
String _formatPercentNumber(S s, double? fraction) {
638+
if (fraction == null) return s.notAvailableShort;
639+
return NumberFormat('0.##').format(fraction * 100);
640+
}
641+
642+
String _formatBondPercent(S s, double? fraction) {
643+
if (fraction == null) return s.notAvailableShort;
644+
return '${_formatPercentNumber(s, fraction)}%';
645+
}
646+
504647
Widget _buildInfoRow(String label, String value) {
505648
return Row(
506649
crossAxisAlignment: CrossAxisAlignment.start,

lib/l10n/intl_de.arb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1665,5 +1665,43 @@
16651665
"bondPayoutCompletedMessage": "Du hast die Auszahlung deines Anteils der Anti-Missbrauchs-Kaution erhalten.",
16661666
"bondPayoutCompletedWithRating": "Du hast die Auszahlung deines Anteils der Anti-Missbrauchs-Kaution erhalten. Du kannst deinen Handelspartner bewerten.",
16671667
"bondPayoutCloseButton": "SCHLIESSEN",
1668-
"errorLoadingBondPayout": "Fehler beim Laden der Auszahlungsdaten"
1668+
"errorLoadingBondPayout": "Fehler beim Laden der Auszahlungsdaten",
1669+
"antiAbuseBond": "Anti-Missbrauchs-Kaution",
1670+
"antiAbuseBondExplanation": "Die Anti-Missbrauchs-Kaution ist ein optionaler Mechanismus, den manche Mostro-Knoten nutzen, um Trades sicherer zu machen. Beim Einstieg in einen Trade hinterlegst du eine kleine Menge Sats als Sicherheit, die vollständig zurückerstattet wird, wenn du ehrlich handelst, aber verloren geht, wenn du versuchst, deine Gegenpartei zu betrügen. So riskiert der Betrüger sein eigenes Geld, was Missbrauch entgegenwirkt.",
1671+
"bondStatusLabel": "Status",
1672+
"bondStatusExplanation": "Ob dieser Knoten zum Handeln eine Anti-Missbrauchs-Kaution verlangt. 'Deaktiviert' bedeutet, dass keine nötig ist.",
1673+
"bondEnabled": "Aktiviert",
1674+
"bondDisabled": "Deaktiviert",
1675+
"bondAppliesToLabel": "Gilt für",
1676+
"bondAppliesToExplanation": "Welche Seite eine Kaution hinterlegen muss: Taker, Maker oder beide.",
1677+
"bondAppliesToTakers": "Taker",
1678+
"bondAppliesToMakers": "Maker",
1679+
"bondAppliesToBoth": "Maker und Taker",
1680+
"bondAmountLabel": "Kautionsbetrag",
1681+
"bondAmountExplanation": "Die Höhe der Kaution: ein Prozentsatz des Orderbetrags, mit einem Mindestbetrag in Sats. Es gilt der größere der beiden Werte.",
1682+
"bondAmountValue": "{percent}% des Betrags (min. {min} Sats)",
1683+
"@bondAmountValue": {
1684+
"placeholders": {
1685+
"percent": {
1686+
"type": "String"
1687+
},
1688+
"min": {
1689+
"type": "String"
1690+
}
1691+
}
1692+
},
1693+
"bondSlashOnTimeoutLabel": "Slash bei Timeout",
1694+
"bondSlashOnTimeoutExplanation": "Ob deine Kaution einbehalten werden kann, wenn du eine Frist verpasst (zum Beispiel nicht zahlst oder nicht rechtzeitig eine Rechnung lieferst). Bei 'Nein' wird sie nur durch die Entscheidung eines Streitschlichters einbehalten.",
1695+
"bondNodeShareLabel": "Knoten-Anteil",
1696+
"bondNodeShareExplanation": "Wenn eine Kaution einbehalten wird, der Anteil, den der Knoten behält. Der Rest wird an die ehrliche Gegenpartei gezahlt.",
1697+
"bondClaimWindowLabel": "Anspruchsfrist",
1698+
"bondClaimWindowExplanation": "Wenn du die ehrliche Partei bist, wenn eine Kaution einbehalten wird, wie viele Tage du hast, um deinen Anteil zu beanspruchen, bevor er verfällt.",
1699+
"bondClaimWindowValue": "{days, plural, =1{1 Tag} other{{days} Tage}}",
1700+
"@bondClaimWindowValue": {
1701+
"placeholders": {
1702+
"days": {
1703+
"type": "int"
1704+
}
1705+
}
1706+
}
16691707
}

lib/l10n/intl_en.arb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1684,5 +1684,43 @@
16841684
"bondPayoutCompletedMessage": "You have received the payment of your share of the anti-abuse bond.",
16851685
"bondPayoutCompletedWithRating": "You have received the payment of your share of the anti-abuse bond. You can rate your counterparty.",
16861686
"bondPayoutCloseButton": "CLOSE",
1687-
"errorLoadingBondPayout": "Error loading payout data"
1687+
"errorLoadingBondPayout": "Error loading payout data",
1688+
"antiAbuseBond": "Anti-abuse bond",
1689+
"antiAbuseBondExplanation": "The anti-abuse bond is an optional mechanism some Mostro nodes use to make trades safer. When you enter a trade you put up a small amount of sats as collateral — fully refunded if you act in good faith, but lost if you try to cheat your counterparty. This puts the cheater's own money at risk, discouraging abuse.",
1690+
"bondStatusLabel": "Status",
1691+
"bondStatusExplanation": "Whether this node requires an anti-abuse bond to trade. 'Disabled' means no bond is needed.",
1692+
"bondEnabled": "Enabled",
1693+
"bondDisabled": "Disabled",
1694+
"bondAppliesToLabel": "Applies to",
1695+
"bondAppliesToExplanation": "Which side must lock a bond: takers, makers, or both.",
1696+
"bondAppliesToTakers": "Takers",
1697+
"bondAppliesToMakers": "Makers",
1698+
"bondAppliesToBoth": "Makers and takers",
1699+
"bondAmountLabel": "Bond amount",
1700+
"bondAmountExplanation": "The bond size: a percentage of the order amount, with a minimum floor in sats. The larger of the two applies.",
1701+
"bondAmountValue": "{percent}% of the order amount (min. {min} sats)",
1702+
"@bondAmountValue": {
1703+
"placeholders": {
1704+
"percent": {
1705+
"type": "String"
1706+
},
1707+
"min": {
1708+
"type": "String"
1709+
}
1710+
}
1711+
},
1712+
"bondSlashOnTimeoutLabel": "Slash on timeout",
1713+
"bondSlashOnTimeoutExplanation": "Whether your bond can be kept if you miss a deadline (for example, not paying or not providing an invoice in time). If 'No', a bond is only kept by a dispute resolver's decision.",
1714+
"bondNodeShareLabel": "Node share",
1715+
"bondNodeShareExplanation": "If a bond is kept, the fraction the node retains. The rest is paid to the honest counterparty.",
1716+
"bondClaimWindowLabel": "Claim window",
1717+
"bondClaimWindowExplanation": "If you are the honest party when a bond is kept, how many days you have to claim your share before it is forfeited.",
1718+
"bondClaimWindowValue": "{days, plural, =1{1 day} other{{days} days}}",
1719+
"@bondClaimWindowValue": {
1720+
"placeholders": {
1721+
"days": {
1722+
"type": "int"
1723+
}
1724+
}
1725+
}
16881726
}

lib/l10n/intl_es.arb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1640,5 +1640,43 @@
16401640
"bondPayoutCompletedMessage": "Ya recibiste el pago de tu parte del bono anti-abuso.",
16411641
"bondPayoutCompletedWithRating": "Ya recibiste el pago de tu parte del bono anti-abuso. Puedes calificar a tu contraparte.",
16421642
"bondPayoutCloseButton": "CERRAR",
1643-
"errorLoadingBondPayout": "Error al cargar los datos del cobro"
1643+
"errorLoadingBondPayout": "Error al cargar los datos del cobro",
1644+
"antiAbuseBond": "Depósito anti-abuso",
1645+
"antiAbuseBondExplanation": "El depósito anti-abuso es un mecanismo opcional que algunos nodos de Mostro usan para hacer los intercambios más seguros. Al entrar en una operación pones en garantía una pequeña cantidad de sats que recuperas por completo si actúas de buena fe, pero que pierdes si intentas estafar a tu contraparte. Así el estafador arriesga su propio dinero, lo que desalienta el abuso.",
1646+
"bondStatusLabel": "Estado",
1647+
"bondStatusExplanation": "Si este nodo exige un depósito anti-abuso para operar. 'Deshabilitado' significa que no hace falta ninguno.",
1648+
"bondEnabled": "Habilitado",
1649+
"bondDisabled": "Deshabilitado",
1650+
"bondAppliesToLabel": "Aplica a",
1651+
"bondAppliesToExplanation": "Qué lado debe bloquear un depósito: takers, makers o ambos.",
1652+
"bondAppliesToTakers": "Takers",
1653+
"bondAppliesToMakers": "Makers",
1654+
"bondAppliesToBoth": "Makers y takers",
1655+
"bondAmountLabel": "Monto del depósito",
1656+
"bondAmountExplanation": "El tamaño del depósito: un porcentaje del monto de la orden, con un mínimo en sats. Se aplica el mayor de los dos.",
1657+
"bondAmountValue": "{percent}% del monto (mín. {min} sats)",
1658+
"@bondAmountValue": {
1659+
"placeholders": {
1660+
"percent": {
1661+
"type": "String"
1662+
},
1663+
"min": {
1664+
"type": "String"
1665+
}
1666+
}
1667+
},
1668+
"bondSlashOnTimeoutLabel": "Slash por timeout",
1669+
"bondSlashOnTimeoutExplanation": "Si tu depósito puede retenerse por no cumplir un plazo (por ejemplo, no pagar o no enviar una factura a tiempo). Si es 'No', solo se retiene por decisión de un resolutor de disputa.",
1670+
"bondNodeShareLabel": "Retención del nodo",
1671+
"bondNodeShareExplanation": "Si un depósito se retiene, la fracción que se queda el nodo. El resto se paga a la contraparte honesta.",
1672+
"bondClaimWindowLabel": "Ventana de reclamo",
1673+
"bondClaimWindowExplanation": "Si eres la parte honesta cuando un depósito se retiene, cuántos días tienes para reclamar tu parte antes de perderla.",
1674+
"bondClaimWindowValue": "{days, plural, =1{1 día} other{{days} días}}",
1675+
"@bondClaimWindowValue": {
1676+
"placeholders": {
1677+
"days": {
1678+
"type": "int"
1679+
}
1680+
}
1681+
}
16441682
}

lib/l10n/intl_fr.arb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1665,5 +1665,43 @@
16651665
"bondPayoutCompletedMessage": "Vous avez reçu le paiement de votre part du dépôt anti-abus.",
16661666
"bondPayoutCompletedWithRating": "Vous avez reçu le paiement de votre part du dépôt anti-abus. Vous pouvez évaluer votre contrepartie.",
16671667
"bondPayoutCloseButton": "FERMER",
1668-
"errorLoadingBondPayout": "Erreur lors du chargement des données de paiement"
1668+
"errorLoadingBondPayout": "Erreur lors du chargement des données de paiement",
1669+
"antiAbuseBond": "Dépôt anti-abus",
1670+
"antiAbuseBondExplanation": "Le dépôt anti-abus est un mécanisme optionnel que certains nœuds Mostro utilisent pour rendre les échanges plus sûrs. En entrant dans un échange, vous déposez une petite quantité de sats en garantie, intégralement remboursée si vous agissez de bonne foi, mais perdue si vous tentez d'escroquer votre contrepartie. Ainsi, l'escroc risque son propre argent, ce qui décourage les abus.",
1671+
"bondStatusLabel": "Statut",
1672+
"bondStatusExplanation": "Si ce nœud exige un dépôt anti-abus pour échanger. 'Désactivé' signifie qu'aucun dépôt n'est requis.",
1673+
"bondEnabled": "Activé",
1674+
"bondDisabled": "Désactivé",
1675+
"bondAppliesToLabel": "S'applique à",
1676+
"bondAppliesToExplanation": "Quel côté doit bloquer un dépôt : preneurs, créateurs ou les deux.",
1677+
"bondAppliesToTakers": "Preneurs",
1678+
"bondAppliesToMakers": "Créateurs",
1679+
"bondAppliesToBoth": "Créateurs et preneurs",
1680+
"bondAmountLabel": "Montant du dépôt",
1681+
"bondAmountExplanation": "La taille du dépôt : un pourcentage du montant de l'ordre, avec un minimum en sats. Le plus élevé des deux s'applique.",
1682+
"bondAmountValue": "{percent}% du montant (min. {min} sats)",
1683+
"@bondAmountValue": {
1684+
"placeholders": {
1685+
"percent": {
1686+
"type": "String"
1687+
},
1688+
"min": {
1689+
"type": "String"
1690+
}
1691+
}
1692+
},
1693+
"bondSlashOnTimeoutLabel": "Slash en cas de délai",
1694+
"bondSlashOnTimeoutExplanation": "Si votre dépôt peut être conservé en cas de délai non respecté (par exemple, ne pas payer ou ne pas fournir une facture à temps). Si 'Non', il n'est conservé que sur décision d'un médiateur de litige.",
1695+
"bondNodeShareLabel": "Part du nœud",
1696+
"bondNodeShareExplanation": "Si un dépôt est conservé, la fraction que le nœud retient. Le reste est versé à la contrepartie honnête.",
1697+
"bondClaimWindowLabel": "Délai de réclamation",
1698+
"bondClaimWindowExplanation": "Si vous êtes la partie honnête lorsqu'un dépôt est conservé, combien de jours vous avez pour réclamer votre part avant de la perdre.",
1699+
"bondClaimWindowValue": "{days, plural, =1{1 jour} other{{days} jours}}",
1700+
"@bondClaimWindowValue": {
1701+
"placeholders": {
1702+
"days": {
1703+
"type": "int"
1704+
}
1705+
}
1706+
}
16691707
}

lib/l10n/intl_it.arb

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1706,5 +1706,43 @@
17061706
"bondPayoutCompletedMessage": "Hai ricevuto il pagamento della tua parte del deposito anti-abuso.",
17071707
"bondPayoutCompletedWithRating": "Hai ricevuto il pagamento della tua parte del deposito anti-abuso. Puoi valutare la tua controparte.",
17081708
"bondPayoutCloseButton": "CHIUDI",
1709-
"errorLoadingBondPayout": "Errore nel caricamento dei dati del riscatto"
1709+
"errorLoadingBondPayout": "Errore nel caricamento dei dati del riscatto",
1710+
"antiAbuseBond": "Deposito anti-abuso",
1711+
"antiAbuseBondExplanation": "Il deposito anti-abuso è un meccanismo opzionale che alcuni nodi Mostro usano per rendere gli scambi più sicuri. Quando entri in uno scambio depositi una piccola quantità di sats come garanzia, che ti viene restituita per intero se agisci in buona fede, ma che perdi se cerchi di truffare la tua controparte. Così il truffatore mette a rischio il proprio denaro, scoraggiando gli abusi.",
1712+
"bondStatusLabel": "Stato",
1713+
"bondStatusExplanation": "Se questo nodo richiede un deposito anti-abuso per fare trading. 'Disabilitato' significa che non ne serve nessuno.",
1714+
"bondEnabled": "Abilitato",
1715+
"bondDisabled": "Disabilitato",
1716+
"bondAppliesToLabel": "Si applica a",
1717+
"bondAppliesToExplanation": "Quale lato deve bloccare un deposito: taker, maker o entrambi.",
1718+
"bondAppliesToTakers": "Taker",
1719+
"bondAppliesToMakers": "Maker",
1720+
"bondAppliesToBoth": "Maker e taker",
1721+
"bondAmountLabel": "Importo del deposito",
1722+
"bondAmountExplanation": "La dimensione del deposito: una percentuale dell'importo dell'ordine, con un minimo in sats. Si applica il maggiore dei due.",
1723+
"bondAmountValue": "{percent}% dell'importo (min. {min} sats)",
1724+
"@bondAmountValue": {
1725+
"placeholders": {
1726+
"percent": {
1727+
"type": "String"
1728+
},
1729+
"min": {
1730+
"type": "String"
1731+
}
1732+
}
1733+
},
1734+
"bondSlashOnTimeoutLabel": "Slash su timeout",
1735+
"bondSlashOnTimeoutExplanation": "Se il tuo deposito può essere trattenuto per il mancato rispetto di una scadenza (ad esempio non pagare o non fornire una fattura in tempo). Se 'No', viene trattenuto solo per decisione di un risolutore di dispute.",
1736+
"bondNodeShareLabel": "Quota del nodo",
1737+
"bondNodeShareExplanation": "Se un deposito viene trattenuto, la frazione che il nodo trattiene. Il resto è pagato alla controparte onesta.",
1738+
"bondClaimWindowLabel": "Finestra di reclamo",
1739+
"bondClaimWindowExplanation": "Se sei la parte onesta quando un deposito viene trattenuto, quanti giorni hai per reclamare la tua quota prima di perderla.",
1740+
"bondClaimWindowValue": "{days, plural, =1{1 giorno} other{{days} giorni}}",
1741+
"@bondClaimWindowValue": {
1742+
"placeholders": {
1743+
"days": {
1744+
"type": "int"
1745+
}
1746+
}
1747+
}
17101748
}

0 commit comments

Comments
 (0)