@fedimint/core 0.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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -0
  3. package/dist/dts/FedimintWallet.d.ts +51 -0
  4. package/dist/dts/FedimintWallet.d.ts.map +1 -0
  5. package/dist/dts/WalletDirector.d.ts +79 -0
  6. package/dist/dts/WalletDirector.d.ts.map +1 -0
  7. package/dist/dts/index.d.ts +5 -0
  8. package/dist/dts/index.d.ts.map +1 -0
  9. package/dist/dts/services/BalanceService.d.ts +15 -0
  10. package/dist/dts/services/BalanceService.d.ts.map +1 -0
  11. package/dist/dts/services/FederationService.d.ts +13 -0
  12. package/dist/dts/services/FederationService.d.ts.map +1 -0
  13. package/dist/dts/services/LightningService.d.ts +48 -0
  14. package/dist/dts/services/LightningService.d.ts.map +1 -0
  15. package/dist/dts/services/MintService.d.ts +23 -0
  16. package/dist/dts/services/MintService.d.ts.map +1 -0
  17. package/dist/dts/services/RecoveryService.d.ts +13 -0
  18. package/dist/dts/services/RecoveryService.d.ts.map +1 -0
  19. package/dist/dts/services/WalletService.d.ts +13 -0
  20. package/dist/dts/services/WalletService.d.ts.map +1 -0
  21. package/dist/dts/services/index.d.ts +7 -0
  22. package/dist/dts/services/index.d.ts.map +1 -0
  23. package/dist/dts/transport/TransportClient.d.ts +55 -0
  24. package/dist/dts/transport/TransportClient.d.ts.map +1 -0
  25. package/dist/dts/transport/index.d.ts +2 -0
  26. package/dist/dts/transport/index.d.ts.map +1 -0
  27. package/dist/dts/types/index.d.ts +4 -0
  28. package/dist/dts/types/index.d.ts.map +1 -0
  29. package/dist/dts/types/transport.d.ts +2 -0
  30. package/dist/dts/types/transport.d.ts.map +1 -0
  31. package/dist/dts/types/utils.d.ts +21 -0
  32. package/dist/dts/types/utils.d.ts.map +1 -0
  33. package/dist/dts/types/wallet.d.ts +241 -0
  34. package/dist/dts/types/wallet.d.ts.map +1 -0
  35. package/dist/dts/utils/logger.d.ts +24 -0
  36. package/dist/dts/utils/logger.d.ts.map +1 -0
  37. package/dist/index.d.ts +578 -0
  38. package/dist/index.js +2 -0
  39. package/dist/index.js.map +1 -0
  40. package/package.json +40 -0
  41. package/src/FedimintWallet.ts +119 -0
  42. package/src/WalletDirector.ts +119 -0
  43. package/src/index.ts +4 -0
  44. package/src/services/BalanceService.test.ts +26 -0
  45. package/src/services/BalanceService.ts +29 -0
  46. package/src/services/FederationService.test.ts +58 -0
  47. package/src/services/FederationService.ts +216 -0
  48. package/src/services/LightningService.test.ts +265 -0
  49. package/src/services/LightningService.ts +289 -0
  50. package/src/services/MintService.test.ts +74 -0
  51. package/src/services/MintService.ts +129 -0
  52. package/src/services/RecoveryService.ts +28 -0
  53. package/src/services/WalletService.test.ts +59 -0
  54. package/src/services/WalletService.ts +50 -0
  55. package/src/services/index.ts +6 -0
  56. package/src/transport/TransportClient.ts +254 -0
  57. package/src/transport/index.ts +1 -0
  58. package/src/types/index.ts +3 -0
  59. package/src/types/transport.ts +1 -0
  60. package/src/types/utils.ts +23 -0
  61. package/src/types/wallet.ts +298 -0
  62. package/src/utils/logger.ts +69 -0
@@ -0,0 +1,129 @@
1
+ import { TransportClient } from '../transport'
2
+ import type {
3
+ Duration,
4
+ JSONObject,
5
+ JSONValue,
6
+ MintSpendNotesResponse,
7
+ MSats,
8
+ NoteCountByDenomination,
9
+ ReissueExternalNotesState,
10
+ SpendNotesState,
11
+ } from '../types'
12
+
13
+ export class MintService {
14
+ constructor(private client: TransportClient) {}
15
+
16
+ /** https://web.fedimint.org/core/FedimintWallet/MintService/redeemEcash */
17
+ async redeemEcash(notes: string) {
18
+ return await this.client.rpcSingle<string>(
19
+ 'mint',
20
+ 'reissue_external_notes',
21
+ {
22
+ oob_notes: notes, // "out of band notes"
23
+ extra_meta: null,
24
+ },
25
+ )
26
+ }
27
+
28
+ async reissueExternalNotes(oobNotes: string, extraMeta: JSONObject = {}) {
29
+ return await this.client.rpcSingle<string>(
30
+ 'mint',
31
+ 'reissue_external_notes',
32
+ {
33
+ oob_notes: oobNotes,
34
+ extra_meta: extraMeta,
35
+ },
36
+ )
37
+ }
38
+
39
+ subscribeReissueExternalNotes(
40
+ operationId: string,
41
+ onSuccess: (state: ReissueExternalNotesState) => void = () => {},
42
+ onError: (error: string) => void = () => {},
43
+ ) {
44
+ const unsubscribe = this.client.rpcStream<ReissueExternalNotesState>(
45
+ 'mint',
46
+ 'subscribe_reissue_external_notes',
47
+ { operation_id: operationId },
48
+ onSuccess,
49
+ onError,
50
+ )
51
+
52
+ return unsubscribe
53
+ }
54
+
55
+ /** https://web.fedimint.org/core/FedimintWallet/MintService/spendNotes */
56
+ async spendNotes(
57
+ amountMsats: number,
58
+ // Tells the wallet to automatically try to cancel the spend if it hasn't completed
59
+ // after the specified number of seconds. If the receiver has already redeemed
60
+ // the notes at this time, the notes will not be cancelled.
61
+ tryCancelAfter: number | Duration = 3600 * 24, // defaults to 1 day
62
+ includeInvite: boolean = false,
63
+ extraMeta: JSONValue = {},
64
+ ) {
65
+ const duration =
66
+ typeof tryCancelAfter === 'number'
67
+ ? { nanos: 0, secs: tryCancelAfter }
68
+ : tryCancelAfter
69
+
70
+ const res = await this.client.rpcSingle<MintSpendNotesResponse>(
71
+ 'mint',
72
+ 'spend_notes',
73
+ {
74
+ amount: amountMsats,
75
+ try_cancel_after: duration,
76
+ include_invite: includeInvite,
77
+ extra_meta: extraMeta,
78
+ },
79
+ )
80
+ const notes = res[1]
81
+ const operationId = res[0]
82
+
83
+ return {
84
+ notes,
85
+ operation_id: operationId,
86
+ }
87
+ }
88
+
89
+ /** https://web.fedimint.org/core/FedimintWallet/MintService/parseEcash */
90
+ async parseNotes(oobNotes: string) {
91
+ return await this.client.rpcSingle<MSats>('mint', 'validate_notes', {
92
+ oob_notes: oobNotes,
93
+ })
94
+ }
95
+
96
+ async tryCancelSpendNotes(operationId: string) {
97
+ await this.client.rpcSingle('mint', 'try_cancel_spend_notes', {
98
+ operation_id: operationId,
99
+ })
100
+ }
101
+
102
+ subscribeSpendNotes(
103
+ operationId: string,
104
+ onSuccess: (state: SpendNotesState) => void = () => {},
105
+ onError: (error: string) => void = () => {},
106
+ ) {
107
+ return this.client.rpcStream<SpendNotesState>(
108
+ 'mint',
109
+ 'subscribe_spend_notes',
110
+ { operation_id: operationId },
111
+ (res) => onSuccess(res),
112
+ onError,
113
+ )
114
+ }
115
+
116
+ async awaitSpendOobRefund(operationId: string) {
117
+ return await this.client.rpcSingle('mint', 'await_spend_oob_refund', {
118
+ operation_id: operationId,
119
+ })
120
+ }
121
+
122
+ async getNotesByDenomination() {
123
+ return await this.client.rpcSingle<NoteCountByDenomination>(
124
+ 'mint',
125
+ 'note_counts_by_denomination',
126
+ {},
127
+ )
128
+ }
129
+ }
@@ -0,0 +1,28 @@
1
+ import type { JSONValue } from '../types'
2
+ import { TransportClient } from '../transport'
3
+
4
+ export class RecoveryService {
5
+ constructor(private client: TransportClient) {}
6
+
7
+ async hasPendingRecoveries() {
8
+ return await this.client.rpcSingle<boolean>(
9
+ '',
10
+ 'has_pending_recoveries',
11
+ {},
12
+ )
13
+ }
14
+
15
+ async waitForAllRecoveries() {
16
+ await this.client.rpcSingle('', 'wait_for_all_recoveries', {})
17
+ }
18
+
19
+ subscribeToRecoveryProgress(
20
+ onSuccess: (progress: { module_id: number; progress: JSONValue }) => void,
21
+ onError: (error: string) => void,
22
+ ) {
23
+ return this.client.rpcStream<{
24
+ module_id: number
25
+ progress: JSONValue
26
+ }>('', 'subscribe_to_recovery_progress', {}, onSuccess, onError)
27
+ }
28
+ }
@@ -0,0 +1,59 @@
1
+ import { expect } from 'vitest'
2
+ import { walletTest } from '../test/fixtures'
3
+ import { TxOutputSummary, WalletSummary } from '../types'
4
+
5
+ walletTest(
6
+ 'getWalletSummary should return empty object if wallet is empty',
7
+ async ({ wallet }) => {
8
+ expect(wallet).toBeDefined()
9
+ expect(wallet.isOpen()).toBe(true)
10
+
11
+ const balance = await wallet.balance.getBalance()
12
+ expect(balance).toEqual(0)
13
+
14
+ const summary = await wallet.wallet.getWalletSummary()
15
+ const expectedSummary = {
16
+ spendable_utxos: expect.any(Array<TxOutputSummary>),
17
+ unsigned_peg_out_txos: expect.any(Array<TxOutputSummary>),
18
+ unsigned_change_utxos: expect.any(Array<TxOutputSummary>),
19
+ unconfirmed_peg_out_txos: expect.any(Array<TxOutputSummary>),
20
+ unconfirmed_change_utxos: expect.any(Array<TxOutputSummary>),
21
+ } satisfies WalletSummary
22
+ expect(summary).toEqual(expect.objectContaining(expectedSummary))
23
+ },
24
+ )
25
+
26
+ walletTest(
27
+ 'generateAddress should always return an address',
28
+ async ({ wallet }) => {
29
+ expect(wallet).toBeDefined()
30
+ expect(wallet.isOpen()).toBe(true)
31
+ const response = await wallet.wallet.generateAddress()
32
+
33
+ expect(response, 'generateAddress').toEqual({
34
+ deposit_address: expect.any(String),
35
+ operation_id: expect.any(String),
36
+ })
37
+ },
38
+ )
39
+
40
+ walletTest(
41
+ 'sendOnchain should return an operation ID after sending funds',
42
+ async ({ fundedWalletBeefy }) => {
43
+ expect(fundedWalletBeefy).toBeDefined()
44
+ expect(fundedWalletBeefy.isOpen()).toBe(true)
45
+
46
+ const amountSat = 100
47
+ const address =
48
+ 'bcrt1qphk8q2v8he2autevdcefnnwjl4yc2hm74uuvhaa6nhrnkd3gfrwq6mnr76'
49
+
50
+ const response = await fundedWalletBeefy.wallet.sendOnchain(
51
+ amountSat,
52
+ address,
53
+ )
54
+
55
+ expect(response, 'send onchain').toEqual({
56
+ operation_id: expect.any(String),
57
+ })
58
+ },
59
+ )
@@ -0,0 +1,50 @@
1
+ import {
2
+ JSONValue,
3
+ WalletSummary,
4
+ GenerateAddressResponse,
5
+ WalletDepositState,
6
+ } from '../types'
7
+ import { TransportClient } from '../transport'
8
+
9
+ export class WalletService {
10
+ constructor(private client: TransportClient) {}
11
+
12
+ async getWalletSummary(): Promise<WalletSummary> {
13
+ return await this.client.rpcSingle('wallet', 'get_wallet_summary', {})
14
+ }
15
+
16
+ async generateAddress(extraMeta: JSONValue = {}) {
17
+ return await this.client.rpcSingle<GenerateAddressResponse>(
18
+ 'wallet',
19
+ 'peg_in',
20
+ {
21
+ extra_meta: extraMeta,
22
+ },
23
+ )
24
+ }
25
+
26
+ async sendOnchain(
27
+ amountSat: number,
28
+ address: string,
29
+ extraMeta: JSONValue = {},
30
+ ): Promise<{ operation_id: string }> {
31
+ return await this.client.rpcSingle('wallet', 'peg_out', {
32
+ amount_sat: amountSat,
33
+ destination_address: address,
34
+ extra_meta: extraMeta,
35
+ })
36
+ }
37
+ subscribeDeposit(
38
+ operation_id: string,
39
+ onSuccess: (state: WalletDepositState) => void = () => {},
40
+ onError: (error: string) => void = () => {},
41
+ ) {
42
+ return this.client.rpcStream(
43
+ 'ln',
44
+ 'subscribe_deposit',
45
+ { operation_id: operation_id },
46
+ onSuccess,
47
+ onError,
48
+ )
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ export { MintService } from './MintService'
2
+ export { BalanceService } from './BalanceService'
3
+ export { LightningService } from './LightningService'
4
+ export { RecoveryService } from './RecoveryService'
5
+ export { FederationService } from './FederationService'
6
+ export { WalletService } from './WalletService'
@@ -0,0 +1,254 @@
1
+ import type {
2
+ CancelFunction,
3
+ JSONValue,
4
+ ModuleKind,
5
+ StreamError,
6
+ StreamResult,
7
+ } from '../types'
8
+ import { Logger } from '../utils/logger'
9
+ import type {
10
+ Transport,
11
+ TransportMessage,
12
+ TransportMessageType,
13
+ } from '@fedimint/types'
14
+
15
+ /**
16
+ * Handles communication with a generic transport.
17
+ * Must be instantiated with a platform-specific transport. (wasm for web, react native, etc.)
18
+ */
19
+ export class TransportClient {
20
+ // Generic Transport. Can be wasm, react native, node, etc.
21
+ private readonly transport: Transport
22
+ private requestCounter = 0
23
+ private requestCallbacks = new Map<number, (value: any) => void>()
24
+ private initPromise: Promise<boolean> | undefined = undefined
25
+ logger: Logger
26
+
27
+ /**
28
+ * @summary Constructor for the TransportClient
29
+ * @param transport - The platform-specific transport to use. (wasm for web, react native, etc.)
30
+ */
31
+ constructor(transport: Transport) {
32
+ this.transport = transport
33
+ this.logger = new Logger(transport.logger)
34
+ this.transport.setMessageHandler(this.handleTransportMessage)
35
+ this.transport.setErrorHandler(this.handleTransportError)
36
+ this.logger.info('TransportClient instantiated')
37
+ this.logger.debug('TransportClient transport', transport)
38
+ }
39
+
40
+ // Idempotent setup - Loads the wasm module
41
+ initialize() {
42
+ if (this.initPromise) return this.initPromise
43
+ this.initPromise = this.sendSingleMessage('init')
44
+ return this.initPromise
45
+ }
46
+
47
+ private handleLogMessage(message: TransportMessage) {
48
+ const { type, level, message: logMessage, ...data } = message
49
+ this.logger.info(String(level), String(logMessage), data)
50
+ }
51
+
52
+ private handleTransportError = (error: unknown) => {
53
+ this.logger.error('TransportClient error', error)
54
+ }
55
+
56
+ private handleTransportMessage = (message: TransportMessage) => {
57
+ const { type, requestId, ...data } = message
58
+ if (type === 'log') {
59
+ this.handleLogMessage(message)
60
+ }
61
+ const streamCallback =
62
+ requestId !== undefined ? this.requestCallbacks.get(requestId) : undefined
63
+ // TODO: Handle errors... maybe have another callbacks list for errors?
64
+ this.logger.debug('TransportClient - handleTransportMessage', message)
65
+ if (streamCallback) {
66
+ streamCallback(data) // {data: something} OR {error: something}
67
+ } else if (requestId !== undefined) {
68
+ this.logger.warn(
69
+ 'TransportClient - handleTransportMessage - received message with no callback',
70
+ requestId,
71
+ message,
72
+ )
73
+ }
74
+ }
75
+
76
+ // TODO: Handle errors... maybe have another callbacks list for errors?
77
+ // TODO: Handle timeouts
78
+ // TODO: Handle multiple errors
79
+
80
+ sendSingleMessage<
81
+ Response extends JSONValue = JSONValue,
82
+ Payload extends JSONValue = JSONValue,
83
+ >(type: TransportMessageType, payload?: Payload) {
84
+ return new Promise<Response>((resolve, reject) => {
85
+ const requestId = ++this.requestCounter
86
+ this.logger.debug(
87
+ 'TransportClient - sendSingleMessage',
88
+ requestId,
89
+ type,
90
+ payload,
91
+ )
92
+ this.requestCallbacks.set(
93
+ requestId,
94
+ (response: StreamResult<Response>) => {
95
+ this.requestCallbacks.delete(requestId)
96
+ this.logger.debug(
97
+ 'TransportClient - sendSingleMessage - response',
98
+ requestId,
99
+ response,
100
+ )
101
+ if (response.data) resolve(response.data)
102
+ else if (response.error) reject(response.error)
103
+ else
104
+ this.logger.warn(
105
+ 'TransportClient - sendSingleMessage - malformed response',
106
+ requestId,
107
+ response,
108
+ )
109
+ },
110
+ )
111
+ this.transport.postMessage({ type, payload, requestId })
112
+ })
113
+ }
114
+
115
+ /**
116
+ * @summary Initiates an RPC stream with the specified module and method.
117
+ *
118
+ * @description
119
+ * This function sets up an RPC stream by sending a request to a worker and
120
+ * handling responses asynchronously. It ensures that unsubscription is handled
121
+ * correctly, even if the unsubscribe function is called before the subscription
122
+ * is fully established, by deferring the unsubscription attempt using `setTimeout`.
123
+ *
124
+ * The function operates in a non-blocking manner, leveraging Promises to manage
125
+ * asynchronous operations and callbacks to handle responses.
126
+ *
127
+ *
128
+ * @template Response - The expected type of the successful response.
129
+ * @template Body - The type of the request body.
130
+ * @param module - The module kind to interact with.
131
+ * @param method - The method name to invoke on the module.
132
+ * @param body - The request payload.
133
+ * @param onSuccess - Callback invoked with the response data on success.
134
+ * @param onError - Callback invoked with error information if an error occurs.
135
+ * @param onEnd - Optional callback invoked when the stream ends.
136
+ * @returns A function that can be called to cancel the subscription.
137
+ *
138
+ */
139
+ rpcStream<
140
+ Response extends JSONValue = JSONValue,
141
+ Body extends JSONValue = JSONValue,
142
+ >(
143
+ module: ModuleKind,
144
+ method: string,
145
+ body: Body,
146
+ onSuccess: (res: Response) => void,
147
+ onError: (res: StreamError['error']) => void,
148
+ onEnd: () => void = () => {},
149
+ ): CancelFunction {
150
+ const requestId = ++this.requestCounter
151
+ this.logger.debug(
152
+ 'TransportClient - rpcStream',
153
+ requestId,
154
+ module,
155
+ method,
156
+ body,
157
+ )
158
+ let unsubscribe: (value: void) => void = () => {}
159
+ let isSubscribed = false
160
+
161
+ const unsubscribePromise = new Promise<void>((resolve) => {
162
+ unsubscribe = () => {
163
+ if (isSubscribed) {
164
+ // If already subscribed, resolve immediately to trigger unsubscription
165
+ resolve()
166
+ } else {
167
+ // If not yet subscribed, defer the unsubscribe attempt to the next event loop tick
168
+ // This ensures that subscription setup has time to complete
169
+ setTimeout(() => unsubscribe(), 0)
170
+ }
171
+ }
172
+ })
173
+
174
+ // Initiate the inner RPC stream setup asynchronously
175
+ this._rpcStreamInner(
176
+ requestId,
177
+ module,
178
+ method,
179
+ body,
180
+ onSuccess,
181
+ onError,
182
+ onEnd,
183
+ unsubscribePromise,
184
+ ).then(() => {
185
+ isSubscribed = true
186
+ })
187
+
188
+ return unsubscribe
189
+ }
190
+
191
+ private async _rpcStreamInner<
192
+ Response extends JSONValue = JSONValue,
193
+ Body extends JSONValue = JSONValue,
194
+ >(
195
+ requestId: number,
196
+ module: ModuleKind,
197
+ method: string,
198
+ body: Body,
199
+ onSuccess: (res: Response) => void,
200
+ onError: (res: StreamError['error']) => void,
201
+ onEnd: () => void = () => {},
202
+ unsubscribePromise: Promise<void>,
203
+ // Unsubscribe function
204
+ ) {
205
+ this.requestCallbacks.set(requestId, (response: StreamResult<Response>) => {
206
+ if (response.error !== undefined) {
207
+ onError(response.error)
208
+ } else if (response.data !== undefined) {
209
+ onSuccess(response.data)
210
+ } else if (response.end !== undefined) {
211
+ this.requestCallbacks.delete(requestId)
212
+ onEnd()
213
+ }
214
+ })
215
+ this.transport.postMessage({
216
+ type: 'rpc',
217
+ payload: { module, method, body },
218
+ requestId,
219
+ })
220
+
221
+ unsubscribePromise.then(() => {
222
+ this.transport.postMessage({
223
+ type: 'unsubscribe',
224
+ requestId,
225
+ })
226
+ this.requestCallbacks.delete(requestId)
227
+ })
228
+ }
229
+
230
+ rpcSingle<
231
+ Response extends JSONValue = JSONValue,
232
+ Error extends string = string,
233
+ >(module: ModuleKind, method: string, body: JSONValue) {
234
+ this.logger.debug('TransportClient - rpcSingle', module, method, body)
235
+ return new Promise<Response>((resolve, reject) => {
236
+ this.rpcStream<Response>(module, method, body, resolve, reject)
237
+ })
238
+ }
239
+
240
+ async cleanup() {
241
+ await this.sendSingleMessage('cleanup')
242
+ this.requestCounter = 0
243
+ this.initPromise = undefined
244
+ this.requestCallbacks.clear()
245
+ }
246
+
247
+ // For Testing
248
+ _getRequestCounter() {
249
+ return this.requestCounter
250
+ }
251
+ _getRequestCallbackMap() {
252
+ return this.requestCallbacks
253
+ }
254
+ }
@@ -0,0 +1 @@
1
+ export { TransportClient } from './TransportClient'
@@ -0,0 +1,3 @@
1
+ export * from './wallet'
2
+ export * from './utils'
3
+ export * from './transport'
@@ -0,0 +1 @@
1
+ export * from '@fedimint/types'
@@ -0,0 +1,23 @@
1
+ import type { JSONValue } from '@fedimint/types'
2
+
3
+ type Alias<T> = T & {}
4
+ type Resolve<T> = T & unknown
5
+
6
+ type Seconds = Alias<number>
7
+ type Nanos = Alias<number>
8
+
9
+ type Duration = {
10
+ nanos: Nanos
11
+ secs: Seconds
12
+ }
13
+
14
+ type MSats = Alias<number>
15
+ type Sats = Alias<number>
16
+
17
+ type JSONObject = Record<string, JSONValue>
18
+
19
+ type Result<T, U = string> =
20
+ | { success: true; data?: T }
21
+ | { success: false; error: U }
22
+
23
+ export { Alias, Resolve, Duration, MSats, Sats, JSONValue, JSONObject, Result }