@fedimint/core-web 0.0.0-20250617190659
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.
- package/LICENSE +21 -0
- package/README.md +16 -0
- package/dist/dts/FedimintWallet.d.ts +114 -0
- package/dist/dts/FedimintWallet.d.ts.map +1 -0
- package/dist/dts/index.d.ts +3 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/services/BalanceService.d.ts +15 -0
- package/dist/dts/services/BalanceService.d.ts.map +1 -0
- package/dist/dts/services/FederationService.d.ts +11 -0
- package/dist/dts/services/FederationService.d.ts.map +1 -0
- package/dist/dts/services/LightningService.d.ts +47 -0
- package/dist/dts/services/LightningService.d.ts.map +1 -0
- package/dist/dts/services/MintService.d.ts +23 -0
- package/dist/dts/services/MintService.d.ts.map +1 -0
- package/dist/dts/services/RecoveryService.d.ts +13 -0
- package/dist/dts/services/RecoveryService.d.ts.map +1 -0
- package/dist/dts/services/WalletService.d.ts +8 -0
- package/dist/dts/services/WalletService.d.ts.map +1 -0
- package/dist/dts/services/index.d.ts +7 -0
- package/dist/dts/services/index.d.ts.map +1 -0
- package/dist/dts/types/index.d.ts +4 -0
- package/dist/dts/types/index.d.ts.map +1 -0
- package/dist/dts/types/utils.d.ts +23 -0
- package/dist/dts/types/utils.d.ts.map +1 -0
- package/dist/dts/types/wallet.d.ts +102 -0
- package/dist/dts/types/wallet.d.ts.map +1 -0
- package/dist/dts/types/worker.d.ts +4 -0
- package/dist/dts/types/worker.d.ts.map +1 -0
- package/dist/dts/utils/logger.d.ts +17 -0
- package/dist/dts/utils/logger.d.ts.map +1 -0
- package/dist/dts/worker/WorkerClient.d.ts +44 -0
- package/dist/dts/worker/WorkerClient.d.ts.map +1 -0
- package/dist/dts/worker/index.d.ts +2 -0
- package/dist/dts/worker/index.d.ts.map +1 -0
- package/dist/index.d.ts +392 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/worker.js +2 -0
- package/dist/worker.js.map +1 -0
- package/package.json +43 -0
- package/src/FedimintWallet.test.ts +73 -0
- package/src/FedimintWallet.ts +215 -0
- package/src/index.ts +2 -0
- package/src/services/BalanceService.test.ts +50 -0
- package/src/services/BalanceService.ts +29 -0
- package/src/services/FederationService.test.ts +58 -0
- package/src/services/FederationService.ts +24 -0
- package/src/services/LightningService.test.ts +208 -0
- package/src/services/LightningService.ts +274 -0
- package/src/services/MintService.test.ts +74 -0
- package/src/services/MintService.ts +129 -0
- package/src/services/RecoveryService.ts +28 -0
- package/src/services/WalletService.test.ts +24 -0
- package/src/services/WalletService.ts +10 -0
- package/src/services/index.ts +6 -0
- package/src/test/TestFedimintWallet.ts +26 -0
- package/src/test/TestingService.ts +79 -0
- package/src/test/crypto.ts +44 -0
- package/src/test/fixtures.test.ts +18 -0
- package/src/test/fixtures.ts +70 -0
- package/src/types/index.ts +3 -0
- package/src/types/utils.ts +29 -0
- package/src/types/wallet.ts +141 -0
- package/src/types/worker.ts +16 -0
- package/src/utils/logger.ts +61 -0
- package/src/worker/WorkerClient.test.ts +6 -0
- package/src/worker/WorkerClient.ts +236 -0
- package/src/worker/index.ts +1 -0
- package/src/worker/worker.js +167 -0
- package/src/worker/worker.test.ts +90 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { WorkerClient } from '../worker'
|
|
2
|
+
import type {
|
|
3
|
+
CreateBolt11Response,
|
|
4
|
+
GatewayInfo,
|
|
5
|
+
JSONObject,
|
|
6
|
+
LightningGateway,
|
|
7
|
+
LnPayState,
|
|
8
|
+
LnReceiveState,
|
|
9
|
+
OutgoingLightningPayment,
|
|
10
|
+
} from '../types'
|
|
11
|
+
|
|
12
|
+
export class LightningService {
|
|
13
|
+
constructor(private client: WorkerClient) {}
|
|
14
|
+
|
|
15
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/createInvoice#lightning-createinvoice */
|
|
16
|
+
async createInvoice(
|
|
17
|
+
amountMsats: number,
|
|
18
|
+
description: string,
|
|
19
|
+
expiryTime?: number, // in seconds
|
|
20
|
+
gatewayInfo?: GatewayInfo,
|
|
21
|
+
extraMeta?: JSONObject,
|
|
22
|
+
) {
|
|
23
|
+
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
|
|
24
|
+
return await this.client.rpcSingle<CreateBolt11Response>(
|
|
25
|
+
'ln',
|
|
26
|
+
'create_bolt11_invoice',
|
|
27
|
+
{
|
|
28
|
+
amount: amountMsats,
|
|
29
|
+
description,
|
|
30
|
+
expiry_time: expiryTime ?? null,
|
|
31
|
+
extra_meta: extraMeta ?? {},
|
|
32
|
+
gateway,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async createInvoiceTweaked(
|
|
38
|
+
amountMsats: number,
|
|
39
|
+
description: string,
|
|
40
|
+
tweakKey: string,
|
|
41
|
+
index: number,
|
|
42
|
+
expiryTime?: number, // in seconds
|
|
43
|
+
gatewayInfo?: GatewayInfo,
|
|
44
|
+
extraMeta?: JSONObject,
|
|
45
|
+
) {
|
|
46
|
+
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
|
|
47
|
+
return await this.client.rpcSingle<CreateBolt11Response>(
|
|
48
|
+
'ln',
|
|
49
|
+
'create_bolt11_invoice_for_user_tweaked',
|
|
50
|
+
{
|
|
51
|
+
amount: amountMsats,
|
|
52
|
+
description,
|
|
53
|
+
expiry_time: expiryTime ?? null,
|
|
54
|
+
user_key: tweakKey,
|
|
55
|
+
index,
|
|
56
|
+
extra_meta: extraMeta ?? {},
|
|
57
|
+
gateway,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Returns the operation ids of payments received to the tweaks of the user secret key
|
|
63
|
+
async scanReceivesForTweaks(
|
|
64
|
+
tweakKey: string,
|
|
65
|
+
indices: number[],
|
|
66
|
+
extraMeta?: JSONObject,
|
|
67
|
+
) {
|
|
68
|
+
return await this.client.rpcSingle<string[]>(
|
|
69
|
+
'ln',
|
|
70
|
+
'scan_receive_for_user_tweaked',
|
|
71
|
+
{
|
|
72
|
+
user_key: tweakKey,
|
|
73
|
+
indices,
|
|
74
|
+
extra_meta: extraMeta ?? {},
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async _getDefaultGatewayInfo() {
|
|
80
|
+
await this.updateGatewayCache()
|
|
81
|
+
const gateways = await this.listGateways()
|
|
82
|
+
return gateways[0]?.info
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/payInvoice#lightning-payinvoice-invoice-string */
|
|
86
|
+
async payInvoice(
|
|
87
|
+
invoice: string,
|
|
88
|
+
gatewayInfo?: GatewayInfo,
|
|
89
|
+
extraMeta?: JSONObject,
|
|
90
|
+
) {
|
|
91
|
+
const gateway = gatewayInfo ?? (await this._getDefaultGatewayInfo())
|
|
92
|
+
return await this.client.rpcSingle<OutgoingLightningPayment>(
|
|
93
|
+
'ln',
|
|
94
|
+
'pay_bolt11_invoice',
|
|
95
|
+
{
|
|
96
|
+
maybe_gateway: gateway,
|
|
97
|
+
invoice,
|
|
98
|
+
extra_meta: extraMeta ?? {},
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/payInvoice#lightning-payinvoicesync-invoice-string */
|
|
104
|
+
async payInvoiceSync(
|
|
105
|
+
invoice: string,
|
|
106
|
+
timeoutMs: number = 10000,
|
|
107
|
+
gatewayInfo?: GatewayInfo,
|
|
108
|
+
extraMeta?: JSONObject,
|
|
109
|
+
) {
|
|
110
|
+
return new Promise<
|
|
111
|
+
| { success: false; error?: string }
|
|
112
|
+
| {
|
|
113
|
+
success: true
|
|
114
|
+
data: { feeMsats: number; preimage: string }
|
|
115
|
+
}
|
|
116
|
+
>(async (resolve, reject) => {
|
|
117
|
+
const { contract_id, fee } = await this.payInvoice(
|
|
118
|
+
invoice,
|
|
119
|
+
gatewayInfo,
|
|
120
|
+
extraMeta,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// TODO: handle error handling for other subscription statuses
|
|
124
|
+
const unsubscribe = this.subscribeLnPay(contract_id, (res) => {
|
|
125
|
+
if (typeof res !== 'string' && 'success' in res) {
|
|
126
|
+
clearTimeout(timeoutId)
|
|
127
|
+
unsubscribe()
|
|
128
|
+
resolve({
|
|
129
|
+
success: true,
|
|
130
|
+
data: { feeMsats: fee, preimage: res.success.preimage },
|
|
131
|
+
})
|
|
132
|
+
} else if (typeof res !== 'string' && 'unexpected_error' in res) {
|
|
133
|
+
reject(new Error(res.unexpected_error.error_message))
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const timeoutId = setTimeout(() => {
|
|
138
|
+
unsubscribe()
|
|
139
|
+
resolve({ success: false, error: 'Payment timeout' })
|
|
140
|
+
}, timeoutMs)
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// TODO: Document
|
|
145
|
+
subscribeLnClaim(
|
|
146
|
+
operationId: string,
|
|
147
|
+
onSuccess: (state: LnReceiveState) => void = () => {},
|
|
148
|
+
onError: (error: string) => void = () => {},
|
|
149
|
+
) {
|
|
150
|
+
return this.client.rpcStream(
|
|
151
|
+
'ln',
|
|
152
|
+
'subscribe_ln_claim',
|
|
153
|
+
{ operation_id: operationId },
|
|
154
|
+
onSuccess,
|
|
155
|
+
onError,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// TODO: Document (for external payments only)
|
|
160
|
+
// TODO: Make this work for BOTH internal and external payments
|
|
161
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/payInvoice#lightning-payinvoice-invoice-string */
|
|
162
|
+
subscribeLnPay(
|
|
163
|
+
operationId: string,
|
|
164
|
+
onSuccess: (state: LnPayState) => void = () => {},
|
|
165
|
+
onError: (error: string) => void = () => {},
|
|
166
|
+
) {
|
|
167
|
+
return this.client.rpcStream(
|
|
168
|
+
'ln',
|
|
169
|
+
'subscribe_ln_pay',
|
|
170
|
+
{ operation_id: operationId },
|
|
171
|
+
onSuccess,
|
|
172
|
+
onError,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/payInvoice#lightning-payinvoice-invoice-string */
|
|
177
|
+
async waitForPay(operationId: string) {
|
|
178
|
+
return new Promise<
|
|
179
|
+
| { success: false; error?: string }
|
|
180
|
+
| { success: true; data: { preimage: string } }
|
|
181
|
+
>((resolve, reject) => {
|
|
182
|
+
let unsubscribe: () => void
|
|
183
|
+
const timeoutId = setTimeout(() => {
|
|
184
|
+
resolve({ success: false, error: 'Waiting for receive timeout' })
|
|
185
|
+
}, 15000)
|
|
186
|
+
|
|
187
|
+
unsubscribe = this.subscribeLnPay(
|
|
188
|
+
operationId,
|
|
189
|
+
(res) => {
|
|
190
|
+
if (typeof res !== 'string' && 'success' in res) {
|
|
191
|
+
clearTimeout(timeoutId)
|
|
192
|
+
unsubscribe()
|
|
193
|
+
resolve({
|
|
194
|
+
success: true,
|
|
195
|
+
data: { preimage: res.success.preimage },
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
(error) => {
|
|
200
|
+
clearTimeout(timeoutId)
|
|
201
|
+
unsubscribe()
|
|
202
|
+
reject(error)
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/createInvoice#lightning-createinvoice */
|
|
209
|
+
subscribeLnReceive(
|
|
210
|
+
operationId: string,
|
|
211
|
+
onSuccess: (state: LnReceiveState) => void = () => {},
|
|
212
|
+
onError: (error: string) => void = () => {},
|
|
213
|
+
) {
|
|
214
|
+
return this.client.rpcStream(
|
|
215
|
+
'ln',
|
|
216
|
+
'subscribe_ln_receive',
|
|
217
|
+
{ operation_id: operationId },
|
|
218
|
+
onSuccess,
|
|
219
|
+
onError,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** https://web.fedimint.org/core/FedimintWallet/LightningService/createInvoice#lightning-createinvoice */
|
|
224
|
+
async waitForReceive(operationId: string, timeoutMs: number = 15000) {
|
|
225
|
+
return new Promise<LnReceiveState>((resolve, reject) => {
|
|
226
|
+
let unsubscribe: () => void
|
|
227
|
+
const timeoutId = setTimeout(() => {
|
|
228
|
+
reject(new Error('Timeout waiting for receive'))
|
|
229
|
+
}, timeoutMs)
|
|
230
|
+
|
|
231
|
+
unsubscribe = this.subscribeLnReceive(
|
|
232
|
+
operationId,
|
|
233
|
+
(res) => {
|
|
234
|
+
if (res === 'claimed') {
|
|
235
|
+
clearTimeout(timeoutId)
|
|
236
|
+
unsubscribe()
|
|
237
|
+
resolve(res)
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
(error) => {
|
|
241
|
+
clearTimeout(timeoutId)
|
|
242
|
+
unsubscribe()
|
|
243
|
+
reject(error)
|
|
244
|
+
},
|
|
245
|
+
)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async getGateway(
|
|
250
|
+
gatewayId: string | null = null,
|
|
251
|
+
forceInternal: boolean = false,
|
|
252
|
+
) {
|
|
253
|
+
return await this.client.rpcSingle<LightningGateway | null>(
|
|
254
|
+
'ln',
|
|
255
|
+
'get_gateway',
|
|
256
|
+
{
|
|
257
|
+
gateway_id: gatewayId,
|
|
258
|
+
force_internal: forceInternal,
|
|
259
|
+
},
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async listGateways() {
|
|
264
|
+
return await this.client.rpcSingle<LightningGateway[]>(
|
|
265
|
+
'ln',
|
|
266
|
+
'list_gateways',
|
|
267
|
+
{},
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async updateGatewayCache() {
|
|
272
|
+
return await this.client.rpcSingle('ln', 'update_gateway_cache', {})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
import { walletTest } from '../test/fixtures'
|
|
3
|
+
|
|
4
|
+
walletTest('redeemEcash should error on invalid ecash', async ({ wallet }) => {
|
|
5
|
+
expect(wallet).toBeDefined()
|
|
6
|
+
expect(wallet.isOpen()).toBe(true)
|
|
7
|
+
|
|
8
|
+
await expect(wallet.mint.redeemEcash('test')).rejects.toThrow()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
walletTest(
|
|
12
|
+
'reissueExternalNotes should throw if wallet is empty',
|
|
13
|
+
async ({ wallet }) => {
|
|
14
|
+
expect(wallet).toBeDefined()
|
|
15
|
+
expect(wallet.isOpen()).toBe(true)
|
|
16
|
+
|
|
17
|
+
await expect(wallet.mint.reissueExternalNotes('test')).rejects.toThrow()
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
walletTest('spendNotes should throw if wallet is empty', async ({ wallet }) => {
|
|
22
|
+
expect(wallet).toBeDefined()
|
|
23
|
+
expect(wallet.isOpen()).toBe(true)
|
|
24
|
+
|
|
25
|
+
await expect(wallet.mint.spendNotes(100)).rejects.toThrow()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
walletTest('parseNotes should parse notes', async ({ wallet }) => {
|
|
29
|
+
expect(wallet).toBeDefined()
|
|
30
|
+
expect(wallet.isOpen()).toBe(true)
|
|
31
|
+
|
|
32
|
+
await expect(wallet.mint.reissueExternalNotes('test')).rejects.toThrow()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
walletTest(
|
|
36
|
+
'getNotesByDenomination should return empty object if wallet is empty',
|
|
37
|
+
async ({ wallet }) => {
|
|
38
|
+
expect(wallet).toBeDefined()
|
|
39
|
+
expect(wallet.isOpen()).toBe(true)
|
|
40
|
+
|
|
41
|
+
const notes = await wallet.mint.getNotesByDenomination()
|
|
42
|
+
const balance = await wallet.balance.getBalance()
|
|
43
|
+
expect(balance).toEqual(0)
|
|
44
|
+
expect(notes).toBeDefined()
|
|
45
|
+
expect(notes).toEqual({})
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
walletTest(
|
|
50
|
+
'getNotesByDenomination should get notes by denomination',
|
|
51
|
+
async ({ fundedWallet }) => {
|
|
52
|
+
expect(fundedWallet).toBeDefined()
|
|
53
|
+
expect(fundedWallet.isOpen()).toBe(true)
|
|
54
|
+
|
|
55
|
+
const notes = await fundedWallet.mint.getNotesByDenomination()
|
|
56
|
+
const balance = await fundedWallet.balance.getBalance()
|
|
57
|
+
expect(balance).toEqual(10000)
|
|
58
|
+
expect(notes).toBeDefined()
|
|
59
|
+
expect(notes).toEqual({
|
|
60
|
+
'1': 2,
|
|
61
|
+
'1024': 3,
|
|
62
|
+
'128': 2,
|
|
63
|
+
'16': 3,
|
|
64
|
+
'2': 3,
|
|
65
|
+
'2048': 2,
|
|
66
|
+
'256': 3,
|
|
67
|
+
'32': 2,
|
|
68
|
+
'4': 2,
|
|
69
|
+
'512': 3,
|
|
70
|
+
'64': 2,
|
|
71
|
+
'8': 2,
|
|
72
|
+
})
|
|
73
|
+
},
|
|
74
|
+
)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { WorkerClient } from '../worker'
|
|
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: WorkerClient) {}
|
|
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 { WorkerClient } from '../worker'
|
|
3
|
+
|
|
4
|
+
export class RecoveryService {
|
|
5
|
+
constructor(private client: WorkerClient) {}
|
|
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,24 @@
|
|
|
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
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WalletSummary } from '../types'
|
|
2
|
+
import { WorkerClient } from '../worker'
|
|
3
|
+
|
|
4
|
+
export class WalletService {
|
|
5
|
+
constructor(private client: WorkerClient) {}
|
|
6
|
+
|
|
7
|
+
async getWalletSummary(): Promise<WalletSummary> {
|
|
8
|
+
return await this.client.rpcSingle('wallet', 'get_wallet_summary', {})
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -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,26 @@
|
|
|
1
|
+
import { FedimintWallet } from '../FedimintWallet'
|
|
2
|
+
import { WorkerClient } from '../worker/WorkerClient'
|
|
3
|
+
import { TestingService } from './TestingService'
|
|
4
|
+
|
|
5
|
+
export class TestFedimintWallet extends FedimintWallet {
|
|
6
|
+
public testing: TestingService
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
super()
|
|
10
|
+
this.testing = new TestingService(this.getWorkerClient(), this.lightning)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async fundWallet(amount: number) {
|
|
14
|
+
const info = await this.testing.getFaucetGatewayInfo()
|
|
15
|
+
const invoice = await this.lightning.createInvoice(amount, '', 1000, info)
|
|
16
|
+
await Promise.all([
|
|
17
|
+
this.testing.payFaucetInvoice(invoice.invoice),
|
|
18
|
+
this.lightning.waitForReceive(invoice.operation_id),
|
|
19
|
+
])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Method to expose the WorkerClient
|
|
23
|
+
getWorkerClient(): WorkerClient {
|
|
24
|
+
return this['_client']
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { LightningService } from '../services'
|
|
2
|
+
import { WorkerClient } from '../worker'
|
|
3
|
+
|
|
4
|
+
export const TESTING_INVITE =
|
|
5
|
+
'fed11qgqrsdnhwden5te0v9cxjtt4dekxzamxw4kz6mmjvvkhydted9ukg6r9xfsnx7th0fhn26tf093juamwv4u8gtnpwpcz7qqpyz0e327ua8geceutfrcaezwt22mk6s2rdy09kg72jrcmncng2gn0kp2m5sk'
|
|
6
|
+
|
|
7
|
+
// This is a testing service that allows for inspecting the internals
|
|
8
|
+
// of the WorkerClient. It is not intended for use in production.
|
|
9
|
+
export class TestingService {
|
|
10
|
+
public TESTING_INVITE: string
|
|
11
|
+
constructor(
|
|
12
|
+
private client: WorkerClient,
|
|
13
|
+
private lightning: LightningService,
|
|
14
|
+
) {
|
|
15
|
+
// Solo Mint on mutinynet
|
|
16
|
+
this.TESTING_INVITE = TESTING_INVITE
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getRequestCounter() {
|
|
20
|
+
return this.client._getRequestCounter()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getRequestCallbackMap() {
|
|
24
|
+
return this.client._getRequestCallbackMap()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async getInviteCode() {
|
|
28
|
+
const res = await fetch(`${import.meta.env.FAUCET}/connect-string`)
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
return await res.text()
|
|
31
|
+
} else {
|
|
32
|
+
throw new Error(`Failed to get invite code: ${await res.text()}`)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async getFaucetGatewayApi() {
|
|
37
|
+
const res = await fetch(`${import.meta.env.FAUCET}/gateway-api`)
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
return await res.text()
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(`Failed to get gateway: ${await res.text()}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getFaucetGatewayInfo() {
|
|
46
|
+
await this.lightning.updateGatewayCache()
|
|
47
|
+
const gateways = await this.lightning.listGateways()
|
|
48
|
+
const api = await this.getFaucetGatewayApi()
|
|
49
|
+
const gateway = gateways.find((g) => g.info.api === api)
|
|
50
|
+
if (!gateway) {
|
|
51
|
+
throw new Error(`Gateway not found: ${api}`)
|
|
52
|
+
}
|
|
53
|
+
return gateway.info
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async payFaucetInvoice(invoice: string) {
|
|
57
|
+
const res = await fetch(`${import.meta.env.FAUCET}/pay`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
body: invoice,
|
|
60
|
+
})
|
|
61
|
+
if (res.ok) {
|
|
62
|
+
return await res.text()
|
|
63
|
+
} else {
|
|
64
|
+
throw new Error(`Failed to pay faucet invoice: ${await res.text()}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async createFaucetInvoice(amount: number) {
|
|
69
|
+
const res = await fetch(`${import.meta.env.FAUCET}/invoice`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: amount.toString(),
|
|
72
|
+
})
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
return await res.text()
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error(`Failed to generate faucet invoice: ${await res.text()}`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as secp256k1 from 'secp256k1'
|
|
2
|
+
|
|
3
|
+
const randomBytes = (size: number): Uint8Array => {
|
|
4
|
+
const array = new Uint8Array(size)
|
|
5
|
+
window.crypto.getRandomValues(array)
|
|
6
|
+
return array
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface KeyPair {
|
|
10
|
+
secretKey: string
|
|
11
|
+
publicKey: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const keyPair = (secretKey?: Uint8Array): KeyPair => {
|
|
15
|
+
const privateKey: Uint8Array = secretKey
|
|
16
|
+
? validatePrivateKey(secretKey)
|
|
17
|
+
: generatePrivateKey()
|
|
18
|
+
|
|
19
|
+
const publicKey = secp256k1.publicKeyCreate(privateKey)
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
secretKey: Array.from(privateKey)
|
|
23
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
24
|
+
.join(''), // Convert Uint8Array to hex string
|
|
25
|
+
publicKey: Array.from(publicKey)
|
|
26
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
27
|
+
.join(''), // Convert Uint8Array to hex string
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const validatePrivateKey = (key: Uint8Array): Uint8Array => {
|
|
32
|
+
if (!secp256k1.privateKeyVerify(key)) {
|
|
33
|
+
throw new Error('Invalid private key provided')
|
|
34
|
+
}
|
|
35
|
+
return key
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const generatePrivateKey = (): Uint8Array => {
|
|
39
|
+
let key: Uint8Array
|
|
40
|
+
do {
|
|
41
|
+
key = randomBytes(32)
|
|
42
|
+
} while (!secp256k1.privateKeyVerify(key))
|
|
43
|
+
return key
|
|
44
|
+
}
|