@buildonspark/spark-sdk 0.3.7 → 0.3.9

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 (87) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bare/index.cjs +8088 -7723
  3. package/dist/bare/index.d.cts +350 -262
  4. package/dist/bare/index.d.ts +350 -262
  5. package/dist/bare/index.js +7968 -7608
  6. package/dist/{chunk-J2P3KTQP.js → chunk-4YFT7DAE.js} +1 -1
  7. package/dist/{chunk-XWLR6G5C.js → chunk-JLF6WJ7K.js} +1 -1
  8. package/dist/{chunk-UYTT3C6H.js → chunk-MFCM6GUD.js} +40 -213
  9. package/dist/{chunk-KDEVNW7C.js → chunk-O4C4HGQL.js} +3391 -3292
  10. package/dist/{chunk-SRPKOCG4.js → chunk-S55NZT4P.js} +8 -10
  11. package/dist/{chunk-P4HYYSMU.js → chunk-WRE2T22S.js} +1 -1
  12. package/dist/{chunk-IC4IUEOS.js → chunk-YEBEN7XD.js} +309 -38
  13. package/dist/{client-Bcb7TUIp.d.cts → client-BIqiUNy4.d.cts} +2 -2
  14. package/dist/{client-D9T58OY8.d.ts → client-BaQf-5gD.d.ts} +2 -2
  15. package/dist/debug.cjs +8068 -7704
  16. package/dist/debug.d.cts +25 -18
  17. package/dist/debug.d.ts +25 -18
  18. package/dist/debug.js +5 -5
  19. package/dist/graphql/objects/index.d.cts +3 -3
  20. package/dist/graphql/objects/index.d.ts +3 -3
  21. package/dist/index.cjs +6871 -6501
  22. package/dist/index.d.cts +9 -8
  23. package/dist/index.d.ts +9 -8
  24. package/dist/index.js +36 -24
  25. package/dist/index.node.cjs +7102 -6903
  26. package/dist/index.node.d.cts +9 -8
  27. package/dist/index.node.d.ts +9 -8
  28. package/dist/index.node.js +35 -23
  29. package/dist/{logging-zkr4UlOi.d.cts → logging-CXhvuqJJ.d.cts} +45 -35
  30. package/dist/{logging-JIaZZIbR.d.ts → logging-DDeMLsVN.d.ts} +45 -35
  31. package/dist/native/{chunk-X2QXUON7.js → chunk-AFP5QR4O.js} +11 -8
  32. package/dist/native/index.react-native.cjs +7054 -6677
  33. package/dist/native/index.react-native.d.cts +180 -92
  34. package/dist/native/index.react-native.d.ts +180 -92
  35. package/dist/native/index.react-native.js +6760 -6393
  36. package/dist/native/{wasm-GKEDPGTM.js → wasm-D4TI35NF.js} +1 -1
  37. package/dist/proto/spark.cjs +309 -38
  38. package/dist/proto/spark.d.cts +1 -1
  39. package/dist/proto/spark.d.ts +1 -1
  40. package/dist/proto/spark.js +5 -1
  41. package/dist/proto/spark_token.d.cts +1 -1
  42. package/dist/proto/spark_token.d.ts +1 -1
  43. package/dist/proto/spark_token.js +2 -2
  44. package/dist/{spark-WA_4wcBr.d.cts → spark-DOpheE8_.d.cts} +69 -7
  45. package/dist/{spark-WA_4wcBr.d.ts → spark-DOpheE8_.d.ts} +69 -7
  46. package/dist/{spark-wallet.browser-DC3jdQPW.d.cts → spark-wallet.browser-CbYo8A_U.d.cts} +8 -8
  47. package/dist/{spark-wallet.browser-BwYkkOBU.d.ts → spark-wallet.browser-Cz8c4kOW.d.ts} +8 -8
  48. package/dist/{spark-wallet.node-CR_zNxmy.d.cts → spark-wallet.node-4WQgWwB2.d.cts} +9 -31
  49. package/dist/{spark-wallet.node-C9d2W-Nb.d.ts → spark-wallet.node-CmIvxtcC.d.ts} +9 -31
  50. package/dist/tests/test-utils.cjs +7341 -7065
  51. package/dist/tests/test-utils.d.cts +7 -5
  52. package/dist/tests/test-utils.d.ts +7 -5
  53. package/dist/tests/test-utils.js +7 -7
  54. package/dist/{token-transactions-BZoJuvuE.d.ts → token-transactions-Bu023ztN.d.ts} +2 -2
  55. package/dist/{token-transactions-I_OFIoNH.d.cts → token-transactions-CV8QD3I7.d.cts} +2 -2
  56. package/dist/types/index.cjs +307 -38
  57. package/dist/types/index.d.cts +2 -2
  58. package/dist/types/index.d.ts +2 -2
  59. package/dist/types/index.js +2 -2
  60. package/dist/{spark-wallet-CE5PYiIb.d.ts → wallet-config-Bmk2eAn8.d.ts} +310 -287
  61. package/dist/{spark-wallet-BuFrUWeE.d.cts → wallet-config-DQw5llqA.d.cts} +310 -287
  62. package/package.json +3 -3
  63. package/src/proto/mock.ts +0 -264
  64. package/src/proto/spark.ts +433 -46
  65. package/src/services/config.ts +5 -0
  66. package/src/services/connection/connection.browser.ts +27 -19
  67. package/src/services/connection/connection.node.ts +79 -24
  68. package/src/services/connection/connection.ts +395 -233
  69. package/src/services/coop-exit.ts +26 -107
  70. package/src/services/deposit.ts +12 -48
  71. package/src/services/lightning.ts +30 -4
  72. package/src/services/signing.ts +187 -37
  73. package/src/services/transfer.ts +553 -723
  74. package/src/services/wallet-config.ts +6 -0
  75. package/src/spark-wallet/proto-descriptors.ts +1 -1
  76. package/src/spark-wallet/spark-wallet.ts +132 -313
  77. package/src/spark-wallet/types.ts +2 -2
  78. package/src/spark_descriptors.pb +0 -0
  79. package/src/tests/connection.test.ts +537 -0
  80. package/src/tests/integration/connection.test.ts +39 -0
  81. package/src/tests/integration/lightning.test.ts +32 -16
  82. package/src/tests/integration/static_deposit.test.ts +13 -11
  83. package/src/tests/integration/transfer.test.ts +13 -1
  84. package/src/tests/isHermeticTest.ts +1 -1
  85. package/src/tests/utils/test-faucet.ts +53 -20
  86. package/src/utils/htlc-transactions.ts +224 -0
  87. package/src/utils/transaction.ts +285 -248
@@ -1,13 +1,9 @@
1
- import { isBare, isError } from "@lightsparkdev/core";
1
+ import { isError } from "@lightsparkdev/core";
2
2
  import { sha256 } from "@noble/hashes/sha2";
3
- import type { Channel, ClientFactory } from "nice-grpc";
4
- import { retryMiddleware } from "nice-grpc-client-middleware-retry";
3
+ import type { Channel } from "nice-grpc";
5
4
  import { ClientMiddlewareCall, Metadata } from "nice-grpc-common";
6
- import {
7
- type Channel as ChannelWeb,
8
- type ClientFactory as ClientFactoryWeb,
9
- } from "nice-grpc-web";
10
- import { clientEnv } from "../../constants.js";
5
+ import type { ClientMiddleware } from "nice-grpc-common";
6
+ import { type Channel as ChannelWeb } from "nice-grpc-web";
11
7
  import { AuthenticationError, NetworkError } from "../../errors/types.js";
12
8
  import { MockServiceClient, MockServiceDefinition } from "../../proto/mock.js";
13
9
  import {
@@ -23,44 +19,178 @@ import {
23
19
  SparkTokenServiceClient,
24
20
  SparkTokenServiceDefinition,
25
21
  } from "../../proto/spark_token.js";
26
- import { RetryOptions, SparkCallOptions } from "../../types/grpc.js";
22
+ import { SparkCallOptions } from "../../types/grpc.js";
27
23
  import { WalletConfigService } from "../config.js";
24
+ import { SparkSDKError } from "../../errors/base.js";
25
+ import type { RetryOptions } from "nice-grpc-client-middleware-retry";
26
+
27
+ // Module-level types used by shared caches
28
+ type ChannelKey = string;
29
+ type BrowserOrNodeJSChannel = Channel | ChannelWeb;
30
+
31
+ type TokenKey = string;
28
32
 
29
33
  type SparkAuthnServiceClientWithClose = SparkAuthnServiceClient & {
30
34
  close?: () => void;
31
35
  };
32
36
 
33
- export class ConnectionManager {
34
- private config: WalletConfigService;
35
- protected clients: Map<
36
- string,
37
- {
38
- client: SparkServiceClient & { close?: () => void };
39
- authToken: string;
40
- }
37
+ type ClientWithClose<T> = T & {
38
+ close?: () => void;
39
+ };
40
+
41
+ export type SparkClientType = "spark" | "stream" | "tokens";
42
+
43
+ /* From nice-grpc/lib/client/channel.d.ts: The address of the server,
44
+ * in the form `protocol://host:port`, where `protocol` is one of `http`
45
+ * or `https`. If the port is not specified, it will be inferred from the protocol. */
46
+ type Address = string;
47
+
48
+ export abstract class ConnectionManager {
49
+ // Static caches shared across all instances
50
+ private static channelCache: Map<
51
+ ChannelKey,
52
+ { channel: BrowserOrNodeJSChannel; refCount: number }
41
53
  > = new Map();
42
- private tokenClients: Map<
43
- string,
44
- {
45
- client: SparkTokenServiceClient & { close?: () => void };
46
- authToken: string;
47
- }
54
+ private static channelInflight: Map<
55
+ ChannelKey,
56
+ Promise<BrowserOrNodeJSChannel>
48
57
  > = new Map();
58
+ private static authTokenCache: Map<TokenKey, string> = new Map();
59
+ private static authInflight: Map<TokenKey, Promise<string>> = new Map();
49
60
 
50
- // We are going to .unref() the underlying channels for stream clients
51
- // to prevent the stream from keeping the process alive
52
- // Using a different map to avoid unforeseen problems with unary calls
53
- private streamClients: Map<
54
- string,
55
- {
56
- client: SparkServiceClient & { close?: () => void };
57
- authToken: string;
58
- channel: Channel | ChannelWeb;
61
+ protected makeChannelKey(address: Address, stream?: boolean): ChannelKey {
62
+ return [address, stream ? "stream" : "unary"].join("|");
63
+ }
64
+
65
+ protected static async acquireChannel<T extends BrowserOrNodeJSChannel>(
66
+ key: ChannelKey,
67
+ create: () => Promise<T>,
68
+ ): Promise<T> {
69
+ const existing = ConnectionManager.channelCache.get(key);
70
+ if (existing) {
71
+ existing.refCount++;
72
+ return existing.channel as T;
59
73
  }
60
- > = new Map();
74
+ let channelPromise = ConnectionManager.channelInflight.get(key);
75
+ if (!channelPromise) {
76
+ channelPromise = (async () => {
77
+ const ch = (await create()) as BrowserOrNodeJSChannel;
78
+ ConnectionManager.channelCache.set(key, { channel: ch, refCount: 1 });
79
+ return ch as BrowserOrNodeJSChannel;
80
+ })();
81
+ ConnectionManager.channelInflight.set(key, channelPromise);
82
+ }
83
+ try {
84
+ return (await channelPromise) as T;
85
+ } finally {
86
+ ConnectionManager.channelInflight.delete(key);
87
+ }
88
+ }
89
+
90
+ protected static releaseChannel(key: ChannelKey) {
91
+ const entry = ConnectionManager.channelCache.get(key);
92
+ if (!entry) return;
93
+ entry.refCount--;
94
+ if (entry.refCount <= 0) {
95
+ const ch = entry.channel;
96
+ if ("close" in ch && typeof ch.close === "function") {
97
+ try {
98
+ ch.close();
99
+ } catch {}
100
+ }
101
+ ConnectionManager.channelCache.delete(key);
102
+ }
103
+ }
104
+
105
+ private static makeAuthTokenKey(
106
+ address: Address,
107
+ identityHex: string,
108
+ ): TokenKey {
109
+ return `${address}|${identityHex}`;
110
+ }
111
+
112
+ private static getCachedAuthToken(address: Address, identityHex: string) {
113
+ return ConnectionManager.authTokenCache.get(
114
+ ConnectionManager.makeAuthTokenKey(address, identityHex),
115
+ );
116
+ }
117
+
118
+ private static setCachedAuthToken(
119
+ address: Address,
120
+ identityHex: string,
121
+ authToken: string,
122
+ ) {
123
+ ConnectionManager.authTokenCache.set(
124
+ ConnectionManager.makeAuthTokenKey(address, identityHex),
125
+ authToken,
126
+ );
127
+ }
61
128
 
62
- // Tracks in-flight authenticate() promises so concurrent callers share one
63
- private authPromises: Map<string, Promise<string>> = new Map();
129
+ private static invalidateCachedAuthToken(
130
+ address: Address,
131
+ identityHex: string,
132
+ ) {
133
+ ConnectionManager.authTokenCache.delete(
134
+ ConnectionManager.makeAuthTokenKey(address, identityHex),
135
+ );
136
+ }
137
+
138
+ private static async getOrCreateAuthToken(
139
+ address: Address,
140
+ identityHex: string,
141
+ authenticate: () => Promise<string>,
142
+ ): Promise<string> {
143
+ const cached = ConnectionManager.getCachedAuthToken(address, identityHex);
144
+ if (cached) {
145
+ return cached;
146
+ }
147
+
148
+ const tokenKey = ConnectionManager.makeAuthTokenKey(address, identityHex);
149
+ let authPromise = ConnectionManager.authInflight.get(tokenKey);
150
+ if (!authPromise) {
151
+ authPromise = (async () => {
152
+ const authToken = await authenticate();
153
+ ConnectionManager.setCachedAuthToken(address, identityHex, authToken);
154
+ return authToken;
155
+ })();
156
+ ConnectionManager.authInflight.set(tokenKey, authPromise);
157
+ }
158
+ try {
159
+ return await authPromise;
160
+ } finally {
161
+ ConnectionManager.authInflight.delete(tokenKey);
162
+ }
163
+ }
164
+
165
+ protected abstract createChannelWithTLS(
166
+ address: Address,
167
+ isStreamClientType?: boolean,
168
+ ): Promise<Channel | ChannelWeb>;
169
+
170
+ protected abstract createGrpcClient<T>(
171
+ definition:
172
+ | SparkAuthnServiceDefinition
173
+ | SparkServiceDefinition
174
+ | SparkTokenServiceDefinition,
175
+ channel: Channel | ChannelWeb,
176
+ withRetries: boolean,
177
+ middleware?: ClientMiddleware<RetryOptions, {}>,
178
+ channelKey?: ChannelKey,
179
+ ): Promise<T & { close?: () => void }>;
180
+
181
+ private config: WalletConfigService;
182
+
183
+ // Note clientsByType is a per instance cache whereas channelCache is static and shared by all instances
184
+ private clientsByType: Map<
185
+ SparkClientType,
186
+ Map<Address, { client: ClientWithClose<unknown>; channelKey: ChannelKey }>
187
+ > = new Map([
188
+ ["spark", new Map()],
189
+ ["stream", new Map()],
190
+ ["tokens", new Map()],
191
+ ]);
192
+
193
+ private identityPublicKeyHex?: string;
64
194
 
65
195
  constructor(config: WalletConfigService) {
66
196
  this.config = config;
@@ -76,259 +206,268 @@ export class ConnectionManager {
76
206
  }
77
207
 
78
208
  public async closeConnections() {
209
+ const sparkMap = this.clientsByType.get("spark");
210
+ if (!sparkMap) return;
79
211
  await Promise.all(
80
- Array.from(this.clients.values()).map((client) =>
81
- client.client.close?.(),
82
- ),
212
+ Array.from(sparkMap.values()).map((entry) => entry.client.close?.()),
83
213
  );
84
- this.clients.clear();
214
+ sparkMap.clear();
85
215
  }
86
216
 
87
- protected createChannelWithTLS(
88
- address: string,
89
- certPath?: string,
90
- ): Promise<Channel | ChannelWeb> {
91
- throw new Error("createChannelWithTLS: Not implemented");
217
+ private getDefinitionForClientType(
218
+ type: SparkClientType,
219
+ ): SparkServiceDefinition | SparkTokenServiceDefinition {
220
+ return type === "tokens"
221
+ ? SparkTokenServiceDefinition
222
+ : SparkServiceDefinition;
92
223
  }
93
224
 
94
- async createSparkStreamClient(
95
- address: string,
96
- certPath?: string,
97
- ): Promise<SparkServiceClient & { close?: () => void }> {
98
- if (this.streamClients.has(address)) {
99
- return this.streamClients.get(address)!.client;
100
- }
101
- const authToken = await this.authenticate(address);
102
- const channel = await this.createChannelWithTLS(address, certPath);
103
-
104
- if (!channel) {
105
- throw new NetworkError("Failed to create channel", {
106
- url: address,
107
- operation: "createChannel",
108
- errorCount: 1,
109
- errors: "Channel is undefined",
110
- });
225
+ protected static isStreamClientType(type: SparkClientType) {
226
+ return type === "stream";
227
+ }
228
+
229
+ private getAddressToClientMap(type: SparkClientType) {
230
+ return this.clientsByType.get(type)!;
231
+ }
232
+
233
+ private async getOrCreateClientInternal<T>(
234
+ type: SparkClientType,
235
+ address: Address,
236
+ ): Promise<ClientWithClose<T>> {
237
+ const addressToClientMap = this.getAddressToClientMap(type);
238
+ const existing = addressToClientMap.get(address);
239
+ if (existing) {
240
+ return existing.client as ClientWithClose<T>;
111
241
  }
112
242
 
113
- const middleware = this.createMiddleware(address, authToken);
114
- const client = await this.createGrpcClient<SparkServiceClient>(
115
- SparkServiceDefinition,
243
+ await this.authenticate(address);
244
+ const isStreamClientType = ConnectionManager.isStreamClientType(type);
245
+ const key = this.makeChannelKey(address, isStreamClientType);
246
+ const channel = await ConnectionManager.acquireChannel(key, () =>
247
+ this.createChannelWithTLS(address, isStreamClientType),
248
+ );
249
+ const middleware = this.createMiddleware(address);
250
+ const def = this.getDefinitionForClientType(type);
251
+ const client = (await this.createGrpcClient<T>(
252
+ def,
116
253
  channel,
117
254
  true,
118
255
  middleware,
119
- );
256
+ key,
257
+ )) as ClientWithClose<T>;
120
258
 
121
- this.streamClients.set(address, { client, authToken, channel });
259
+ addressToClientMap.set(address, { client, channelKey: key });
122
260
  return client;
123
261
  }
124
262
 
125
- async createSparkClient(
263
+ async createSparkStreamClient(
126
264
  address: string,
127
- certPath?: string,
128
265
  ): Promise<SparkServiceClient & { close?: () => void }> {
129
- if (this.clients.has(address)) {
130
- return this.clients.get(address)!.client;
131
- }
132
- const authToken = await this.authenticate(address);
133
- const channel = await this.createChannelWithTLS(address, certPath);
134
-
135
- const middleware = this.createMiddleware(address, authToken);
136
- const client = await this.createGrpcClient<SparkServiceClient>(
137
- SparkServiceDefinition,
138
- channel,
139
- true,
140
- middleware,
266
+ return this.getOrCreateClientInternal<SparkServiceClient>(
267
+ "stream",
268
+ address,
141
269
  );
270
+ }
142
271
 
143
- this.clients.set(address, { client, authToken });
144
- return client;
272
+ async createSparkClient(
273
+ address: string,
274
+ ): Promise<SparkServiceClient & { close?: () => void }> {
275
+ return this.getOrCreateClientInternal<SparkServiceClient>("spark", address);
145
276
  }
146
277
 
147
278
  async createSparkTokenClient(
148
279
  address: string,
149
- certPath?: string,
150
280
  ): Promise<SparkTokenServiceClient & { close?: () => void }> {
151
- if (this.tokenClients.has(address)) {
152
- return this.tokenClients.get(address)!.client;
153
- }
154
- const authToken = await this.authenticate(address);
155
- const channel = await this.createChannelWithTLS(address, certPath);
156
-
157
- const middleware = this.createMiddleware(address, authToken);
158
- const tokenClient = await this.createGrpcClient<SparkTokenServiceClient>(
159
- SparkTokenServiceDefinition,
160
- channel,
161
- true,
162
- middleware,
281
+ return this.getOrCreateClientInternal<SparkTokenServiceClient>(
282
+ "tokens",
283
+ address,
163
284
  );
285
+ }
164
286
 
165
- this.tokenClients.set(address, { client: tokenClient, authToken });
166
- return tokenClient;
287
+ async getChannelForClient(clientType: SparkClientType, address: Address) {
288
+ const key = this.getAddressToClientMap(clientType).get(address)?.channelKey;
289
+ if (!key) return undefined;
290
+ return ConnectionManager.channelCache.get(key)?.channel;
167
291
  }
168
292
 
169
- async getStreamChannel(address: string) {
170
- return this.streamClients.get(address)?.channel;
293
+ private async getIdentityPublicKeyHex(): Promise<string> {
294
+ if (this.identityPublicKeyHex) return this.identityPublicKeyHex;
295
+ const identityPublicKey = await this.config.signer.getIdentityPublicKey();
296
+ const hex = Array.from(identityPublicKey)
297
+ .map((b) => b.toString(16).padStart(2, "0"))
298
+ .join("");
299
+ this.identityPublicKeyHex = hex;
300
+ return hex;
171
301
  }
172
302
 
173
- private async authenticate(address: string, certPath?: string) {
174
- const existing = this.authPromises.get(address);
175
- if (existing) {
176
- return existing;
177
- }
303
+ protected async authenticate(address: string) {
304
+ const identityHex = await this.getIdentityPublicKeyHex();
305
+ return ConnectionManager.getOrCreateAuthToken(
306
+ address,
307
+ identityHex,
308
+ async () => {
309
+ const MAX_ATTEMPTS = 8;
310
+ let lastError: Error | undefined;
311
+
312
+ const identityPublicKey =
313
+ await this.config.signer.getIdentityPublicKey();
314
+ const sparkAuthnClient =
315
+ await this.createSparkAuthnGrpcConnection(address);
316
+
317
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
318
+ try {
319
+ const challengeResp = await sparkAuthnClient.get_challenge({
320
+ publicKey: identityPublicKey,
321
+ });
322
+ const protectedChallenge = challengeResp.protectedChallenge;
323
+ const challenge = protectedChallenge?.challenge;
324
+
325
+ if (!challenge) {
326
+ throw new AuthenticationError("Invalid challenge response", {
327
+ endpoint: "get_challenge",
328
+ reason: "Missing challenge in response",
329
+ });
330
+ }
178
331
 
179
- const authPromise = (async () => {
180
- const MAX_ATTEMPTS = 3;
181
- let lastError: Error | undefined;
332
+ const challengeBytes = Challenge.encode(challenge).finish();
333
+ const hash = sha256(challengeBytes);
182
334
 
183
- /* React Native can cause some outgoing requests to be paused which can result
184
- in challenges expiring, so we'll retry any authentication failures: */
185
- for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
186
- let sparkAuthnClient: SparkAuthnServiceClientWithClose | undefined;
187
- try {
188
- const identityPublicKey =
189
- await this.config.signer.getIdentityPublicKey();
190
- sparkAuthnClient = await this.createSparkAuthnGrpcConnection(
191
- address,
192
- certPath,
193
- );
194
-
195
- const challengeResp = await sparkAuthnClient.get_challenge({
196
- publicKey: identityPublicKey,
197
- });
198
-
199
- if (!challengeResp.protectedChallenge?.challenge) {
200
- throw new AuthenticationError("Invalid challenge response", {
201
- endpoint: "get_challenge",
202
- reason: "Missing challenge in response",
335
+ const derSignatureBytes =
336
+ await this.config.signer.signMessageWithIdentityKey(hash);
337
+
338
+ const verifyResp = await sparkAuthnClient.verify_challenge({
339
+ protectedChallenge,
340
+ signature: derSignatureBytes,
341
+ publicKey: identityPublicKey,
203
342
  });
204
- }
205
343
 
206
- const challengeBytes = Challenge.encode(
207
- challengeResp.protectedChallenge.challenge,
208
- ).finish();
209
- const hash = sha256(challengeBytes);
210
-
211
- const derSignatureBytes =
212
- await this.config.signer.signMessageWithIdentityKey(hash);
213
-
214
- const verifyResp = await sparkAuthnClient.verify_challenge({
215
- protectedChallenge: challengeResp.protectedChallenge,
216
- signature: derSignatureBytes,
217
- publicKey: identityPublicKey,
218
- });
219
-
220
- sparkAuthnClient.close?.();
221
- return verifyResp.sessionToken;
222
- } catch (error: unknown) {
223
- if (isError(error)) {
224
- sparkAuthnClient?.close?.();
225
-
226
- if (error.message.includes("challenge expired")) {
227
- console.warn(
228
- `Authentication attempt ${attempt + 1} failed due to expired challenge, retrying...`,
229
- );
230
- lastError = error;
231
- continue;
344
+ if (sparkAuthnClient.close) {
345
+ sparkAuthnClient.close();
232
346
  }
233
-
234
- if (
235
- error.message.includes("UNAVAILABLE: No connection established.")
236
- ) {
237
- console.warn(
238
- `Authentication attempt ${attempt + 1} failed due to unavailable status, retrying...`,
347
+ return verifyResp.sessionToken;
348
+ } catch (error: unknown) {
349
+ if (isError(error)) {
350
+ if (sparkAuthnClient.close) {
351
+ sparkAuthnClient.close();
352
+ }
353
+
354
+ if (isExpiredChallengeError(error, attempt)) {
355
+ lastError = error;
356
+ continue;
357
+ }
358
+
359
+ if (isConnectionError(error, attempt)) {
360
+ lastError = error;
361
+ await new Promise((resolve) => setTimeout(resolve, 250));
362
+ continue;
363
+ }
364
+
365
+ throw new AuthenticationError(
366
+ "Authentication failed",
367
+ { endpoint: "authenticate", reason: error.message },
368
+ error,
369
+ );
370
+ } else {
371
+ lastError = new Error(
372
+ `Unknown error during authentication: ${String(error)}`,
239
373
  );
240
- lastError = error;
241
- continue;
242
374
  }
243
-
244
- throw new AuthenticationError(
245
- "Authentication failed",
246
- {
247
- endpoint: "authenticate",
248
- reason: error.message,
249
- },
250
- error,
251
- );
252
- } else {
253
- lastError = new Error(
254
- `Unknown error during authentication: ${String(error)}`,
255
- );
256
375
  }
257
376
  }
258
- }
259
-
260
- throw new AuthenticationError(
261
- "Authentication failed after retrying expired challenges",
262
- {
263
- endpoint: "authenticate",
264
- reason: lastError?.message ?? "Unknown error",
265
- },
266
- lastError,
267
- );
268
- })();
269
-
270
- this.authPromises.set(address, authPromise);
271
377
 
272
- try {
273
- return await authPromise;
274
- } finally {
275
- this.authPromises.delete(address);
276
- }
378
+ throw new AuthenticationError(
379
+ "Authentication failed after retrying expired challenges",
380
+ {
381
+ endpoint: "authenticate",
382
+ reason: lastError?.message ?? "Unknown error",
383
+ },
384
+ lastError,
385
+ );
386
+ },
387
+ );
277
388
  }
278
389
 
279
390
  private async createSparkAuthnGrpcConnection(
280
391
  address: string,
281
- certPath?: string,
282
392
  ): Promise<SparkAuthnServiceClientWithClose> {
283
- const channel = await this.createChannelWithTLS(address, certPath);
284
- const authnMiddleware = this.createAuthnMiddleware();
285
- return this.createGrpcClient<SparkAuthnServiceClient>(
286
- SparkAuthnServiceDefinition,
287
- channel,
288
- false,
289
- authnMiddleware,
290
- );
393
+ try {
394
+ const key = this.makeChannelKey(address, false);
395
+ const channel = await ConnectionManager.acquireChannel(key, () =>
396
+ this.createChannelWithTLS(address, false),
397
+ );
398
+ const authnMiddleware = this.createAuthnMiddleware();
399
+ const client = await this.createGrpcClient<SparkAuthnServiceClient>(
400
+ SparkAuthnServiceDefinition,
401
+ channel,
402
+ false,
403
+ authnMiddleware,
404
+ key,
405
+ );
406
+ return client;
407
+ } catch (error) {
408
+ throw new SparkSDKError(
409
+ "Failed to create Spark Authn gRPC connection",
410
+ {},
411
+ error instanceof Error ? error : new Error(String(error)),
412
+ );
413
+ }
291
414
  }
292
415
 
293
416
  protected createAuthnMiddleware() {
294
- return async function* (
417
+ return async function* <Req, Res>(
295
418
  this: ConnectionManager,
296
- call: ClientMiddlewareCall<any, any>,
419
+ call: ClientMiddlewareCall<Req, Res>,
297
420
  options: SparkCallOptions,
298
421
  ) {
299
422
  return yield* call.next(call.request, options);
300
- }.bind(this);
423
+ }.bind(this) as <Req, Res>(
424
+ call: ClientMiddlewareCall<Req, Res>,
425
+ options: SparkCallOptions,
426
+ ) => AsyncGenerator<Res, Res | void, undefined>;
301
427
  }
302
428
 
303
- protected createMiddleware(
304
- address: string,
305
- authToken: string,
306
- ):
307
- | ((
308
- call: ClientMiddlewareCall<any, any>,
309
- options: SparkCallOptions,
310
- ) => AsyncGenerator<any, any, undefined>)
311
- | undefined {
312
- return undefined;
429
+ protected createMiddleware(address: Address) {
430
+ return async function* <Req, Res>(
431
+ this: ConnectionManager,
432
+ call: ClientMiddlewareCall<Req, Res>,
433
+ options: SparkCallOptions,
434
+ ) {
435
+ const metadata = Metadata(options.metadata);
436
+ const authToken = await this.authenticate(address);
437
+ try {
438
+ return yield* call.next(call.request as Req, {
439
+ ...options,
440
+ metadata: metadata.set("Authorization", `Bearer ${authToken}`),
441
+ });
442
+ } catch (error: unknown) {
443
+ return yield* this.handleMiddlewareError(
444
+ error,
445
+ address,
446
+ call,
447
+ metadata,
448
+ options,
449
+ );
450
+ }
451
+ }.bind(this) as <Req, Res>(
452
+ call: ClientMiddlewareCall<Req, Res>,
453
+ options: SparkCallOptions,
454
+ ) => AsyncGenerator<Res, Res | void, undefined>;
313
455
  }
314
456
 
315
- protected async *handleMiddlewareError(
457
+ protected async *handleMiddlewareError<Req, Res>(
316
458
  error: unknown,
317
459
  address: string,
318
- call: ClientMiddlewareCall<any, any>,
460
+ call: ClientMiddlewareCall<Req, Res>,
319
461
  metadata: Metadata,
320
462
  options: SparkCallOptions,
321
463
  ) {
322
464
  if (isError(error)) {
323
465
  if (error.message.includes("token has expired")) {
466
+ const identityHex = await this.getIdentityPublicKeyHex();
467
+ ConnectionManager.invalidateCachedAuthToken(address, identityHex);
324
468
  const newAuthToken = await this.authenticate(address);
325
- const clientData = this.clients.get(address);
326
- if (!clientData) {
327
- throw new Error(`No client found for address: ${address}`);
328
- }
329
- clientData.authToken = newAuthToken;
330
469
 
331
- return yield* call.next(call.request, {
470
+ return yield* call.next(call.request as Req, {
332
471
  ...options,
333
472
  metadata: metadata.set("Authorization", `Bearer ${newAuthToken}`),
334
473
  });
@@ -338,15 +477,38 @@ export class ConnectionManager {
338
477
  throw error;
339
478
  }
340
479
 
341
- protected async createGrpcClient<T>(
342
- defintion:
343
- | SparkAuthnServiceDefinition
344
- | SparkServiceDefinition
345
- | SparkTokenServiceDefinition,
346
- channel: Channel | ChannelWeb,
347
- withRetries: boolean,
348
- middleware?: any,
349
- ): Promise<T & { close?: () => void }> {
350
- throw new Error("createGrpcClient: Not implemented");
480
+ async subscribeToEvents(address: string, signal: AbortSignal) {
481
+ const sparkStreamClient = await this.createSparkStreamClient(address);
482
+ const identityPublicKey = await this.config.signer.getIdentityPublicKey();
483
+ const stream = sparkStreamClient.subscribe_to_events(
484
+ { identityPublicKey },
485
+ { signal },
486
+ );
487
+ return stream;
488
+ }
489
+ }
490
+
491
+ function isExpiredChallengeError(error: Error, attempt: number) {
492
+ const isExpired = error.message.includes("challenge expired");
493
+ if (isExpired) {
494
+ console.warn(
495
+ `Authentication attempt ${attempt + 1} failed due to expired challenge, retrying...`,
496
+ );
497
+ }
498
+ return isExpired;
499
+ }
500
+
501
+ function isConnectionError(error: Error, attempt: number) {
502
+ const isConnectionError =
503
+ error.message.includes("RST_STREAM") ||
504
+ error.message.includes("INTERNAL") ||
505
+ error.message.includes("Internal server error") ||
506
+ error.message.includes("unavailable") ||
507
+ error.message.includes("UNAVAILABLE") ||
508
+ error.message.includes("UNKNOWN") ||
509
+ error.message.includes("Received HTTP status code");
510
+ if (isConnectionError) {
511
+ console.warn(`Connection error: ${error.message}, retrying...`);
351
512
  }
513
+ return isConnectionError;
352
514
  }