applesauce-wallet-connect 3.1.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,10 @@ import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
2
2
  import { simpleTimeout } from "applesauce-core";
3
3
  import { create } from "applesauce-factory";
4
4
  import { finalizeEvent, getPublicKey, nip04, nip44, verifyEvent } from "nostr-tools";
5
- import { BehaviorSubject, defer, filter, firstValueFrom, from, fromEvent, identity, ignoreElements, lastValueFrom, map, merge, mergeMap, repeat, ReplaySubject, retry, share, switchMap, takeUntil, tap, timer, toArray, } from "rxjs";
5
+ import { BehaviorSubject, combineLatest, defer, filter, firstValueFrom, from, fromEvent, identity, ignoreElements, lastValueFrom, map, merge, mergeMap, of, repeat, ReplaySubject, retry, share, switchMap, take, takeUntil, tap, timer, toArray, } from "rxjs";
6
6
  import { WalletRequestBlueprint } from "./blueprints/index.js";
7
7
  import { createWalletError } from "./helpers/error.js";
8
- import { createWalletAuthURI, getPreferredEncryption, getWalletNotification, getWalletRequestEncryption, getWalletResponse, getWalletResponseRequestId, getWalletSupport, isWalletNotificationLocked, isWalletResponseLocked, parseWalletConnectURI, supportsMethod, supportsNotifications, supportsNotificationType, unlockWalletNotification, unlockWalletResponse, WALLET_INFO_KIND, WALLET_LEGACY_NOTIFICATION_KIND, WALLET_NOTIFICATION_KIND, WALLET_RESPONSE_KIND, } from "./helpers/index.js";
8
+ import { createWalletAuthURI, getPreferredEncryption, getWalletRequestEncryption, getWalletResponseRequestId, getWalletSupport, isValidWalletNotification, isValidWalletResponse, parseWalletConnectURI, supportsMethod, supportsNotifications, supportsNotificationType, unlockWalletNotification, unlockWalletResponse, WALLET_INFO_KIND, WALLET_LEGACY_NOTIFICATION_KIND, WALLET_NOTIFICATION_KIND, WALLET_RESPONSE_KIND, } from "./helpers/index.js";
9
9
  import { getConnectionMethods, } from "./interop.js";
10
10
  export class WalletConnect {
11
11
  /** A fallback method to use for subscriptionMethod if none is passed in when creating the client */
@@ -22,7 +22,12 @@ export class WalletConnect {
22
22
  secret;
23
23
  signer;
24
24
  /** The relays to use for the connection */
25
- relays;
25
+ relays$ = new BehaviorSubject([]);
26
+ get relays() {
27
+ return this.relays$.value;
28
+ }
29
+ /** Whether to accept the relay hint from the wallet service */
30
+ acceptRelayHint;
26
31
  /** The wallet service public key ( unset if waiting for service ) */
27
32
  service$ = new BehaviorSubject(undefined);
28
33
  get service() {
@@ -42,7 +47,8 @@ export class WalletConnect {
42
47
  waitForService$;
43
48
  constructor(options) {
44
49
  this.secret = options.secret;
45
- this.relays = options.relays;
50
+ this.relays$.next(options.relays);
51
+ this.acceptRelayHint = options.acceptRelayHint ?? true;
46
52
  this.service$.next(options.service);
47
53
  this.defaultTimeout = options.timeout || 30000; // 30 second default timeout
48
54
  // Create a signer for the factory
@@ -64,18 +70,18 @@ export class WalletConnect {
64
70
  this.subscriptionMethod = (relays, filters) => subscriptionMethod(relays, filters);
65
71
  this.publishMethod = (relays, event) => publishMethod(relays, event);
66
72
  // Create shared observable for all wallet events
67
- this.events$ = this.service$.pipe(switchMap((service) => {
73
+ this.events$ = combineLatest([this.service$, this.relays$]).pipe(switchMap(([service, relays]) => {
68
74
  const client = getPublicKey(this.secret);
69
75
  // If the service is not known yet, subscribe to a wallet info event tagging the client
70
76
  if (!service)
71
- return from(this.subscriptionMethod(this.relays, [{ kinds: [WALLET_INFO_KIND], "#p": [client] }])).pipe(
77
+ return from(this.subscriptionMethod(relays, [{ kinds: [WALLET_INFO_KIND], "#p": [client] }])).pipe(
72
78
  // Keep the connection open indefinitely
73
79
  repeat(),
74
80
  // Retry on connection failure
75
81
  retry(),
76
82
  // Ignore strings (support for applesauce-relay)
77
83
  filter((event) => typeof event !== "string"));
78
- return from(this.subscriptionMethod(this.relays, [
84
+ return from(this.subscriptionMethod(relays, [
79
85
  // Subscribe to response events
80
86
  {
81
87
  kinds: [WALLET_RESPONSE_KIND, WALLET_NOTIFICATION_KIND, WALLET_LEGACY_NOTIFICATION_KIND],
@@ -103,19 +109,30 @@ export class WalletConnect {
103
109
  resetOnRefCountZero: () => timer(60000), // Keep info observable around for 1 minute after last unsubscribe
104
110
  }));
105
111
  this.encryption$ = this.support$.pipe(map((info) => (info ? getPreferredEncryption(info) : "nip04")));
106
- this.notifications$ = this.events$.pipe(filter((event) => event.kind === WALLET_NOTIFICATION_KIND), mergeMap((event) => this.handleNotificationEvent(event)));
107
- this.waitForService$ = this.events$.pipe(
108
- // Complete when the service is set
109
- takeUntil(this.service$),
110
- // Only listen for wallet info events
111
- filter((event) => event.kind === WALLET_INFO_KIND && !this.service),
112
- // Set the service to the pubkey of the wallet info event
113
- tap((event) => {
114
- // Set the service to the pubkey of the wallet info event
115
- this.service$.next(event.pubkey);
116
- }),
117
- // Get the service pubkey from the event
118
- map((event) => event.pubkey),
112
+ this.notifications$ = this.events$.pipe(filter((event) => isValidWalletNotification(event)), mergeMap((event) => this.handleNotificationEvent(event)));
113
+ this.waitForService$ = defer(() =>
114
+ // If service is already set, return it
115
+ this.service$.value
116
+ ? of(this.service$.value)
117
+ : // Otherwise listen for new wallet info events
118
+ this.events$.pipe(
119
+ // Only listen for wallet info events
120
+ filter((event) => event.kind === WALLET_INFO_KIND),
121
+ // Set the service to the pubkey of the wallet info event
122
+ tap((event) => {
123
+ // Set the service to the pubkey of the wallet info event
124
+ this.service$.next(event.pubkey);
125
+ // Switch to the relay from the service if its set
126
+ if (this.acceptRelayHint) {
127
+ const relay = event.tags.find((t) => t[0] === "p" && t[2])?.[2];
128
+ if (relay)
129
+ this.relays$.next([relay]);
130
+ }
131
+ }),
132
+ // Get the service pubkey from the event
133
+ map((event) => event.pubkey),
134
+ // Complete after the first value
135
+ take(1))).pipe(
119
136
  // Only create a single subscription to avoid multiple side effects
120
137
  share());
121
138
  }
@@ -126,11 +143,7 @@ export class WalletConnect {
126
143
  const requestId = getWalletResponseRequestId(event);
127
144
  if (!requestId)
128
145
  throw new Error("Response missing request ID");
129
- let response;
130
- if (isWalletResponseLocked(event))
131
- response = await unlockWalletResponse(event, this.signer, encryption);
132
- else
133
- response = getWalletResponse(event);
146
+ const response = await unlockWalletResponse(event, this.signer, encryption);
134
147
  if (!response)
135
148
  throw new Error("Failed to decrypt or parse response");
136
149
  return response;
@@ -139,25 +152,21 @@ export class WalletConnect {
139
152
  async handleNotificationEvent(event) {
140
153
  if (!verifyEvent(event))
141
154
  throw new Error("Invalid notification event signature");
142
- let notification;
143
- if (isWalletNotificationLocked(event))
144
- notification = await unlockWalletNotification(event, this.signer);
145
- else
146
- notification = getWalletNotification(event);
155
+ const notification = await unlockWalletNotification(event, this.signer);
147
156
  if (!notification)
148
157
  throw new Error("Failed to decrypt or parse notification");
149
158
  return notification;
150
159
  }
151
- /** Core RPC method that makes a request and returns the response */
152
- request(request, options = {}) {
160
+ /** Generic call method with generic type */
161
+ genericCall(method, params, options = {}) {
153
162
  if (!this.service)
154
163
  throw new Error("WalletConnect is not connected to a service");
155
- // Create the request evnet
164
+ // Create the request event
156
165
  return defer(async () => {
157
166
  // Get the preferred encryption method for the wallet
158
167
  const encryption = await firstValueFrom(this.encryption$);
159
168
  // Create the request event
160
- const draft = await create({ signer: this.signer }, WalletRequestBlueprint, this.service, request, encryption);
169
+ const draft = await create({ signer: this.signer }, WalletRequestBlueprint, this.service, { method, params }, encryption);
161
170
  // Sign the request event
162
171
  return await this.signer.signEvent(draft);
163
172
  }).pipe(
@@ -167,12 +176,29 @@ export class WalletConnect {
167
176
  // Create an observable that publishes the request event when subscribed to
168
177
  const request$ = defer(() => from(this.publishMethod(this.relays, requestEvent))).pipe(ignoreElements());
169
178
  // Create an observable that listens for response events
170
- const responses$ = this.events$.pipe(filter((response) => response.kind === WALLET_RESPONSE_KIND && getWalletResponseRequestId(response) === requestEvent.id), mergeMap((response) => this.handleResponseEvent(response, encryption)),
179
+ const responses$ = this.events$.pipe(filter(isValidWalletResponse), filter((response) => getWalletResponseRequestId(response) === requestEvent.id), mergeMap((response) => this.handleResponseEvent(response, encryption)),
171
180
  // Set timeout for response events
172
181
  simpleTimeout(options.timeout || this.defaultTimeout));
173
182
  return merge(request$, responses$);
174
183
  }));
175
184
  }
185
+ /** Request method with generic type */
186
+ async genericRequest(method, params, options = {}) {
187
+ const result = await firstValueFrom(this.genericCall(method, params, options));
188
+ if (result.result_type !== method)
189
+ throw new Error(`Unexpected response type: ${result.result_type}`);
190
+ if (result.error)
191
+ throw createWalletError(result.error.type, result.error.message);
192
+ return result.result;
193
+ }
194
+ /** Call a method and return an observable of results */
195
+ call(method, params, options = {}) {
196
+ return this.genericCall(method, params, options);
197
+ }
198
+ /** Typed request method, returns the result or throws and error */
199
+ async request(method, params, options = {}) {
200
+ return this.genericRequest(method, params, options);
201
+ }
176
202
  /**
177
203
  * Listen for a type of notification
178
204
  * @returns a method to unsubscribe the listener
@@ -215,18 +241,14 @@ export class WalletConnect {
215
241
  const support = await this.getSupport();
216
242
  return support ? supportsNotificationType(support, type) : false;
217
243
  }
244
+ // Methods for common types
218
245
  /** Pay a lightning invoice */
219
246
  async payInvoice(invoice, amount) {
220
- const response = await firstValueFrom(this.request({ method: "pay_invoice", params: { invoice, amount } }));
221
- if (response.result_type !== "pay_invoice")
222
- throw new Error(`Unexpected response type: ${response.result_type}`);
223
- if (response.error)
224
- throw createWalletError(response.error.type, response.error.message);
225
- return response.result;
247
+ return await this.genericRequest("pay_invoice", { invoice, amount });
226
248
  }
227
249
  /** Pay multiple lightning invoices */
228
250
  async payMultipleInvoices(invoices) {
229
- return await lastValueFrom(this.request({ method: "multi_pay_invoice", params: { invoices } })
251
+ return await lastValueFrom(this.genericCall("multi_pay_invoice", { invoices })
230
252
  .pipe(map((response) => {
231
253
  if (response.result_type !== "multi_pay_invoice")
232
254
  throw new Error(`Unexpected response type: ${response.result_type}`);
@@ -238,16 +260,11 @@ export class WalletConnect {
238
260
  }
239
261
  /** Send a keysend payment */
240
262
  async payKeysend(pubkey, amount, preimage, tlv_records) {
241
- const response = await firstValueFrom(this.request({ method: "pay_keysend", params: { pubkey, amount, preimage, tlv_records } }));
242
- if (response.result_type !== "pay_keysend")
243
- throw new Error(`Unexpected response type: ${response.result_type}`);
244
- if (response.error)
245
- throw createWalletError(response.error.type, response.error.message);
246
- return response.result;
263
+ return await this.genericRequest("pay_keysend", { pubkey, amount, preimage, tlv_records });
247
264
  }
248
265
  /** Send multiple keysend payments */
249
266
  async payMultipleKeysend(keysends) {
250
- return lastValueFrom(this.request({ method: "multi_pay_keysend", params: { keysends } }).pipe(map((response) => {
267
+ return lastValueFrom(this.genericCall("multi_pay_keysend", { keysends }).pipe(map((response) => {
251
268
  if (response.result_type !== "multi_pay_keysend")
252
269
  throw new Error(`Unexpected response type: ${response.result_type}`);
253
270
  if (response.error)
@@ -257,48 +274,23 @@ export class WalletConnect {
257
274
  }
258
275
  /** Create a new invoice */
259
276
  async makeInvoice(amount, options) {
260
- const response = await firstValueFrom(this.request({ method: "make_invoice", params: { amount, ...options } }));
261
- if (response.result_type !== "make_invoice")
262
- throw new Error(`Unexpected response type: ${response.result_type}`);
263
- if (response.error)
264
- throw createWalletError(response.error.type, response.error.message);
265
- return response.result;
277
+ return await this.genericRequest("make_invoice", { amount, ...options });
266
278
  }
267
279
  /** Look up an invoice by payment hash or invoice string */
268
280
  async lookupInvoice(payment_hash, invoice) {
269
- const response = await firstValueFrom(this.request({ method: "lookup_invoice", params: { payment_hash, invoice } }));
270
- if (response.result_type !== "lookup_invoice")
271
- throw new Error(`Unexpected response type: ${response.result_type}`);
272
- if (response.error)
273
- throw createWalletError(response.error.type, response.error.message);
274
- return response.result;
281
+ return await this.genericRequest("lookup_invoice", { payment_hash, invoice });
275
282
  }
276
283
  /** List transactions */
277
284
  async listTransactions(params) {
278
- const response = await firstValueFrom(this.request({ method: "list_transactions", params: params || {} }));
279
- if (response.result_type !== "list_transactions")
280
- throw new Error(`Unexpected response type: ${response.result_type}`);
281
- if (response.error)
282
- throw createWalletError(response.error.type, response.error.message);
283
- return response.result;
285
+ return await this.genericRequest("list_transactions", params || {});
284
286
  }
285
287
  /** Get wallet balance */
286
288
  async getBalance() {
287
- const response = await firstValueFrom(this.request({ method: "get_balance", params: {} }));
288
- if (response.result_type !== "get_balance")
289
- throw new Error(`Unexpected response type: ${response.result_type}`);
290
- if (response.error)
291
- throw createWalletError(response.error.type, response.error.message);
292
- return response.result;
289
+ return await this.genericRequest("get_balance", {});
293
290
  }
294
291
  /** Get wallet info */
295
292
  async getInfo() {
296
- const response = await firstValueFrom(this.request({ method: "get_info", params: {} }));
297
- if (response.result_type !== "get_info")
298
- throw new Error(`Unexpected response type: ${response.result_type}`);
299
- if (response.error)
300
- throw createWalletError(response.error.type, response.error.message);
301
- return response.result;
293
+ return await this.genericRequest("get_info", {});
302
294
  }
303
295
  /** Serialize the WalletConnect instance */
304
296
  toJSON() {
@@ -1,51 +1,31 @@
1
1
  import { EventSigner } from "applesauce-factory";
2
- import { NostrEvent } from "nostr-tools";
3
2
  import { Observable, Subscription } from "rxjs";
4
3
  import { WalletAuthURI } from "./helpers/auth-uri.js";
5
4
  import { WalletErrorCode } from "./helpers/error.js";
5
+ import { CommonWalletMethods, TWalletMethod, WalletInfo } from "./helpers/methods.js";
6
6
  import { NotificationType, WalletNotification } from "./helpers/notification.js";
7
- import { GetBalanceParams, GetInfoParams, ListTransactionsParams, LookupInvoiceParams, MakeInvoiceParams, MultiPayInvoiceParams, MultiPayKeysendParams, PayInvoiceParams, PayKeysendParams, WalletRequest } from "./helpers/request.js";
8
- import { GetBalanceResult, GetInfoResult, ListTransactionsResult, LookupInvoiceResult, MakeInvoiceResult, MultiPayInvoiceResult, MultiPayKeysendResult, PayInvoiceResult, PayKeysendResult, WalletResponse } from "./helpers/response.js";
7
+ import { WalletRequestEvent } from "./helpers/request.js";
9
8
  import { WalletSupport } from "./helpers/support.js";
10
9
  import { NostrConnectionMethodsOptions, NostrPool, NostrPublishMethod, NostrSubscriptionMethod } from "./interop.js";
11
- /** Handler function for pay_invoice method */
12
- export type PayInvoiceHandler = (params: PayInvoiceParams) => Promise<PayInvoiceResult>;
13
- /** Handler function for multi_pay_invoice method */
14
- export type MultiPayInvoiceHandler = (params: MultiPayInvoiceParams) => Promise<MultiPayInvoiceResult[]>;
15
- /** Handler function for pay_keysend method */
16
- export type PayKeysendHandler = (params: PayKeysendParams) => Promise<PayKeysendResult>;
17
- /** Handler function for multi_pay_keysend method */
18
- export type MultiPayKeysendHandler = (params: MultiPayKeysendParams) => Promise<MultiPayKeysendResult[]>;
19
- /** Handler function for make_invoice method */
20
- export type MakeInvoiceHandler = (params: MakeInvoiceParams) => Promise<MakeInvoiceResult>;
21
- /** Handler function for lookup_invoice method */
22
- export type LookupInvoiceHandler = (params: LookupInvoiceParams) => Promise<LookupInvoiceResult>;
23
- /** Handler function for list_transactions method */
24
- export type ListTransactionsHandler = (params: ListTransactionsParams) => Promise<ListTransactionsResult>;
25
- /** Handler function for get_balance method */
26
- export type GetBalanceHandler = (params: GetBalanceParams) => Promise<GetBalanceResult>;
27
- /** Handler function for get_info method */
28
- export type GetInfoHandler = (params: GetInfoParams) => Promise<GetInfoResult>;
29
- /** Map of method handlers for the wallet service */
30
- export interface WalletServiceHandlers {
31
- pay_invoice?: PayInvoiceHandler;
32
- multi_pay_invoice?: MultiPayInvoiceHandler;
33
- pay_keysend?: PayKeysendHandler;
34
- multi_pay_keysend?: MultiPayKeysendHandler;
35
- make_invoice?: MakeInvoiceHandler;
36
- lookup_invoice?: LookupInvoiceHandler;
37
- list_transactions?: ListTransactionsHandler;
38
- get_balance?: GetBalanceHandler;
39
- get_info?: GetInfoHandler;
40
- }
10
+ /** Generic type for wallet method handlers */
11
+ export type WalletMethodHandler<Method extends TWalletMethod> = (params: Method["request"]["params"]) => Promise<Method["response"]["result"]>;
12
+ /** Map of method handlers for the wallet service for a specific set of methods */
13
+ export type WalletServiceHandlers<Methods extends TWalletMethod = TWalletMethod> = {
14
+ [K in Methods["method"]]?: WalletMethodHandler<Extract<Methods, {
15
+ method: K;
16
+ }>>;
17
+ };
18
+ /** Serialized wallet service */
41
19
  export type SerializedWalletService = {
42
20
  /** The client's public key */
43
21
  client: string;
44
22
  /** The relays to use for the service */
45
23
  relays: string[];
46
24
  };
25
+ /** Only the necessary info for the getInfo method on wallet service */
26
+ export type WalletServiceInfo = Partial<Omit<WalletInfo, "methods" | "notifications">>;
47
27
  /** Options for creating a WalletService */
48
- export interface WalletServiceOptions extends NostrConnectionMethodsOptions {
28
+ export interface WalletServiceOptions<Methods extends TWalletMethod = CommonWalletMethods> extends NostrConnectionMethodsOptions {
49
29
  /** The relays to use for the service */
50
30
  relays: string[];
51
31
  /** The signer to use for creating and unlocking events */
@@ -54,13 +34,15 @@ export interface WalletServiceOptions extends NostrConnectionMethodsOptions {
54
34
  secret?: Uint8Array;
55
35
  /** The client's public key (used for restoring the service) */
56
36
  client?: string;
37
+ /** A method for getting the general wallet information (Can be overwritten if get_info is set in handlers) */
38
+ getInfo?: () => Promise<WalletServiceInfo>;
57
39
  /** Map of method handlers */
58
- handlers: WalletServiceHandlers;
40
+ handlers: WalletServiceHandlers<Methods>;
59
41
  /** An array of notifications this wallet supports */
60
42
  notifications?: NotificationType[];
61
43
  }
62
44
  /** NIP-47 Wallet Service implementation */
63
- export declare class WalletService {
45
+ export declare class WalletService<Methods extends TWalletMethod = CommonWalletMethods> {
64
46
  /** A fallback method to use for subscriptionMethod if none is passed in when creating the client */
65
47
  static subscriptionMethod: NostrSubscriptionMethod | undefined;
66
48
  /** A fallback method to use for publishMethod if none is passed in when creating the client */
@@ -72,27 +54,29 @@ export declare class WalletService {
72
54
  /** A method for publishing events */
73
55
  protected readonly publishMethod: NostrPublishMethod;
74
56
  protected log: import("debug").Debugger;
57
+ /** A special method for getting the generic wallet information */
58
+ getInfo?: () => Promise<WalletServiceInfo>;
75
59
  /** The relays to use for the service */
76
60
  readonly relays: string[];
77
61
  /** The signer used for creating and unlocking events */
78
62
  protected readonly signer: EventSigner;
79
63
  /** Map of method handlers */
80
- protected readonly handlers: WalletServiceHandlers;
64
+ protected readonly handlers: WalletServiceHandlers<Methods>;
81
65
  /** Wallet support information */
82
- protected readonly support: WalletSupport;
66
+ protected readonly support: WalletSupport<Methods>;
83
67
  /** The service's public key */
84
68
  pubkey: string | null;
85
69
  /** The client's secret key */
86
- protected secret: Uint8Array;
70
+ protected secret?: Uint8Array;
87
71
  /** The client's public key */
88
72
  client: string;
89
73
  /** Shared observable for all wallet request events */
90
- protected events$: Observable<NostrEvent> | null;
74
+ protected events$: Observable<WalletRequestEvent> | null;
91
75
  /** Subscription to the events observable */
92
76
  protected subscription: Subscription | null;
93
77
  /** Whether the service is currently running */
94
78
  running: boolean;
95
- constructor(options: WalletServiceOptions);
79
+ constructor(options: WalletServiceOptions<Methods>);
96
80
  /** Start the wallet service */
97
81
  start(): Promise<void>;
98
82
  /** Stop the wallet service */
@@ -106,15 +90,18 @@ export declare class WalletService {
106
90
  /** Publish the wallet support event */
107
91
  protected publishSupportEvent(): Promise<void>;
108
92
  /** Handle a wallet request event */
109
- protected handleRequestEvent(requestEvent: NostrEvent): Promise<void>;
93
+ protected handleRequestEvent(requestEvent: WalletRequestEvent): Promise<void>;
110
94
  /** Process a decrypted wallet request */
111
- protected processRequest(requestEvent: NostrEvent, request: WalletRequest): Promise<void>;
95
+ protected processRequest<Method extends Methods>(requestEvent: WalletRequestEvent, request: Method["request"]): Promise<void>;
112
96
  /** Send a success response */
113
- protected sendSuccessResponse<T extends WalletResponse>(requestEvent: NostrEvent, method: T["result_type"], result: T["result"]): Promise<void>;
97
+ protected sendSuccessResponse<Method extends Methods>(requestEvent: WalletRequestEvent, method: Method["response"]["result_type"], result: Method["response"]["result"]): Promise<void>;
114
98
  /** Send an error response */
115
- protected sendErrorResponse(requestEvent: NostrEvent, errorType: WalletErrorCode, errorMessage: string): Promise<void>;
99
+ protected sendErrorResponse<Method extends Methods>(requestEvent: WalletRequestEvent, errorType: WalletErrorCode, errorMessage: string): Promise<void>;
116
100
  /** Send a response event */
117
- protected sendResponse(requestEvent: NostrEvent, response: WalletResponse): Promise<void>;
101
+ protected sendResponse<Method extends Methods>(requestEvent: WalletRequestEvent, response: Method["response"] | Method["error"]): Promise<void>;
118
102
  /** Creates a service for a nostr+walletauth URI */
119
- static fromAuthURI(uri: string | WalletAuthURI, options: Omit<WalletServiceOptions, "relays">): WalletService;
103
+ static fromAuthURI<Methods extends TWalletMethod = CommonWalletMethods>(uri: string | WalletAuthURI, options: Omit<WalletServiceOptions<Methods>, "relays"> & {
104
+ /** A relay or method to select a single relay for the client and service to communicate over */
105
+ overrideRelay?: string | ((relays: string[]) => string);
106
+ }): WalletService<Methods>;
120
107
  }
@@ -7,8 +7,8 @@ import { WalletLegacyNotificationBlueprint, WalletNotificationBlueprint } from "
7
7
  import { WalletResponseBlueprint } from "./blueprints/response.js";
8
8
  import { WalletSupportBlueprint } from "./blueprints/support.js";
9
9
  import { parseWalletAuthURI } from "./helpers/auth-uri.js";
10
- import { WalletBaseError } from "./helpers/error.js";
11
- import { getWalletRequest, isWalletRequestExpired, isWalletRequestLocked, unlockWalletRequest, WALLET_REQUEST_KIND, } from "./helpers/request.js";
10
+ import { NotImplementedError, WalletBaseError } from "./helpers/error.js";
11
+ import { getWalletRequest, isValidWalletRequest, isWalletRequestExpired, unlockWalletRequest, WALLET_REQUEST_KIND, } from "./helpers/request.js";
12
12
  import { getConnectionMethods, } from "./interop.js";
13
13
  /** NIP-47 Wallet Service implementation */
14
14
  export class WalletService {
@@ -23,6 +23,8 @@ export class WalletService {
23
23
  /** A method for publishing events */
24
24
  publishMethod;
25
25
  log = logger.extend("WalletService");
26
+ /** A special method for getting the generic wallet information */
27
+ getInfo;
26
28
  /** The relays to use for the service */
27
29
  relays;
28
30
  /** The signer used for creating and unlocking events */
@@ -49,10 +51,16 @@ export class WalletService {
49
51
  this.handlers = options.handlers;
50
52
  // Set the client's secret and public key
51
53
  if (options.secret) {
54
+ // Service was created with a custom secret
52
55
  this.secret = options.secret;
53
56
  this.client = getPublicKey(this.secret);
54
57
  }
58
+ else if (options.client) {
59
+ // Service was restored with only the clients pubkey
60
+ this.client = options.client;
61
+ }
55
62
  else {
63
+ // Generate secret and client pubkey
56
64
  this.secret = generateSecretKey();
57
65
  this.client = getPublicKey(this.secret);
58
66
  }
@@ -98,7 +106,9 @@ export class WalletService {
98
106
  // Ignore strings (support for applesauce-relay)
99
107
  filter((event) => typeof event !== "string"),
100
108
  // Only include valid wallet request events
101
- filter((event) => event.kind === WALLET_REQUEST_KIND && event.pubkey === this.client),
109
+ filter(isValidWalletRequest),
110
+ // Ensure they are to our pubkey
111
+ filter((event) => event.pubkey === this.client),
102
112
  // Verify event signature
103
113
  filter((event) => verifyEvent(event)),
104
114
  // Only create a single subscription to the relays
@@ -129,6 +139,8 @@ export class WalletService {
129
139
  }
130
140
  /** Get the connection URI for the service */
131
141
  getConnectURI() {
142
+ if (!this.secret)
143
+ throw new Error("Service was not created with a secret");
132
144
  if (!this.pubkey)
133
145
  throw new Error("Service is not running");
134
146
  if (!this.relays.length)
@@ -152,7 +164,9 @@ export class WalletService {
152
164
  /** Publish the wallet support event */
153
165
  async publishSupportEvent() {
154
166
  try {
155
- const draft = await create({ signer: this.signer }, WalletSupportBlueprint, this.support, this.client);
167
+ // Tell the client which relay to use if there is only one (for nostr+walletauth URI connections)
168
+ const overrideRelay = this.relays.length === 1 ? this.relays[0] : undefined;
169
+ const draft = await create({ signer: this.signer }, (WalletSupportBlueprint), this.support, this.client, overrideRelay);
156
170
  const event = await this.signer.signEvent(draft);
157
171
  await this.publishMethod(this.relays, event);
158
172
  }
@@ -168,13 +182,7 @@ export class WalletService {
168
182
  if (isWalletRequestExpired(requestEvent))
169
183
  return await this.sendErrorResponse(requestEvent, "OTHER", "Request has expired");
170
184
  // Unlock the request if needed
171
- let request;
172
- if (isWalletRequestLocked(requestEvent)) {
173
- request = await unlockWalletRequest(requestEvent, this.signer);
174
- }
175
- else {
176
- request = getWalletRequest(requestEvent);
177
- }
185
+ const request = await unlockWalletRequest(requestEvent, this.signer);
178
186
  if (!request)
179
187
  return await this.sendErrorResponse(requestEvent, "OTHER", "Failed to decrypt or parse request");
180
188
  // Handle the request based on its method
@@ -188,44 +196,27 @@ export class WalletService {
188
196
  /** Process a decrypted wallet request */
189
197
  async processRequest(requestEvent, request) {
190
198
  const handler = this.handlers[request.method];
191
- if (!handler) {
192
- await this.sendErrorResponse(requestEvent, "NOT_IMPLEMENTED", `Method ${request.method} not supported`);
193
- return;
194
- }
195
199
  try {
196
- let result;
197
- const method = request.method; // Store method for use in catch block
198
- switch (method) {
199
- case "pay_invoice":
200
- result = await handler(request.params);
201
- break;
202
- case "multi_pay_invoice":
203
- result = await handler(request.params);
204
- break;
205
- case "pay_keysend":
206
- result = await handler(request.params);
207
- break;
208
- case "multi_pay_keysend":
209
- result = await handler(request.params);
210
- break;
211
- case "make_invoice":
212
- result = await handler(request.params);
213
- break;
214
- case "lookup_invoice":
215
- result = await handler(request.params);
216
- break;
217
- case "list_transactions":
218
- result = await handler(request.params);
219
- break;
220
- case "get_balance":
221
- result = await handler(request.params);
222
- break;
223
- case "get_info":
224
- result = await handler(request.params);
225
- break;
200
+ let result = undefined;
201
+ // If the user has not implemented the method
202
+ if (!handler) {
203
+ // If its the get_info try to use the builtin getInfo method
204
+ if (request.method === "get_info") {
205
+ result = { ...(this.getInfo?.() ?? {}), ...this.support };
206
+ }
207
+ else {
208
+ // Else throw not supported error
209
+ throw new NotImplementedError(`Method ${request.method} not supported`);
210
+ }
226
211
  }
212
+ // Otherwise use the user provided handler
213
+ if (!result && handler)
214
+ result = await handler(request.params);
215
+ // Throw if failed to get result
216
+ if (!result)
217
+ throw new NotImplementedError(`Method ${request.method} not supported`);
227
218
  // Send success response
228
- await this.sendSuccessResponse(requestEvent, method, result);
219
+ await this.sendSuccessResponse(requestEvent, request.method, result);
229
220
  }
230
221
  catch (error) {
231
222
  this.log(`Error executing ${request.method}:`, error);
@@ -269,7 +260,7 @@ export class WalletService {
269
260
  /** Send a response event */
270
261
  async sendResponse(requestEvent, response) {
271
262
  try {
272
- const draft = await create({ signer: this.signer }, WalletResponseBlueprint, requestEvent, response);
263
+ const draft = await create({ signer: this.signer }, WalletResponseBlueprint(requestEvent, response));
273
264
  const event = await this.signer.signEvent(draft);
274
265
  await this.publishMethod(this.relays, event);
275
266
  }
@@ -280,10 +271,13 @@ export class WalletService {
280
271
  }
281
272
  /** Creates a service for a nostr+walletauth URI */
282
273
  static fromAuthURI(uri, options) {
283
- const { client, relays } = typeof uri === "string" ? parseWalletAuthURI(uri) : uri;
274
+ const authURI = typeof uri === "string" ? parseWalletAuthURI(uri) : uri;
275
+ const relays = options.overrideRelay
276
+ ? [typeof options.overrideRelay === "function" ? options.overrideRelay(authURI.relays) : options.overrideRelay]
277
+ : authURI.relays;
284
278
  return new WalletService({
285
279
  ...options,
286
- client,
280
+ client: authURI.client,
287
281
  relays,
288
282
  });
289
283
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-wallet-connect",
3
- "version": "3.1.0",
3
+ "version": "4.1.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -58,23 +58,25 @@
58
58
  }
59
59
  },
60
60
  "dependencies": {
61
- "applesauce-core": "^3.1.0",
62
- "applesauce-factory": "^3.1.0",
63
- "nostr-tools": "~2.15",
64
61
  "@noble/hashes": "^1.7.1",
62
+ "applesauce-core": "^4.1.0",
63
+ "applesauce-factory": "^4.0.0",
64
+ "nostr-tools": "~2.17",
65
65
  "rxjs": "^7.8.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@hirez_io/observer-spy": "^2.2.0",
69
69
  "@types/debug": "^4.1.12",
70
+ "rimraf": "^6.0.1",
70
71
  "typescript": "^5.8.3",
71
- "vitest": "^3.2.3"
72
+ "vitest": "^3.2.4"
72
73
  },
73
74
  "funding": {
74
75
  "type": "lightning",
75
76
  "url": "lightning:nostrudel@geyser.fund"
76
77
  },
77
78
  "scripts": {
79
+ "prebuild": "rimraf dist",
78
80
  "build": "tsc",
79
81
  "watch:build": "tsc --watch > /dev/null",
80
82
  "test": "vitest run --passWithNoTests",