@fedimint/core-web 0.0.3 → 0.0.5

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.
Files changed (61) hide show
  1. package/README.md +4 -4
  2. package/dist/FedimintWallet.d.ts +42 -65
  3. package/dist/FedimintWallet.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +1 -1
  6. package/dist/services/BalanceService.d.ts +34 -0
  7. package/dist/services/BalanceService.d.ts.map +1 -0
  8. package/dist/services/FederationService.d.ts +11 -0
  9. package/dist/services/FederationService.d.ts.map +1 -0
  10. package/dist/services/LightningService.d.ts +18 -0
  11. package/dist/services/LightningService.d.ts.map +1 -0
  12. package/dist/services/MintService.d.ts +15 -0
  13. package/dist/services/MintService.d.ts.map +1 -0
  14. package/dist/services/RecoveryService.d.ts +13 -0
  15. package/dist/services/RecoveryService.d.ts.map +1 -0
  16. package/dist/services/index.d.ts +6 -0
  17. package/dist/services/index.d.ts.map +1 -0
  18. package/dist/types/wallet.d.ts +16 -11
  19. package/dist/types/wallet.d.ts.map +1 -1
  20. package/dist/utils/logger.d.ts +17 -0
  21. package/dist/utils/logger.d.ts.map +1 -0
  22. package/dist/worker/WorkerClient.d.ts +44 -0
  23. package/dist/worker/WorkerClient.d.ts.map +1 -0
  24. package/dist/worker/index.d.ts +2 -0
  25. package/dist/worker/index.d.ts.map +1 -0
  26. package/dist/worker.js +1 -1
  27. package/dist/worker.js.map +1 -1
  28. package/package.json +9 -9
  29. package/src/FedimintWallet.test.ts +77 -0
  30. package/src/FedimintWallet.ts +91 -466
  31. package/src/services/BalanceService.test.ts +50 -0
  32. package/src/services/BalanceService.ts +50 -0
  33. package/src/services/FederationService.test.ts +58 -0
  34. package/src/services/FederationService.ts +22 -0
  35. package/src/services/LightningService.test.ts +168 -0
  36. package/src/services/LightningService.ts +144 -0
  37. package/src/services/MintService.test.ts +19 -0
  38. package/src/services/MintService.ts +96 -0
  39. package/src/services/RecoveryService.ts +26 -0
  40. package/src/services/index.ts +5 -0
  41. package/src/test/TestFedimintWallet.ts +17 -0
  42. package/src/test/TestingService.ts +59 -0
  43. package/src/test/setupTests.ts +43 -0
  44. package/src/types/wallet.ts +20 -13
  45. package/src/utils/logger.ts +61 -0
  46. package/src/worker/WorkerClient.ts +229 -0
  47. package/src/worker/index.ts +1 -0
  48. package/src/worker/worker.js +96 -0
  49. package/src/worker/worker.test.ts +90 -0
  50. package/node_modules/fedimint-client-wasm/fedimint_client_wasm.d.ts +0 -49
  51. package/node_modules/fedimint-client-wasm/fedimint_client_wasm.js +0 -4
  52. package/node_modules/fedimint-client-wasm/fedimint_client_wasm_bg.js +0 -1411
  53. package/node_modules/fedimint-client-wasm/fedimint_client_wasm_bg.wasm +0 -0
  54. package/node_modules/fedimint-client-wasm/package.json +0 -26
  55. package/src/worker.js +0 -80
  56. package/wasm/fedimint_client_wasm.d.ts +0 -49
  57. package/wasm/fedimint_client_wasm.js +0 -4
  58. package/wasm/fedimint_client_wasm_bg.js +0 -1411
  59. package/wasm/fedimint_client_wasm_bg.wasm +0 -0
  60. package/wasm/fedimint_client_wasm_bg.wasm.d.ts +0 -110
  61. package/wasm/package.json +0 -26
@@ -1,93 +1,96 @@
1
+ import { WorkerClient } from './worker/WorkerClient'
1
2
  import {
2
- JSONValue,
3
- JSONObject,
4
- LightningGateway,
5
- OutgoingLightningPayment,
6
- LnPayState,
7
- LnReceiveState,
8
- StreamResult,
9
- StreamError,
10
- CreateBolt11Response,
11
- ModuleKind,
12
- GatewayInfo,
13
- CancelFunction,
14
- } from './types/wallet'
3
+ BalanceService,
4
+ MintService,
5
+ LightningService,
6
+ FederationService,
7
+ RecoveryService,
8
+ } from './services'
9
+ import { logger, type LogLevel } from './utils/logger'
15
10
 
16
11
  const DEFAULT_CLIENT_NAME = 'fm-default' as const
17
12
 
18
13
  export class FedimintWallet {
19
- private worker: Worker | null = null
20
- private initPromise: Promise<void> | null = null
21
- private openPromise: Promise<void>
22
- private resolveOpen: () => void = () => {}
14
+ private _client: WorkerClient
15
+
16
+ public balance: BalanceService
17
+ public mint: MintService
18
+ public lightning: LightningService
19
+ public federation: FederationService
20
+ public recovery: RecoveryService
21
+
22
+ private _openPromise: Promise<void> | null = null
23
+ private _resolveOpen: () => void = () => {}
23
24
  private _isOpen: boolean = false
24
- private requestCounter: number = 0
25
- private requestCallbacks: Map<number, (value: any) => void> = new Map()
26
25
 
27
- constructor(lazy: boolean = false, open: boolean = true) {
28
- this.openPromise = new Promise((resolve) => {
29
- this.resolveOpen = resolve
26
+ /**
27
+ * Creates a new instance of FedimintWallet.
28
+ *
29
+ * This constructor initializes a FedimintWallet instance, which manages communication
30
+ * with a Web Worker. The Web Worker is responsible for running WebAssembly code that
31
+ * handles the core Fedimint Client operations.
32
+ *
33
+ * (default) When not in lazy mode, the constructor immediately initializes the
34
+ * Web Worker and begins loading the WebAssembly module in the background. This
35
+ * allows for faster subsequent operations but may increase initial load time.
36
+ *
37
+ * In lazy mode, the Web Worker and WebAssembly initialization are deferred until
38
+ * the first operation that requires them, reducing initial overhead at the cost
39
+ * of a slight delay on the first operation.
40
+ *
41
+ * @param {boolean} lazy - If true, delays Web Worker and WebAssembly initialization
42
+ * until needed. Default is false.
43
+ *
44
+ * @example
45
+ * // Create a wallet with immediate initialization
46
+ * const wallet = new FedimintWallet();
47
+ * wallet.open();
48
+ *
49
+ * // Create a wallet with lazy initialization
50
+ * const lazyWallet = new FedimintWallet(true);
51
+ * // Some time later...
52
+ * lazyWallet.initialize();
53
+ * lazyWallet.open();
54
+ */
55
+ constructor(lazy: boolean = false) {
56
+ this._openPromise = new Promise((resolve) => {
57
+ this._resolveOpen = resolve
30
58
  })
31
- if (lazy) return
32
- this.initialize()
33
- }
34
-
35
- async waitForOpen() {
36
- if (this._isOpen) return
37
- return this.openPromise
38
- }
59
+ this._client = new WorkerClient()
60
+ this.mint = new MintService(this._client)
61
+ this.lightning = new LightningService(this._client)
62
+ this.balance = new BalanceService(this._client)
63
+ this.federation = new FederationService(this._client)
64
+ this.recovery = new RecoveryService(this._client)
39
65
 
40
- private getNextRequestId(): number {
41
- return ++this.requestCounter
42
- }
66
+ logger.info('FedimintWallet instantiated')
43
67
 
44
- // Sends a single message and deletes the request callback
45
- // The first response
46
- private sendSingleMessage(type: string, payload?: any): Promise<any> {
47
- return new Promise((resolve, reject) => {
48
- const requestId = this.getNextRequestId()
49
- this.requestCallbacks.set(requestId, (data) => {
50
- this.requestCallbacks.delete(requestId)
51
- if (data.data) resolve(data.data)
52
- else if (data.error) reject(data.error)
53
- })
54
- try {
55
- this.worker!.postMessage({ type, payload, requestId })
56
- } catch (e) {
57
- reject(e)
58
- }
59
- })
68
+ if (!lazy) {
69
+ this.initialize()
70
+ }
60
71
  }
61
72
 
62
- // Setup
63
- initialize() {
64
- if (this.initPromise) return this.initPromise
65
- this.worker = new Worker(new URL('./worker.js', import.meta.url))
66
- this.worker.onmessage = this.handleWorkerMessage.bind(this)
67
- this.initPromise = this.sendSingleMessage('init')
68
- return this.initPromise
73
+ async initialize() {
74
+ logger.info('Initializing WorkerClient')
75
+ await this._client.initialize()
76
+ logger.info('WorkerClient initialized')
69
77
  }
70
78
 
71
- private handleWorkerMessage(event: MessageEvent) {
72
- const { type, requestId, ...data } = event.data
73
- const streamCallback = this.requestCallbacks.get(requestId)
74
- // TODO: Handle errors... maybe have another callbacks list for errors?
75
- if (streamCallback) {
76
- streamCallback(data) // {data: something} OR {error: something}
77
- }
79
+ async waitForOpen() {
80
+ if (this._isOpen) return Promise.resolve()
81
+ return this._openPromise
78
82
  }
79
83
 
80
84
  async open(clientName: string = DEFAULT_CLIENT_NAME) {
81
- await this.initialize()
85
+ await this._client.initialize()
82
86
  // TODO: Determine if this should be safe or throw
83
- if (this._isOpen)
84
- throw new Error(
85
- 'The FedimintWallet is already open. You can only call `FedimintWallet.open on closed clients.`',
86
- )
87
- const { success } = await this.sendSingleMessage('open', { clientName })
87
+ if (this._isOpen) throw new Error('The FedimintWallet is already open.')
88
+ const { success } = await this._client.sendSingleMessage('open', {
89
+ clientName,
90
+ })
88
91
  if (success) {
89
92
  this._isOpen = !!success
90
- this.resolveOpen()
93
+ this._resolveOpen()
91
94
  }
92
95
  return success
93
96
  }
@@ -96,234 +99,20 @@ export class FedimintWallet {
96
99
  inviteCode: string,
97
100
  clientName: string = DEFAULT_CLIENT_NAME,
98
101
  ) {
99
- await this.initialize()
102
+ await this._client.initialize()
100
103
  // TODO: Determine if this should be safe or throw
101
104
  if (this._isOpen)
102
105
  throw new Error(
103
- 'Failed to Join Federation. You have already joined a federation, and you can only join one federation per wallet.',
106
+ 'The FedimintWallet is already open. You can only call `joinFederation` on closed clients.',
104
107
  )
105
- const response = await this.sendSingleMessage('join', {
108
+ const response = await this._client.sendSingleMessage('join', {
106
109
  inviteCode,
107
110
  clientName,
108
111
  })
109
- if (response.success) this._isOpen = true
110
- }
111
-
112
- /**
113
- * @summary Initiates an RPC stream with the specified module and method.
114
- *
115
- * @description
116
- * This function sets up an RPC stream by sending a request to a worker and
117
- * handling responses asynchronously. It ensures that unsubscription is handled
118
- * correctly, even if the unsubscribe function is called before the subscription
119
- * is fully established, by deferring the unsubscription attempt using `setTimeout`.
120
- *
121
- * The function operates in a non-blocking manner, leveraging Promises to manage
122
- * asynchronous operations and callbacks to handle responses.
123
- *
124
- *
125
- * @template Response - The expected type of the successful response.
126
- * @template Body - The type of the request body.
127
- * @param module - The module kind to interact with.
128
- * @param method - The method name to invoke on the module.
129
- * @param body - The request payload.
130
- * @param onSuccess - Callback invoked with the response data on success.
131
- * @param onError - Callback invoked with error information if an error occurs.
132
- * @param onEnd - Optional callback invoked when the stream ends.
133
- * @returns A function that can be called to cancel the subscription.
134
- *
135
- */
136
- private _rpcStream<
137
- Response extends JSONValue = JSONValue,
138
- Body extends JSONValue = JSONValue,
139
- >(
140
- module: ModuleKind,
141
- method: string,
142
- body: Body,
143
- onSuccess: (res: Response) => void,
144
- onError: (res: StreamError['error']) => void,
145
- onEnd: () => void = () => {},
146
- ): CancelFunction {
147
- const requestId = this.getNextRequestId()
148
-
149
- let unsubscribe: (value: void) => void = () => {}
150
- let isSubscribed = false
151
-
152
- const unsubscribePromise = new Promise<void>((resolve) => {
153
- unsubscribe = () => {
154
- if (isSubscribed) {
155
- // If already subscribed, resolve immediately to trigger unsubscription
156
- resolve()
157
- } else {
158
- // If not yet subscribed, defer the unsubscribe attempt to the next event loop tick
159
- // This ensures that subscription setup has time to complete
160
- setTimeout(() => unsubscribe(), 0)
161
- }
162
- }
163
- })
164
-
165
- // Initiate the inner RPC stream setup asynchronously
166
- this._rpcStreamInner(
167
- requestId,
168
- module,
169
- method,
170
- body,
171
- onSuccess,
172
- onError,
173
- onEnd,
174
- unsubscribePromise,
175
- ).then(() => {
176
- isSubscribed = true
177
- })
178
-
179
- return unsubscribe
180
- }
181
-
182
- private async _rpcStreamInner<
183
- Response extends JSONValue = JSONValue,
184
- Body extends JSONValue = JSONValue,
185
- >(
186
- requestId: number,
187
- module: ModuleKind,
188
- method: string,
189
- body: Body,
190
- onSuccess: (res: Response) => void,
191
- onError: (res: StreamError['error']) => void,
192
- onEnd: () => void = () => {},
193
- unsubscribePromise: Promise<void>,
194
- // Unsubscribe function
195
- ): Promise<void> {
196
- await this.openPromise
197
- if (!this.worker || !this._isOpen)
198
- throw new Error('FedimintWallet is not open')
199
-
200
- this.requestCallbacks.set(requestId, (response: StreamResult<Response>) => {
201
- if (response.error !== undefined) {
202
- onError(response.error)
203
- } else if (response.data !== undefined) {
204
- onSuccess(response.data)
205
- } else if (response.end !== undefined) {
206
- this.requestCallbacks.delete(requestId)
207
- onEnd()
208
- }
209
- })
210
- this.worker.postMessage({
211
- type: 'rpc',
212
- payload: { module, method, body },
213
- requestId,
214
- })
215
-
216
- unsubscribePromise.then(() => {
217
- console.trace('UNSUBSCRIBING', requestId)
218
- this.worker?.postMessage({
219
- type: 'unsubscribe',
220
- requestId,
221
- })
222
- this.requestCallbacks.delete(requestId)
223
- })
224
- }
225
-
226
- private async _rpcSingle<Response extends JSONValue = JSONValue>(
227
- module: ModuleKind,
228
- method: string,
229
- body: JSONValue,
230
- ): Promise<Response> {
231
- return new Promise((resolve, reject) => {
232
- this._rpcStream<Response>(module, method, body, resolve, reject)
233
- })
234
- }
235
-
236
- async getBalance(): Promise<number> {
237
- return await this._rpcSingle('', 'get_balance', {})
238
- }
239
-
240
- // Mint module methods
241
-
242
- async redeemEcash(notes: string): Promise<void> {
243
- await this._rpcSingle('mint', 'reissue_external_notes', {
244
- oob_notes: notes, // "out of band notes"
245
- extra_meta: null,
246
- })
247
- }
248
-
249
- async reissueExternalNotes(
250
- oobNotes: string,
251
- extraMeta: JSONObject,
252
- ): Promise<string> {
253
- return await this._rpcSingle('mint', 'reissue_external_notes', {
254
- oob_notes: oobNotes,
255
- extra_meta: extraMeta,
256
- })
257
- }
258
-
259
- subscribeReissueExternalNotes(
260
- operationId: string,
261
- onSuccess: (state: JSONValue) => void = () => {},
262
- onError: (error: string) => void = () => {},
263
- ) {
264
- type ReissueExternalNotesState =
265
- | 'Created'
266
- | 'Issuing'
267
- | 'Done'
268
- | { Failed: { error: string } }
269
-
270
- const unsubscribe = this._rpcStream<ReissueExternalNotesState>(
271
- 'mint',
272
- 'subscribe_reissue_external_notes',
273
- { operation_id: operationId },
274
- (res) => onSuccess(res),
275
- onError,
276
- )
277
-
278
- return unsubscribe
279
- }
280
-
281
- async spendNotes(
282
- minAmount: number,
283
- tryCancelAfter: number,
284
- includeInvite: boolean,
285
- extraMeta: JSONValue,
286
- ): Promise<JSONValue> {
287
- return await this._rpcSingle('mint', 'spend_notes', {
288
- min_amount: minAmount,
289
- try_cancel_after: tryCancelAfter,
290
- include_invite: includeInvite,
291
- extra_meta: extraMeta,
292
- })
293
- }
294
-
295
- async validateNotes(oobNotes: string): Promise<number> {
296
- return await this._rpcSingle('mint', 'validate_notes', {
297
- oob_notes: oobNotes,
298
- })
299
- }
300
-
301
- async tryCancelSpendNotes(operationId: string): Promise<void> {
302
- await this._rpcSingle('mint', 'try_cancel_spend_notes', {
303
- operation_id: operationId,
304
- })
305
- }
306
-
307
- subscribeSpendNotes(
308
- operationId: string,
309
- onSuccess: (state: JSONValue) => void = () => {},
310
- onError: (error: string) => void = () => {},
311
- ) {
312
- const unsubscribe = this._rpcStream(
313
- 'mint',
314
- 'subscribe_spend_notes',
315
- { operation_id: operationId },
316
- (res) => onSuccess(res),
317
- onError,
318
- )
319
-
320
- return unsubscribe
321
- }
322
-
323
- async awaitSpendOobRefund(operationId: string): Promise<JSONValue> {
324
- return await this._rpcSingle('mint', 'await_spend_oob_refund', {
325
- operation_id: operationId,
326
- })
112
+ if (response.success) {
113
+ this._isOpen = true
114
+ this._resolveOpen()
115
+ }
327
116
  }
328
117
 
329
118
  /**
@@ -331,185 +120,21 @@ export class FedimintWallet {
331
120
  * After this call, the FedimintWallet instance should be discarded.
332
121
  */
333
122
  async cleanup() {
334
- this.worker?.terminate()
335
- this.worker = null
336
- this.openPromise = Promise.resolve()
337
- this.requestCallbacks.clear()
123
+ this._openPromise = null
124
+ this._isOpen = false
125
+ this._client.cleanup()
338
126
  }
339
127
 
340
128
  isOpen() {
341
- return this.worker !== null && this._isOpen
342
- }
343
-
344
- async getConfig(): Promise<JSONValue> {
345
- return await this._rpcSingle('', 'get_config', {})
346
- }
347
-
348
- async getFederationId(): Promise<string> {
349
- return await this._rpcSingle('', 'get_federation_id', {})
350
- }
351
-
352
- async getInviteCode(peer: number): Promise<string | null> {
353
- return await this._rpcSingle('', 'get_invite_code', { peer })
354
- }
355
-
356
- async listOperations(): Promise<JSONValue[]> {
357
- return await this._rpcSingle('', 'list_operations', {})
358
- }
359
-
360
- async hasPendingRecoveries(): Promise<boolean> {
361
- return await this._rpcSingle('', 'has_pending_recoveries', {})
362
- }
363
-
364
- async waitForAllRecoveries(): Promise<void> {
365
- await this._rpcSingle('', 'wait_for_all_recoveries', {})
366
- }
367
-
368
- /// STREAMING RPCs --------------------
369
-
370
- subscribeBalance(
371
- onSuccess: (balance: number) => void = () => {},
372
- onError: (error: string) => void = () => {},
373
- ) {
374
- const unsubscribe = this._rpcStream<string>(
375
- '',
376
- 'subscribe_balance_changes',
377
- {},
378
- (res) => onSuccess(parseInt(res)),
379
- onError,
380
- )
381
-
382
- return unsubscribe
383
- }
384
-
385
- subscribeToRecoveryProgress(
386
- onSuccess: (progress: {
387
- module_id: number
388
- progress: JSONValue
389
- }) => void = () => {},
390
- onError: (error: string) => void = () => {},
391
- ) {
392
- const unsubscribe = this._rpcStream<{
393
- module_id: number
394
- progress: JSONValue
395
- }>('', 'subscribe_to_recovery_progress', {}, onSuccess, onError)
396
-
397
- return unsubscribe
398
- }
399
-
400
- // Lightning Network module methods
401
-
402
- async createBolt11InvoiceWithGateway(
403
- amount: number,
404
- description: string,
405
- expiryTime: number | null = null,
406
- extraMeta: JSONObject = {},
407
- gatewayInfo: GatewayInfo,
408
- ) {
409
- return await this._rpcSingle('ln', 'create_bolt11_invoice', {
410
- amount,
411
- description,
412
- expiry_time: expiryTime,
413
- extra_meta: extraMeta,
414
- gateway: gatewayInfo,
415
- })
416
- }
417
-
418
- async createBolt11Invoice(
419
- amount: number,
420
- description: string,
421
- expiryTime: number | null = null,
422
- extraMeta: JSONObject = {},
423
- ): Promise<CreateBolt11Response> {
424
- await this.updateGatewayCache()
425
- const gateway = await this._getDefaultGatewayInfo()
426
- return await this._rpcSingle('ln', 'create_bolt11_invoice', {
427
- amount,
428
- description,
429
- expiry_time: expiryTime,
430
- extra_meta: extraMeta,
431
- gateway: gateway.info,
432
- })
129
+ return this._isOpen
433
130
  }
434
131
 
435
- async payBolt11InvoiceWithGateway(
436
- invoice: string,
437
- gatewayInfo: GatewayInfo,
438
- extraMeta: JSONObject = {},
439
- ) {
440
- return await this._rpcSingle('ln', 'pay_bolt11_invoice', {
441
- maybe_gateway: gatewayInfo,
442
- invoice,
443
- extra_meta: extraMeta,
444
- })
445
- }
446
-
447
- async _getDefaultGatewayInfo(): Promise<LightningGateway> {
448
- const gateways = await this.listGateways()
449
- return gateways[0]
450
- }
451
-
452
- async payBolt11Invoice(
453
- invoice: string,
454
- extraMeta: JSONObject = {},
455
- ): Promise<OutgoingLightningPayment> {
456
- await this.updateGatewayCache()
457
- const gateway = await this._getDefaultGatewayInfo()
458
- return await this._rpcSingle('ln', 'pay_bolt11_invoice', {
459
- maybe_gateway: gateway.info,
460
- invoice,
461
- extra_meta: extraMeta,
462
- })
463
- }
464
-
465
- subscribeLnPay(
466
- operationId: string,
467
- onSuccess: (state: LnPayState) => void = () => {},
468
- onError: (error: string) => void = () => {},
469
- ) {
470
- const unsubscribe = this._rpcStream(
471
- 'ln',
472
- 'subscribe_ln_pay',
473
- { operation_id: operationId },
474
- onSuccess,
475
- onError,
476
- )
477
-
478
- return unsubscribe
479
- }
480
-
481
- subscribeLnReceive(
482
- operationId: string,
483
- onSuccess: (state: LnReceiveState) => void = () => {},
484
- onError: (error: string) => void = () => {},
485
- ) {
486
- const unsubscribe = this._rpcStream(
487
- 'ln',
488
- 'subscribe_ln_receive',
489
- { operation_id: operationId },
490
- onSuccess,
491
- onError,
492
- )
493
-
494
- return unsubscribe
495
- }
496
-
497
- async getGateway(
498
- gatewayId: string | null = null,
499
- forceInternal: boolean = false,
500
- ): Promise<LightningGateway | null> {
501
- return await this._rpcSingle('ln', 'get_gateway', {
502
- gateway_id: gatewayId,
503
- force_internal: forceInternal,
504
- })
505
- }
506
-
507
- async listGateways(): Promise<LightningGateway[]> {
508
- return await this._rpcSingle('ln', 'list_gateways', {})
509
- }
510
-
511
- async updateGatewayCache(): Promise<void> {
512
- console.trace('Updating gateway cache')
513
- await this._rpcSingle('ln', 'update_gateway_cache', {})
132
+ /**
133
+ * Sets the log level for the library.
134
+ * @param level The desired log level ('DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE').
135
+ */
136
+ setLogLevel(level: LogLevel) {
137
+ logger.setLevel(level)
138
+ logger.info(`Log level set to ${level}.`)
514
139
  }
515
140
  }
@@ -0,0 +1,50 @@
1
+ import { test, expect } from 'vitest'
2
+ import { TestFedimintWallet } from '../test/TestFedimintWallet'
3
+ import { beforeAll } from 'vitest'
4
+
5
+ let randomTestingId: string
6
+ let wallet: TestFedimintWallet
7
+
8
+ beforeAll(async () => {
9
+ randomTestingId = Math.random().toString(36).substring(2, 15)
10
+ wallet = new TestFedimintWallet()
11
+ expect(wallet).toBeDefined()
12
+ await expect(
13
+ wallet.joinFederation(wallet.testing.TESTING_INVITE, randomTestingId),
14
+ ).resolves.toBeUndefined()
15
+ expect(wallet.isOpen()).toBe(true)
16
+
17
+ // Cleanup after all tests
18
+ return async () => {
19
+ // clear up browser resources
20
+ await wallet.cleanup()
21
+ // remove the wallet db
22
+ indexedDB.deleteDatabase(randomTestingId)
23
+ // swap out the randomTestingId for a new one, to avoid raciness
24
+ randomTestingId = Math.random().toString(36).substring(2, 15)
25
+ }
26
+ })
27
+
28
+ test('getBalance should be initially zero', async () => {
29
+ expect(wallet).toBeDefined()
30
+ expect(wallet.isOpen()).toBe(true)
31
+ const beforeGetBalance = wallet.testing.getRequestCounter()
32
+ await expect(wallet.balance.getBalance()).resolves.toEqual(0)
33
+ expect(wallet.testing.getRequestCounter()).toBe(beforeGetBalance + 1)
34
+ })
35
+
36
+ test('subscribe balance', async () => {
37
+ expect(wallet).toBeDefined()
38
+ expect(wallet.isOpen()).toBe(true)
39
+
40
+ const counterBefore = wallet.testing.getRequestCounter()
41
+ const callbacksBefore = wallet.testing.getRequestCallbackMap().size
42
+ const unsubscribe = await wallet.balance.subscribeBalance((balance) => {
43
+ expect(balance).toEqual(0)
44
+ })
45
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
46
+ expect(wallet.testing.getRequestCallbackMap().size).toBe(callbacksBefore + 1)
47
+
48
+ await expect(wallet.balance.getBalance()).resolves.toEqual(0)
49
+ expect(unsubscribe()).toBeUndefined()
50
+ })
@@ -0,0 +1,50 @@
1
+ import { WorkerClient } from '../worker'
2
+
3
+ /**
4
+ * Balance Service
5
+ *
6
+ * The Balance Service provides methods to interact with the balance of a Fedimint wallet.
7
+ */
8
+ export class BalanceService {
9
+ constructor(private client: WorkerClient) {}
10
+
11
+ /**
12
+ * Get the balance of the current wallet
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const balance = await wallet.balance.getBalance()
17
+ * ```
18
+ */
19
+ async getBalance(): Promise<number> {
20
+ return await this.client.rpcSingle('', 'get_balance', {})
21
+ }
22
+
23
+ /**
24
+ * Subscribe to the balance of the current wallet
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const unsubscribe = wallet.balance.subscribeBalance((balance) => {
29
+ * console.log(balance)
30
+ * })
31
+ *
32
+ * // ...Cleanup Later
33
+ * unsubscribe()
34
+ * ```
35
+ */
36
+ subscribeBalance(
37
+ onSuccess: (balance: number) => void = () => {},
38
+ onError: (error: string) => void = () => {},
39
+ ) {
40
+ const unsubscribe = this.client.rpcStream<string>(
41
+ '',
42
+ 'subscribe_balance_changes',
43
+ {},
44
+ (res) => onSuccess(parseInt(res)),
45
+ onError,
46
+ )
47
+
48
+ return unsubscribe
49
+ }
50
+ }