@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.
- package/LICENSE +21 -0
- package/README.md +18 -0
- package/dist/dts/FedimintWallet.d.ts +51 -0
- package/dist/dts/FedimintWallet.d.ts.map +1 -0
- package/dist/dts/WalletDirector.d.ts +79 -0
- package/dist/dts/WalletDirector.d.ts.map +1 -0
- package/dist/dts/index.d.ts +5 -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 +13 -0
- package/dist/dts/services/FederationService.d.ts.map +1 -0
- package/dist/dts/services/LightningService.d.ts +48 -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 +13 -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/transport/TransportClient.d.ts +55 -0
- package/dist/dts/transport/TransportClient.d.ts.map +1 -0
- package/dist/dts/transport/index.d.ts +2 -0
- package/dist/dts/transport/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/transport.d.ts +2 -0
- package/dist/dts/types/transport.d.ts.map +1 -0
- package/dist/dts/types/utils.d.ts +21 -0
- package/dist/dts/types/utils.d.ts.map +1 -0
- package/dist/dts/types/wallet.d.ts +241 -0
- package/dist/dts/types/wallet.d.ts.map +1 -0
- package/dist/dts/utils/logger.d.ts +24 -0
- package/dist/dts/utils/logger.d.ts.map +1 -0
- package/dist/index.d.ts +578 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
- package/src/FedimintWallet.ts +119 -0
- package/src/WalletDirector.ts +119 -0
- package/src/index.ts +4 -0
- package/src/services/BalanceService.test.ts +26 -0
- package/src/services/BalanceService.ts +29 -0
- package/src/services/FederationService.test.ts +58 -0
- package/src/services/FederationService.ts +216 -0
- package/src/services/LightningService.test.ts +265 -0
- package/src/services/LightningService.ts +289 -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 +59 -0
- package/src/services/WalletService.ts +50 -0
- package/src/services/index.ts +6 -0
- package/src/transport/TransportClient.ts +254 -0
- package/src/transport/index.ts +1 -0
- package/src/types/index.ts +3 -0
- package/src/types/transport.ts +1 -0
- package/src/types/utils.ts +23 -0
- package/src/types/wallet.ts +298 -0
- 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 @@
|
|
|
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 }
|