Skip to content

Commit ede339e

Browse files
authored
Merge pull request #381 from relaystr/feat/gift-wrap-custom-signer
Feat: gift wrap add custom signer parameter
2 parents 8f7582e + 0ae978d commit ede339e

File tree

2 files changed

+118
-20
lines changed

2 files changed

+118
-20
lines changed

packages/ndk/lib/domain_layer/usecases/gift_wrap/gift_wrap.dart

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'dart:convert';
33
import '../../../data_layer/models/nip_01_event_model.dart';
44
import '../../../data_layer/repositories/signers/bip340_event_signer.dart';
55
import '../../entities/nip_01_event.dart';
6+
import '../../repositories/event_signer.dart';
67
import '../accounts/accounts.dart';
78
import '../../../shared/nips/nip01/bip340.dart';
89

@@ -14,16 +15,34 @@ class GiftWrap {
1415

1516
GiftWrap({required this.accounts});
1617

18+
/// Returns the signer to use for signing operations.
19+
/// Uses [customSigner] if provided, otherwise falls back to logged-in account's signer.
20+
EventSigner _getSigner({EventSigner? customSigner}) {
21+
if (customSigner != null) {
22+
return customSigner;
23+
}
24+
final account = accounts.getLoggedAccount();
25+
if (account == null) {
26+
throw Exception("cannot sign without account or custom signer");
27+
}
28+
return account.signer;
29+
}
30+
1731
/// converts a Nip01Event to a giftWrap Nip01Event \
1832
/// [rumor] the event you want to wrap \
1933
/// [recipientPubkey] the reciever of the rumor \
34+
/// [customSigner] optional signer to use instead of the logged-in account's signer \
2035
/// [returns] the wrapped event
2136
Future<Nip01Event> toGiftWrap({
2237
required Nip01Event rumor,
2338
required String recipientPubkey,
39+
EventSigner? customSigner,
2440
}) async {
25-
final sealedRumor =
26-
await sealRumor(rumor: rumor, recipientPubkey: recipientPubkey);
41+
final sealedRumor = await sealRumor(
42+
rumor: rumor,
43+
recipientPubkey: recipientPubkey,
44+
customSigner: customSigner,
45+
);
2746

2847
final giftWrap = await wrapEvent(
2948
recipientPublicKey: recipientPubkey,
@@ -34,16 +53,24 @@ class GiftWrap {
3453

3554
/// Unwraps a gift-wrapped event to retrieve the original rumor \
3655
/// [giftWrap] the gift-wrapped event to unwrap \
56+
/// [customSigner] optional signer to use instead of the logged-in account's signer \
3757
/// [returns] the original rumor event
3858
Future<Nip01Event> fromGiftWrap({
3959
required Nip01Event giftWrap,
60+
EventSigner? customSigner,
4061
}) async {
4162
if (giftWrap.kind != kGiftWrapEventkind) {
4263
throw Exception("Event is not a gift wrap (kind:1059)");
4364
}
4465

45-
final sealEvent = await unwrapEvent(wrappedEvent: giftWrap);
46-
final rumor = await unsealRumor(sealedEvent: sealEvent);
66+
final sealEvent = await unwrapEvent(
67+
wrappedEvent: giftWrap,
68+
customSigner: customSigner,
69+
);
70+
final rumor = await unsealRumor(
71+
sealedEvent: sealEvent,
72+
customSigner: customSigner,
73+
);
4774

4875
return rumor;
4976
}
@@ -75,16 +102,15 @@ class GiftWrap {
75102

76103
/// Seals a rumor (creates a kind:13 event)
77104
///
105+
/// [customSigner] optional signer to use instead of the logged-in account's signer
78106
Future<Nip01Event> sealRumor({
79107
required Nip01Event rumor,
80108
required String recipientPubkey,
109+
EventSigner? customSigner,
81110
}) async {
82-
final account = accounts.getLoggedAccount();
83-
if (account == null) {
84-
throw Exception("cannot sign without account");
85-
}
111+
final signer = _getSigner(customSigner: customSigner);
86112

87-
final encryptedContent = await account.signer.encryptNip44(
113+
final encryptedContent = await signer.encryptNip44(
88114
plaintext: Nip01EventModel.fromEntity(rumor).toJsonString(),
89115
recipientPubKey: recipientPubkey,
90116
);
@@ -94,7 +120,7 @@ class GiftWrap {
94120
}
95121

96122
final sealEvent = Nip01Event(
97-
pubKey: account.pubkey,
123+
pubKey: signer.getPublicKey(),
98124
kind: kSealEventKind,
99125
tags: [],
100126
content: encryptedContent,
@@ -103,15 +129,17 @@ class GiftWrap {
103129
return sealEvent;
104130
}
105131

132+
/// Unseals a sealed event to retrieve the rumor
133+
///
134+
/// [customSigner] optional signer to use instead of the logged-in account's signer
106135
Future<Nip01Event> unsealRumor({
107136
required Nip01Event sealedEvent,
137+
EventSigner? customSigner,
108138
}) async {
109-
final account = accounts.getLoggedAccount();
110-
if (account == null) {
111-
throw Exception("Cannot decrypt without account");
112-
}
139+
final signer = _getSigner(customSigner: customSigner);
140+
113141
// Now decrypt the seal to get the rumor
114-
final decryptedRumorJson = await account.signer.decryptNip44(
142+
final decryptedRumorJson = await signer.decryptNip44(
115143
ciphertext: sealedEvent.content,
116144
senderPubKey: sealedEvent.pubKey,
117145
);
@@ -180,15 +208,16 @@ class GiftWrap {
180208
return gWEventSigned;
181209
}
182210

211+
/// Unwraps a gift wrap to get the seal event
212+
///
213+
/// [customSigner] optional signer to use instead of the logged-in account's signer
183214
Future<Nip01Event> unwrapEvent({
184215
required Nip01Event wrappedEvent,
216+
EventSigner? customSigner,
185217
}) async {
186-
final account = accounts.getLoggedAccount();
187-
if (account == null) {
188-
throw Exception("Cannot decrypt without account");
189-
}
218+
final signer = _getSigner(customSigner: customSigner);
190219

191-
final decryptedEventJson = await account.signer.decryptNip44(
220+
final decryptedEventJson = await signer.decryptNip44(
192221
ciphertext: wrappedEvent.content,
193222
senderPubKey: wrappedEvent.pubKey,
194223
);

packages/ndk/test/usecases/gift_wrap/gift_wrap_test.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ void main() {
1111
// Test keys
1212
final key1 = Bip340.generatePrivateKey();
1313
final key2 = Bip340.generatePrivateKey();
14+
final key3 = Bip340.generatePrivateKey(); // For custom signer tests
1415

1516
setUp(() {
1617
ndk = Ndk(
@@ -111,5 +112,73 @@ void main() {
111112
.any((tag) => tag[0] == 'client' && tag[1] == 'test_client'),
112113
isTrue);
113114
});
115+
116+
test(
117+
'Can use custom signer instead of logged-in account for wrap and unwrap',
118+
() async {
119+
// Create a custom signer with key3 (different from logged-in key1)
120+
final customSigner = Bip340EventSigner(
121+
privateKey: key3.privateKey,
122+
publicKey: key3.publicKey,
123+
);
124+
125+
// Create a rumor with custom pubkey matching the custom signer
126+
final originalRumor = await giftWrapService.createRumor(
127+
customPubkey: key3.publicKey,
128+
content: 'Test message with custom signer',
129+
kind: 1,
130+
tags: [],
131+
);
132+
133+
// Wrap the rumor using the custom signer (not the logged-in account)
134+
final giftWrap = await giftWrapService.toGiftWrap(
135+
rumor: originalRumor,
136+
recipientPubkey: key2.publicKey,
137+
customSigner: customSigner,
138+
);
139+
140+
// Create a signer for the recipient to unwrap
141+
final recipientSigner = Bip340EventSigner(
142+
privateKey: key2.privateKey,
143+
publicKey: key2.publicKey,
144+
);
145+
146+
// Unwrap using the custom signer (without switching logged-in account)
147+
final unwrappedRumor = await giftWrapService.fromGiftWrap(
148+
giftWrap: giftWrap,
149+
customSigner: recipientSigner,
150+
);
151+
152+
// Verify the unwrapped rumor matches the original
153+
expect(unwrappedRumor.content, equals(originalRumor.content));
154+
expect(unwrappedRumor.kind, equals(originalRumor.kind));
155+
expect(unwrappedRumor.pubKey, equals(key3.publicKey));
156+
});
157+
158+
test('Custom signer seal uses correct pubkey', () async {
159+
// Create a custom signer
160+
final customSigner = Bip340EventSigner(
161+
privateKey: key3.privateKey,
162+
publicKey: key3.publicKey,
163+
);
164+
165+
// Create a rumor
166+
final rumor = await giftWrapService.createRumor(
167+
customPubkey: key3.publicKey,
168+
content: 'Test seal pubkey',
169+
kind: 1,
170+
tags: [],
171+
);
172+
173+
// Seal using the custom signer
174+
final seal = await giftWrapService.sealRumor(
175+
rumor: rumor,
176+
recipientPubkey: key2.publicKey,
177+
customSigner: customSigner,
178+
);
179+
180+
// Verify the seal uses the custom signer's pubkey
181+
expect(seal.pubKey, equals(key3.publicKey));
182+
});
114183
});
115184
}

0 commit comments

Comments
 (0)