@@ -6,6 +6,7 @@ import 'package:dart_nostr/nostr/model/event/event.dart';
66import 'package:mostro_mobile/core/config.dart' ;
77import 'package:mostro_mobile/data/models/enums/action.dart' ;
88import 'package:mostro_mobile/data/models/payload.dart' ;
9+ import 'package:mostro_mobile/features/mostro/transport.dart' ;
910import 'package:mostro_mobile/shared/utils/nostr_utils.dart' ;
1011
1112class MostroMessage <T extends Payload > {
@@ -25,9 +26,9 @@ class MostroMessage<T extends Payload> {
2526 this .timestamp,
2627 }) : _payload = payload;
2728
28- Map <String , dynamic > toJson () {
29+ Map <String , dynamic > toJson ({ int ? version} ) {
2930 Map <String , dynamic > json = {
30- 'version' : Config .mostroVersion,
31+ 'version' : version ?? Config .mostroVersion,
3132 'request_id' : requestId,
3233 'trade_index' : tradeIndex,
3334 };
@@ -97,30 +98,37 @@ class MostroMessage<T extends Payload> {
9798 return null ;
9899 }
99100
100- String sign (NostrKeyPairs keyPair) {
101+ String sign (NostrKeyPairs keyPair, { int ? version} ) {
101102 //IMPORTANT : Use 'restore' key for restore and last-trade-index actions, 'order' for everything else, as per protocol
102103 final wrapperKey =
103104 action == Action .restore || action == Action .lastTradeIndex
104105 ? 'restore'
105106 : 'order' ;
106- final message = {wrapperKey: toJson ()};
107+ final message = {wrapperKey: toJson (version : version )};
107108 final serializedEvent = jsonEncode (message);
108- final bytes = utf8.encode (serializedEvent);
109+ return _mostroSign (serializedEvent, keyPair);
110+ }
111+
112+ /// Signs a UTF-8 string the Mostro way: SHA-256 digest, hex-encoded, then
113+ /// Schnorr-signed. Shared by the message [sign] and the protocol-v2 identity
114+ /// proof so both produce signatures the daemon can verify identically.
115+ String _mostroSign (String message, NostrKeyPairs keyPair) {
116+ final bytes = utf8.encode (message);
109117 final digest = sha256.convert (bytes);
110118 final hash = hex.encode (digest.bytes);
111- final signature = keyPair.sign (hash);
112- return signature;
119+ return keyPair.sign (hash);
113120 }
114121
115- String serialize ({NostrKeyPairs ? keyPair}) {
122+ String serialize ({NostrKeyPairs ? keyPair, int ? version }) {
116123 //IMPORTANT : Use 'restore' key for restore and last-trade-index actions, 'order' for everything else, as per protocol
117124 final wrapperKey =
118125 action == Action .restore || action == Action .lastTradeIndex
119126 ? 'restore'
120127 : 'order' ;
121- final message = {wrapperKey: toJson ()};
128+ final message = {wrapperKey: toJson (version : version )};
122129 final serializedEvent = jsonEncode (message);
123- final signature = (keyPair != null ) ? '"${sign (keyPair )}"' : null ;
130+ final signature =
131+ (keyPair != null ) ? '"${sign (keyPair , version : version )}"' : null ;
124132 final content = '[$serializedEvent , $signature ]' ;
125133 return content;
126134 }
@@ -159,4 +167,106 @@ class MostroMessage<T extends Payload> {
159167 difficulty: difficulty,
160168 );
161169 }
170+
171+ /// Wraps the message for protocol v2 (NIP-44 direct, kind 14).
172+ ///
173+ /// Produces the 3-tuple `[message, tradeSig, identityProof]` (§3.3), NIP-44
174+ /// encrypts it toward [recipientPubKey] with the trade key, and emits a
175+ /// kind-14 event **signed by the trade key** carrying a `p` tag. Mirrors
176+ /// `mostro-core` 's `transport.rs` wrap:
177+ /// - the message JSON carries `version: 2` ;
178+ /// - in reputation mode (master key present) `tradeSig` is the trade-key
179+ /// signature over the message and `identityProof` is
180+ /// `[identityPubkey, sig]` where the signature is over
181+ /// `mostro-transport-v2-identity:<tradePubkey>:<messageJSON>` made with the
182+ /// master key;
183+ /// - in full-privacy mode (no master key) both are `null` .
184+ ///
185+ /// PoW (NIP-13), when [difficulty] > 0, is mined on the kind-14 event id and
186+ /// signed by the trade key — the first-contact lane is preserved.
187+ Future <NostrEvent > wrapNip44 ({
188+ required NostrKeyPairs tradeKey,
189+ required String recipientPubKey,
190+ NostrKeyPairs ? masterKey,
191+ int ? keyIndex,
192+ int difficulty = 0 ,
193+ }) async {
194+ tradeIndex = keyIndex;
195+
196+ final wrapperKey =
197+ action == Action .restore || action == Action .lastTradeIndex
198+ ? 'restore'
199+ : 'order' ;
200+ final messageMap = {wrapperKey: toJson (version: 2 )};
201+ final messageJson = jsonEncode (messageMap);
202+
203+ // Reputation mode binds the identity; full privacy omits both signatures.
204+ final String ? tradeSig =
205+ masterKey != null ? _mostroSign (messageJson, tradeKey) : null ;
206+
207+ List <String >? identityProof;
208+ if (masterKey != null ) {
209+ final payload =
210+ 'mostro-transport-v2-identity:${tradeKey .public }:$messageJson ' ;
211+ identityProof = [masterKey.public, _mostroSign (payload, masterKey)];
212+ }
213+
214+ final tuple = jsonEncode ([messageMap, tradeSig, identityProof]);
215+
216+ final encrypted = await NostrUtils .encryptNIP44 (
217+ tuple,
218+ tradeKey.private,
219+ recipientPubKey,
220+ );
221+
222+ final event = NostrEvent .fromPartialData (
223+ kind: 14 ,
224+ content: encrypted,
225+ keyPairs: tradeKey,
226+ tags: [
227+ ['p' , recipientPubKey],
228+ ],
229+ createdAt: DateTime .now (),
230+ );
231+
232+ if (difficulty > 0 ) {
233+ return NostrUtils .mineProofOfWork (event, difficulty, tradeKey);
234+ }
235+ return event;
236+ }
237+
238+ /// Wraps the message for the transport advertised by the node's
239+ /// [protocolVersion] (§5 Phase B): v2 (NIP-44 direct, kind 14) via
240+ /// [wrapNip44] or v1 (gift wrap, kind 1059) via [wrap] .
241+ ///
242+ /// Single entry point so every outbound Mostro send — order actions, restore
243+ /// requests, dispute creation — selects the transport consistently from the
244+ /// connected node, instead of hard-coding the v1 path.
245+ Future <NostrEvent > wrapForTransport ({
246+ required int ? protocolVersion,
247+ required NostrKeyPairs tradeKey,
248+ required String recipientPubKey,
249+ NostrKeyPairs ? masterKey,
250+ int ? keyIndex,
251+ int difficulty = 0 ,
252+ }) {
253+ switch (resolveTransport (protocolVersion)) {
254+ case Transport .nip44:
255+ return wrapNip44 (
256+ tradeKey: tradeKey,
257+ recipientPubKey: recipientPubKey,
258+ masterKey: masterKey,
259+ keyIndex: keyIndex,
260+ difficulty: difficulty,
261+ );
262+ case Transport .giftWrap:
263+ return wrap (
264+ tradeKey: tradeKey,
265+ recipientPubKey: recipientPubKey,
266+ masterKey: masterKey,
267+ keyIndex: keyIndex,
268+ difficulty: difficulty,
269+ );
270+ }
271+ }
162272}
0 commit comments