@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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/dist/dts/FedimintWallet.d.ts +114 -0
  4. package/dist/dts/FedimintWallet.d.ts.map +1 -0
  5. package/dist/dts/index.d.ts +3 -0
  6. package/dist/dts/index.d.ts.map +1 -0
  7. package/dist/dts/services/BalanceService.d.ts +15 -0
  8. package/dist/dts/services/BalanceService.d.ts.map +1 -0
  9. package/dist/dts/services/FederationService.d.ts +11 -0
  10. package/dist/dts/services/FederationService.d.ts.map +1 -0
  11. package/dist/dts/services/LightningService.d.ts +47 -0
  12. package/dist/dts/services/LightningService.d.ts.map +1 -0
  13. package/dist/dts/services/MintService.d.ts +23 -0
  14. package/dist/dts/services/MintService.d.ts.map +1 -0
  15. package/dist/dts/services/RecoveryService.d.ts +13 -0
  16. package/dist/dts/services/RecoveryService.d.ts.map +1 -0
  17. package/dist/dts/services/WalletService.d.ts +8 -0
  18. package/dist/dts/services/WalletService.d.ts.map +1 -0
  19. package/dist/dts/services/index.d.ts +7 -0
  20. package/dist/dts/services/index.d.ts.map +1 -0
  21. package/dist/dts/types/index.d.ts +4 -0
  22. package/dist/dts/types/index.d.ts.map +1 -0
  23. package/dist/dts/types/utils.d.ts +23 -0
  24. package/dist/dts/types/utils.d.ts.map +1 -0
  25. package/dist/dts/types/wallet.d.ts +102 -0
  26. package/dist/dts/types/wallet.d.ts.map +1 -0
  27. package/dist/dts/types/worker.d.ts +4 -0
  28. package/dist/dts/types/worker.d.ts.map +1 -0
  29. package/dist/dts/utils/logger.d.ts +17 -0
  30. package/dist/dts/utils/logger.d.ts.map +1 -0
  31. package/dist/dts/worker/WorkerClient.d.ts +44 -0
  32. package/dist/dts/worker/WorkerClient.d.ts.map +1 -0
  33. package/dist/dts/worker/index.d.ts +2 -0
  34. package/dist/dts/worker/index.d.ts.map +1 -0
  35. package/dist/index.d.ts +392 -0
  36. package/dist/index.js +2 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/worker.js +2 -0
  39. package/dist/worker.js.map +1 -0
  40. package/package.json +43 -0
  41. package/src/FedimintWallet.test.ts +73 -0
  42. package/src/FedimintWallet.ts +215 -0
  43. package/src/index.ts +2 -0
  44. package/src/services/BalanceService.test.ts +50 -0
  45. package/src/services/BalanceService.ts +29 -0
  46. package/src/services/FederationService.test.ts +58 -0
  47. package/src/services/FederationService.ts +24 -0
  48. package/src/services/LightningService.test.ts +208 -0
  49. package/src/services/LightningService.ts +274 -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 +24 -0
  54. package/src/services/WalletService.ts +10 -0
  55. package/src/services/index.ts +6 -0
  56. package/src/test/TestFedimintWallet.ts +26 -0
  57. package/src/test/TestingService.ts +79 -0
  58. package/src/test/crypto.ts +44 -0
  59. package/src/test/fixtures.test.ts +18 -0
  60. package/src/test/fixtures.ts +70 -0
  61. package/src/types/index.ts +3 -0
  62. package/src/types/utils.ts +29 -0
  63. package/src/types/wallet.ts +141 -0
  64. package/src/types/worker.ts +16 -0
  65. package/src/utils/logger.ts +61 -0
  66. package/src/worker/WorkerClient.test.ts +6 -0
  67. package/src/worker/WorkerClient.ts +236 -0
  68. package/src/worker/index.ts +1 -0
  69. package/src/worker/worker.js +167 -0
  70. package/src/worker/worker.test.ts +90 -0
@@ -0,0 +1,215 @@
1
+ import { WorkerClient } from './worker'
2
+ import {
3
+ BalanceService,
4
+ MintService,
5
+ LightningService,
6
+ FederationService,
7
+ RecoveryService,
8
+ WalletService,
9
+ } from './services'
10
+ import { logger, type LogLevel } from './utils/logger'
11
+ import { FederationConfig, JSONValue } from './types'
12
+
13
+ const DEFAULT_CLIENT_NAME = 'fm-default' as const
14
+
15
+ export class FedimintWallet {
16
+ private _client: WorkerClient
17
+
18
+ public balance: BalanceService
19
+ public mint: MintService
20
+ public lightning: LightningService
21
+ public federation: FederationService
22
+ public recovery: RecoveryService
23
+ public wallet: WalletService
24
+
25
+ private _openPromise: Promise<void> | undefined = undefined
26
+ private _resolveOpen: () => void = () => {}
27
+ private _isOpen: boolean = false
28
+
29
+ /**
30
+ * Creates a new instance of FedimintWallet.
31
+ *
32
+ * This constructor initializes a FedimintWallet instance, which manages communication
33
+ * with a Web Worker. The Web Worker is responsible for running WebAssembly code that
34
+ * handles the core Fedimint Client operations.
35
+ *
36
+ * (default) When not in lazy mode, the constructor immediately initializes the
37
+ * Web Worker and begins loading the WebAssembly module in the background. This
38
+ * allows for faster subsequent operations but may increase initial load time.
39
+ *
40
+ * In lazy mode, the Web Worker and WebAssembly initialization are deferred until
41
+ * the first operation that requires them, reducing initial overhead at the cost
42
+ * of a slight delay on the first operation.
43
+ *
44
+ * @param {boolean} lazy - If true, delays Web Worker and WebAssembly initialization
45
+ * until needed. Default is false.
46
+ *
47
+ * @example
48
+ * // Create a wallet with immediate initialization
49
+ * const wallet = new FedimintWallet();
50
+ * wallet.open();
51
+ *
52
+ * // Create a wallet with lazy initialization
53
+ * const lazyWallet = new FedimintWallet(true);
54
+ * // Some time later...
55
+ * lazyWallet.initialize();
56
+ * lazyWallet.open();
57
+ */
58
+ constructor(lazy: boolean = false) {
59
+ this._openPromise = new Promise((resolve) => {
60
+ this._resolveOpen = resolve
61
+ })
62
+ this._client = new WorkerClient()
63
+ this.mint = new MintService(this._client)
64
+ this.lightning = new LightningService(this._client)
65
+ this.balance = new BalanceService(this._client)
66
+ this.federation = new FederationService(this._client)
67
+ this.recovery = new RecoveryService(this._client)
68
+ this.wallet = new WalletService(this._client)
69
+
70
+ logger.info('FedimintWallet instantiated')
71
+
72
+ if (!lazy) {
73
+ this.initialize()
74
+ }
75
+ }
76
+
77
+ async initialize() {
78
+ logger.info('Initializing WorkerClient')
79
+ await this._client.initialize()
80
+ logger.info('WorkerClient initialized')
81
+ }
82
+
83
+ async waitForOpen() {
84
+ if (this._isOpen) return Promise.resolve()
85
+ return this._openPromise
86
+ }
87
+
88
+ async open(clientName: string = DEFAULT_CLIENT_NAME) {
89
+ await this._client.initialize()
90
+ // TODO: Determine if this should be safe or throw
91
+ if (this._isOpen) throw new Error('The FedimintWallet is already open.')
92
+ const { success } = await this._client.sendSingleMessage<{
93
+ success: boolean
94
+ }>('open', { clientName })
95
+ if (success) {
96
+ this._isOpen = !!success
97
+ this._resolveOpen()
98
+ }
99
+ return success
100
+ }
101
+
102
+ async joinFederation(
103
+ inviteCode: string,
104
+ clientName: string = DEFAULT_CLIENT_NAME,
105
+ ) {
106
+ await this._client.initialize()
107
+ // TODO: Determine if this should be safe or throw
108
+ if (this._isOpen)
109
+ throw new Error(
110
+ 'The FedimintWallet is already open. You can only call `joinFederation` on closed clients.',
111
+ )
112
+ try {
113
+ const response = await this._client.sendSingleMessage<{
114
+ success: boolean
115
+ }>('join', { inviteCode, clientName })
116
+ if (response.success) {
117
+ this._isOpen = true
118
+ this._resolveOpen()
119
+ }
120
+
121
+ return response.success
122
+ } catch (e) {
123
+ logger.error('Error joining federation', e)
124
+ return false
125
+ }
126
+ }
127
+
128
+ /**
129
+ * This should ONLY be called when UNLOADING the wallet client.
130
+ * After this call, the FedimintWallet instance should be discarded.
131
+ */
132
+ async cleanup() {
133
+ this._openPromise = undefined
134
+ this._isOpen = false
135
+ await this._client.cleanup()
136
+ }
137
+
138
+ isOpen() {
139
+ return this._isOpen
140
+ }
141
+
142
+ async previewFederation(inviteCode: string) {
143
+ const response = this._client.sendSingleMessage<{
144
+ config: FederationConfig
145
+ federation_id: string
146
+ }>('previewFederation', { inviteCode })
147
+ return response
148
+ }
149
+
150
+ /**
151
+ * Sets the log level for the library.
152
+ * @param level The desired log level ('DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE').
153
+ */
154
+ setLogLevel(level: LogLevel) {
155
+ logger.setLevel(level)
156
+ logger.info(`Log level set to ${level}.`)
157
+ }
158
+
159
+ /**
160
+ * Parses a federation invite code and retrieves its details.
161
+ *
162
+ * This method sends the provided invite code to the WorkerClient for parsing.
163
+ * The response includes the federation_id and url.
164
+ *
165
+ * @param {string} inviteCode - The invite code to be parsed.
166
+ * @returns {Promise<{ federation_id: string, url: string}>}
167
+ * A promise that resolves to an object containing:
168
+ * - `federation_id`: The id of the feder.
169
+ * - `url`: One of the apipoints to connect to the federation
170
+ *
171
+ * @throws {Error} If the WorkerClient encounters an issue during the parsing process.
172
+ *
173
+ * @example
174
+ * const inviteCode = "example-invite-code";
175
+ * const parsedCode = await wallet.parseInviteCode(inviteCode);
176
+ * console.log(parsedCode.federation_id, parsedCode.url);
177
+ */
178
+ async parseInviteCode(inviteCode: string) {
179
+ const response = await this._client.sendSingleMessage<{
180
+ type: string
181
+ data: JSONValue
182
+ requestId: number
183
+ }>('parseInviteCode', { inviteCode })
184
+ return response
185
+ }
186
+
187
+ /**
188
+ * Parses a BOLT11 Lightning invoice and retrieves its details.
189
+ *
190
+ * This method sends the provided invoice string to the WorkerClient for parsing.
191
+ * The response includes details such as the amount, expiry, and memo.
192
+ *
193
+ * @param {string} invoiceStr - The BOLT11 invoice string to be parsed.
194
+ * @returns {Promise<{ amount: string, expiry: number, memo: string }>}
195
+ * A promise that resolves to an object containing:
196
+ * - `amount`: The amount specified in the invoice.
197
+ * - `expiry`: The expiry time of the invoice in seconds.
198
+ * - `memo`: A description or memo attached to the invoice.
199
+ *
200
+ * @throws {Error} If the WorkerClient encounters an issue during the parsing process.
201
+ *
202
+ * @example
203
+ * const invoiceStr = "lnbc1...";
204
+ * const parsedInvoice = await wallet.parseBolt11Invoice(invoiceStr);
205
+ * console.log(parsedInvoice.amount, parsedInvoice.expiry, parsedInvoice.memo);
206
+ */
207
+ async parseBolt11Invoice(invoiceStr: string) {
208
+ const response = await this._client.sendSingleMessage<{
209
+ type: string
210
+ data: JSONValue
211
+ requestId: number
212
+ }>('parseBolt11Invoice', { invoiceStr })
213
+ return response
214
+ }
215
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { FedimintWallet } from './FedimintWallet'
2
+ export type * from './types'
@@ -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.toBe(true)
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,29 @@
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
+ /** https://web.fedimint.org/core/FedimintWallet/BalanceService/getBalance */
12
+ async getBalance() {
13
+ return await this.client.rpcSingle<number>('', 'get_balance', {})
14
+ }
15
+
16
+ /** https://web.fedimint.org/core/FedimintWallet/BalanceService/subscribeBalance */
17
+ subscribeBalance(
18
+ onSuccess: (balanceMsats: number) => void = () => {},
19
+ onError: (error: string) => void = () => {},
20
+ ) {
21
+ return this.client.rpcStream<string>(
22
+ '',
23
+ 'subscribe_balance_changes',
24
+ {},
25
+ (res) => onSuccess(parseInt(res)),
26
+ onError,
27
+ )
28
+ }
29
+ }
@@ -0,0 +1,58 @@
1
+ import { expect } from 'vitest'
2
+ import { walletTest } from '../test/fixtures'
3
+
4
+ walletTest(
5
+ 'getConfig should return the federation config',
6
+ async ({ wallet }) => {
7
+ expect(wallet).toBeDefined()
8
+ expect(wallet.isOpen()).toBe(true)
9
+ const counterBefore = wallet.testing.getRequestCounter()
10
+ await expect(wallet.federation.getConfig()).resolves.toMatchObject({
11
+ api_endpoints: expect.any(Object),
12
+ broadcast_public_keys: expect.any(Object),
13
+ consensus_version: expect.any(Object),
14
+ meta: expect.any(Object),
15
+ modules: expect.any(Object),
16
+ })
17
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
18
+ },
19
+ )
20
+
21
+ walletTest(
22
+ 'getFederationId should return the federation id',
23
+ async ({ wallet }) => {
24
+ expect(wallet).toBeDefined()
25
+ expect(wallet.isOpen()).toBe(true)
26
+
27
+ const counterBefore = wallet.testing.getRequestCounter()
28
+ const federationId = await wallet.federation.getFederationId()
29
+ expect(federationId).toBeTypeOf('string')
30
+ expect(federationId).toHaveLength(64)
31
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
32
+ },
33
+ )
34
+
35
+ walletTest(
36
+ 'getInviteCode should return the invite code',
37
+ async ({ wallet }) => {
38
+ expect(wallet).toBeDefined()
39
+ expect(wallet.isOpen()).toBe(true)
40
+
41
+ const counterBefore = wallet.testing.getRequestCounter()
42
+ const inviteCode = await wallet.federation.getInviteCode(0)
43
+ expect(inviteCode).toBeTypeOf('string')
44
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
45
+ },
46
+ )
47
+
48
+ walletTest(
49
+ 'listOperations should return the list of operations',
50
+ async ({ wallet }) => {
51
+ expect(wallet).toBeDefined()
52
+ expect(wallet.isOpen()).toBe(true)
53
+
54
+ const counterBefore = wallet.testing.getRequestCounter()
55
+ await expect(wallet.federation.listOperations()).resolves.toMatchObject([])
56
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
57
+ },
58
+ )
@@ -0,0 +1,24 @@
1
+ import type { JSONValue } from '../types'
2
+ import { WorkerClient } from '../worker'
3
+
4
+ export class FederationService {
5
+ constructor(private client: WorkerClient) {}
6
+
7
+ async getConfig() {
8
+ return await this.client.rpcSingle('', 'get_config', {})
9
+ }
10
+
11
+ async getFederationId() {
12
+ return await this.client.rpcSingle<string>('', 'get_federation_id', {})
13
+ }
14
+
15
+ async getInviteCode(peer: number = 0) {
16
+ return await this.client.rpcSingle<string | null>('', 'get_invite_code', {
17
+ peer,
18
+ })
19
+ }
20
+
21
+ async listOperations() {
22
+ return await this.client.rpcSingle<JSONValue[]>('', 'list_operations', {})
23
+ }
24
+ }
@@ -0,0 +1,208 @@
1
+ import { expect } from 'vitest'
2
+ import { keyPair } from '../test/crypto'
3
+ import { walletTest } from '../test/fixtures'
4
+
5
+ walletTest(
6
+ 'createInvoice should create a bolt11 invoice',
7
+ async ({ wallet }) => {
8
+ expect(wallet).toBeDefined()
9
+ expect(wallet.isOpen()).toBe(true)
10
+
11
+ const counterBefore = wallet.testing.getRequestCounter()
12
+ const invoice = await wallet.lightning.createInvoice(100, 'test')
13
+ expect(invoice).toBeDefined()
14
+ expect(invoice).toMatchObject({
15
+ invoice: expect.any(String),
16
+ operation_id: expect.any(String),
17
+ })
18
+ // 3 requests were made, one for the invoice, one for refreshing the
19
+ // gateway cache, one for getting the gateway info
20
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 3)
21
+
22
+ // Test with expiry time
23
+ await expect(
24
+ wallet.lightning.createInvoice(100, 'test', 1000),
25
+ ).resolves.toBeDefined()
26
+ },
27
+ )
28
+
29
+ walletTest('createInvoice with expiry', async ({ wallet }) => {
30
+ expect(wallet).toBeDefined()
31
+ expect(wallet.isOpen()).toBe(true)
32
+
33
+ const invoice = await wallet.lightning.createInvoice(100, 'test', 1000)
34
+ expect(invoice).toBeDefined()
35
+ expect(invoice).toMatchObject({
36
+ invoice: expect.any(String),
37
+ operation_id: expect.any(String),
38
+ })
39
+ })
40
+
41
+ walletTest(
42
+ 'listGateways should return a list of gateways',
43
+ async ({ wallet }) => {
44
+ expect(wallet).toBeDefined()
45
+ expect(wallet.isOpen()).toBe(true)
46
+
47
+ const counterBefore = wallet.testing.getRequestCounter()
48
+ const gateways = await wallet.lightning.listGateways()
49
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
50
+ expect(gateways).toBeDefined()
51
+ expect(gateways).toMatchObject(expect.any(Array))
52
+ },
53
+ )
54
+
55
+ walletTest(
56
+ 'updateGatewayCache should update the gateway cache',
57
+ async ({ wallet }) => {
58
+ expect(wallet).toBeDefined()
59
+ expect(wallet.isOpen()).toBe(true)
60
+
61
+ const counterBefore = wallet.testing.getRequestCounter()
62
+ await expect(wallet.lightning.updateGatewayCache()).resolves.toBeDefined()
63
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
64
+ },
65
+ )
66
+
67
+ walletTest('getGateway should return a gateway', async ({ wallet }) => {
68
+ expect(wallet).toBeDefined()
69
+ expect(wallet.isOpen()).toBe(true)
70
+
71
+ const counterBefore = wallet.testing.getRequestCounter()
72
+ const gateway = await wallet.lightning.getGateway()
73
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 1)
74
+ expect(gateway).toMatchObject({
75
+ api: expect.any(String),
76
+ fees: expect.any(Object),
77
+ gateway_id: expect.any(String),
78
+ gateway_redeem_key: expect.any(String),
79
+ lightning_alias: expect.any(String),
80
+ mint_channel_id: expect.any(Number),
81
+ node_pub_key: expect.any(String),
82
+ route_hints: expect.any(Array),
83
+ })
84
+ })
85
+
86
+ walletTest(
87
+ 'payInvoice should throw on insufficient funds',
88
+ async ({ wallet }) => {
89
+ expect(wallet).toBeDefined()
90
+ expect(wallet.isOpen()).toBe(true)
91
+
92
+ const invoice = await wallet.lightning.createInvoice(100, 'test')
93
+ expect(invoice).toBeDefined()
94
+ expect(invoice).toMatchObject({
95
+ invoice: expect.any(String),
96
+ operation_id: expect.any(String),
97
+ })
98
+
99
+ const counterBefore = wallet.testing.getRequestCounter()
100
+ // Insufficient funds
101
+ try {
102
+ await wallet.lightning.payInvoice(invoice.invoice)
103
+ expect.unreachable('Should throw error')
104
+ } catch (error) {
105
+ expect(error).toBeDefined()
106
+ }
107
+ // 3 requests were made, one for paying the invoice, one for refreshing the
108
+ // gateway cache, one for getting the gateway info
109
+ expect(wallet.testing.getRequestCounter()).toBe(counterBefore + 3)
110
+ },
111
+ )
112
+
113
+ walletTest(
114
+ 'payInvoice should pay a bolt11 invoice',
115
+ { timeout: 20_000 },
116
+ async ({ fundedWallet }) => {
117
+ expect(fundedWallet).toBeDefined()
118
+ expect(fundedWallet.isOpen()).toBe(true)
119
+ const initialBalance = await fundedWallet.balance.getBalance()
120
+ expect(initialBalance).toBeGreaterThan(0)
121
+ const externalInvoice = await fundedWallet.testing.createFaucetInvoice(1)
122
+ const gatewayInfo = await fundedWallet.testing.getFaucetGatewayInfo()
123
+ const payment = await fundedWallet.lightning.payInvoice(
124
+ externalInvoice,
125
+ gatewayInfo,
126
+ )
127
+ expect(payment).toMatchObject({
128
+ contract_id: expect.any(String),
129
+ fee: expect.any(Number),
130
+ payment_type: expect.any(Object),
131
+ })
132
+ const finalBalance = await fundedWallet.balance.getBalance()
133
+ expect(finalBalance).toBeLessThan(initialBalance)
134
+ },
135
+ )
136
+
137
+ walletTest(
138
+ 'createInvoiceTweaked should create a bolt11 invoice with a tweaked public key',
139
+ async ({ wallet }) => {
140
+ expect(wallet).toBeDefined()
141
+ expect(wallet.isOpen()).toBe(true)
142
+
143
+ // Make an ephemeral key pair
144
+ const { publicKey, secretKey } = keyPair()
145
+ const tweak = 1
146
+
147
+ // Create an invoice paying to the tweaked public key
148
+ const invoice = await wallet.lightning.createInvoiceTweaked(
149
+ 1000,
150
+ 'test tweaked',
151
+ publicKey,
152
+ tweak,
153
+ )
154
+ expect(invoice).toBeDefined()
155
+ expect(invoice).toMatchObject({
156
+ invoice: expect.any(String),
157
+ operation_id: expect.any(String),
158
+ })
159
+ },
160
+ )
161
+
162
+ walletTest(
163
+ 'scanReceivesForTweaks should return the operation id',
164
+ async ({ wallet }) => {
165
+ expect(wallet).toBeDefined()
166
+ expect(wallet.isOpen()).toBe(true)
167
+
168
+ // Make an ephemeral key pair
169
+ const { publicKey, secretKey } = keyPair()
170
+ const tweak = 1
171
+
172
+ const gatewayInfo = await wallet.testing.getFaucetGatewayInfo()
173
+
174
+ // Create an invoice paying to the tweaked public key
175
+ const invoice = await wallet.lightning.createInvoiceTweaked(
176
+ 1000,
177
+ 'test tweaked',
178
+ publicKey,
179
+ tweak,
180
+ undefined,
181
+ gatewayInfo,
182
+ )
183
+ await expect(
184
+ wallet.testing.payFaucetInvoice(invoice.invoice),
185
+ ).resolves.toBeDefined()
186
+
187
+ // Scan for the receive
188
+ const operationIds = await wallet.lightning.scanReceivesForTweaks(
189
+ secretKey,
190
+ [tweak],
191
+ {},
192
+ )
193
+ expect(operationIds).toBeDefined()
194
+ expect(operationIds).toHaveLength(1)
195
+
196
+ // Subscribe to claiming the receive
197
+ const subscription = await wallet.lightning.subscribeLnClaim(
198
+ operationIds[0],
199
+ (state) => {
200
+ expect(state).toBeDefined()
201
+ expect(state).toMatchObject({
202
+ state: 'claimed',
203
+ })
204
+ },
205
+ )
206
+ expect(subscription).toBeDefined()
207
+ },
208
+ )