Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 138 additions & 3 deletions lib/features/mostro/mostro_instance.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
import 'package:dart_nostr/nostr/model/event/event.dart';

/// Anti-abuse bond policy advertised by a Mostro daemon via the kind-38385
/// info event.
///
/// Three states must be distinguished:
/// - [unsupported]: the daemon does not emit the `bond_enabled` tag at all
/// (legacy daemon that predates the anti-abuse bond feature).
/// - [disabled]: the daemon emits `bond_enabled="false"`; the operator has
/// not enabled the feature.
/// - [enabled]: the daemon emits `bond_enabled="true"`; the bond is active
/// and the remaining six bond tags are present.
enum BondPolicy { unsupported, disabled, enabled }

/// Which side of a trade a bond applies to.
enum BondApplyTo { take, make, both }

class MostroInstance {
final String pubKey;
final String mostroVersion;
Expand All @@ -23,6 +38,19 @@ class MostroInstance {
final String fiatCurrenciesAccepted;
final int maxOrdersPerResponse;

/// Bond policy state. See [BondPolicy] for the three-state semantics.
final BondPolicy bondPolicy;

/// The following six fields carry the bond parameters and are only
/// meaningful when [bondPolicy] is [BondPolicy.enabled]. They are null
/// otherwise.
final BondApplyTo? bondApplyTo;
final bool? bondSlashOnWaitingTimeout;
final double? bondAmountPct;
final int? bondBaseAmountSats;
final double? bondSlashNodeSharePct;
final int? bondPayoutClaimWindowDays;

MostroInstance(
this.pubKey,
this.mostroVersion,
Expand All @@ -44,8 +72,15 @@ class MostroInstance {
this.supportedNetworks,
this.lndNodeUri,
this.fiatCurrenciesAccepted,
this.maxOrdersPerResponse,
);
this.maxOrdersPerResponse, {
this.bondPolicy = BondPolicy.unsupported,
this.bondApplyTo,
this.bondSlashOnWaitingTimeout,
this.bondAmountPct,
this.bondBaseAmountSats,
this.bondSlashNodeSharePct,
this.bondPayoutClaimWindowDays,
});

factory MostroInstance.fromEvent(NostrEvent event) {
return MostroInstance(
Expand All @@ -70,6 +105,13 @@ class MostroInstance {
event.lndNodeUri,
event.fiatCurrenciesAccepted,
event.maxOrdersPerResponse,
bondPolicy: event.bondPolicy,
bondApplyTo: event.bondApplyTo,
bondSlashOnWaitingTimeout: event.bondSlashOnWaitingTimeout,
bondAmountPct: event.bondAmountPct,
bondBaseAmountSats: event.bondBaseAmountSats,
bondSlashNodeSharePct: event.bondSlashNodeSharePct,
bondPayoutClaimWindowDays: event.bondPayoutClaimWindowDays,
);
}
}
Expand All @@ -80,6 +122,25 @@ extension MostroInstanceExtensions on NostrEvent {
return (tag != null && tag.length > 1) ? tag[1] : 'Tag: $key not found';
}

/// Returns the tag value, or null when the tag is missing or empty.
///
/// Use this for optional tags where absence is semantically meaningful
/// (e.g. anti-abuse bond tags, which only appear on modern daemons).
///
/// Empty or whitespace-only values are treated as missing so they cannot
/// be misparsed as legitimate data downstream (e.g. an empty
/// `bond_enabled=""` would otherwise be classified as `disabled` instead
/// of `unsupported`, breaking the three-state semantics).
String? _getOptionalTagValue(String key) {
final tag = tags?.firstWhere(
(t) => t.isNotEmpty && t[0] == key,
orElse: () => const <String>[],
);
if (tag == null || tag.length < 2) return null;
final value = tag[1].trim();
return value.isEmpty ? null : value;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

String get pubKey => _getTagValue('d');
String get mostroVersion => _getTagValue('mostro_version');
String get commitHash => _getTagValue('mostro_commit_hash');
Expand All @@ -103,5 +164,79 @@ extension MostroInstanceExtensions on NostrEvent {
String get supportedNetworks => _getTagValue('lnd_networks');
String get lndNodeUri => _getTagValue('lnd_uris');
String get fiatCurrenciesAccepted => _getTagValue('fiat_currencies_accepted');
int get maxOrdersPerResponse => int.parse(_getTagValue('max_orders_per_response'));
int get maxOrdersPerResponse =>
int.parse(_getTagValue('max_orders_per_response'));

/// Parses the anti-abuse bond policy from the `bond_enabled` tag.
///
/// - Tag absent → [BondPolicy.unsupported] (legacy daemon).
/// - `"true"` → [BondPolicy.enabled].
/// - Any other value → [BondPolicy.disabled].
BondPolicy get bondPolicy {
final raw = _getOptionalTagValue('bond_enabled');
if (raw == null) return BondPolicy.unsupported;
return raw.toLowerCase() == 'true'
? BondPolicy.enabled
: BondPolicy.disabled;
}

BondApplyTo? get bondApplyTo {
final raw = _getOptionalTagValue('bond_apply_to');
switch (raw) {
case 'take':
return BondApplyTo.take;
case 'make':
return BondApplyTo.make;
case 'both':
return BondApplyTo.both;
Comment thread
AndreaDiazCorreia marked this conversation as resolved.
default:
return null;
}
}

/// Parses `bond_slash_on_waiting_timeout`. Returns `null` for any value
/// other than `"true"` or `"false"` (case-insensitive) so malformed data
/// is not silently collapsed into a valid policy state.
bool? get bondSlashOnWaitingTimeout {
final raw = _getOptionalTagValue('bond_slash_on_waiting_timeout')
?.toLowerCase();
if (raw == 'true') return true;
if (raw == 'false') return false;
return null;
}

/// Bond fraction of the order amount. Must be a percentage in `[0.0, 1.0]`;
/// out-of-range values are treated as invalid and yield `null`.
double? get bondAmountPct {
final raw = _getOptionalTagValue('bond_amount_pct');
final value = raw == null ? null : double.tryParse(raw);
if (value == null) return null;
return (value >= 0.0 && value <= 1.0) ? value : null;
}

/// Minimum bond floor in sats. Negative values are treated as invalid.
int? get bondBaseAmountSats {
final raw = _getOptionalTagValue('bond_base_amount_sats');
final value = raw == null ? null : int.tryParse(raw);
if (value == null) return null;
return value >= 0 ? value : null;
}

/// Node share of a slashed bond. Spec constrains this to `[0.0, 1.0]`;
/// out-of-range values are treated as invalid and yield `null`.
double? get bondSlashNodeSharePct {
final raw = _getOptionalTagValue('bond_slash_node_share_pct');
final value = raw == null ? null : double.tryParse(raw);
if (value == null) return null;
return (value >= 0.0 && value <= 1.0) ? value : null;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// Payout claim window in days. Must be positive; non-positive or
/// unparseable values yield `null`.
int? get bondPayoutClaimWindowDays {
final raw = _getOptionalTagValue('bond_payout_claim_window_days');
final value = raw == null ? null : int.tryParse(raw);
if (value == null) return null;
return value > 0 ? value : null;
}
}
Loading
Loading