@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.
- package/CHANGELOG.md +15 -0
- package/dist/bare/index.cjs +8088 -7723
- package/dist/bare/index.d.cts +350 -262
- package/dist/bare/index.d.ts +350 -262
- package/dist/bare/index.js +7968 -7608
- package/dist/{chunk-J2P3KTQP.js → chunk-4YFT7DAE.js} +1 -1
- package/dist/{chunk-XWLR6G5C.js → chunk-JLF6WJ7K.js} +1 -1
- package/dist/{chunk-UYTT3C6H.js → chunk-MFCM6GUD.js} +40 -213
- package/dist/{chunk-KDEVNW7C.js → chunk-O4C4HGQL.js} +3391 -3292
- package/dist/{chunk-SRPKOCG4.js → chunk-S55NZT4P.js} +8 -10
- package/dist/{chunk-P4HYYSMU.js → chunk-WRE2T22S.js} +1 -1
- package/dist/{chunk-IC4IUEOS.js → chunk-YEBEN7XD.js} +309 -38
- package/dist/{client-Bcb7TUIp.d.cts → client-BIqiUNy4.d.cts} +2 -2
- package/dist/{client-D9T58OY8.d.ts → client-BaQf-5gD.d.ts} +2 -2
- package/dist/debug.cjs +8068 -7704
- package/dist/debug.d.cts +25 -18
- package/dist/debug.d.ts +25 -18
- package/dist/debug.js +5 -5
- package/dist/graphql/objects/index.d.cts +3 -3
- package/dist/graphql/objects/index.d.ts +3 -3
- package/dist/index.cjs +6871 -6501
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +36 -24
- package/dist/index.node.cjs +7102 -6903
- package/dist/index.node.d.cts +9 -8
- package/dist/index.node.d.ts +9 -8
- package/dist/index.node.js +35 -23
- package/dist/{logging-zkr4UlOi.d.cts → logging-CXhvuqJJ.d.cts} +45 -35
- package/dist/{logging-JIaZZIbR.d.ts → logging-DDeMLsVN.d.ts} +45 -35
- package/dist/native/{chunk-X2QXUON7.js → chunk-AFP5QR4O.js} +11 -8
- package/dist/native/index.react-native.cjs +7054 -6677
- package/dist/native/index.react-native.d.cts +180 -92
- package/dist/native/index.react-native.d.ts +180 -92
- package/dist/native/index.react-native.js +6760 -6393
- package/dist/native/{wasm-GKEDPGTM.js → wasm-D4TI35NF.js} +1 -1
- package/dist/proto/spark.cjs +309 -38
- package/dist/proto/spark.d.cts +1 -1
- package/dist/proto/spark.d.ts +1 -1
- package/dist/proto/spark.js +5 -1
- package/dist/proto/spark_token.d.cts +1 -1
- package/dist/proto/spark_token.d.ts +1 -1
- package/dist/proto/spark_token.js +2 -2
- package/dist/{spark-WA_4wcBr.d.cts → spark-DOpheE8_.d.cts} +69 -7
- package/dist/{spark-WA_4wcBr.d.ts → spark-DOpheE8_.d.ts} +69 -7
- package/dist/{spark-wallet.browser-DC3jdQPW.d.cts → spark-wallet.browser-CbYo8A_U.d.cts} +8 -8
- package/dist/{spark-wallet.browser-BwYkkOBU.d.ts → spark-wallet.browser-Cz8c4kOW.d.ts} +8 -8
- package/dist/{spark-wallet.node-CR_zNxmy.d.cts → spark-wallet.node-4WQgWwB2.d.cts} +9 -31
- package/dist/{spark-wallet.node-C9d2W-Nb.d.ts → spark-wallet.node-CmIvxtcC.d.ts} +9 -31
- package/dist/tests/test-utils.cjs +7341 -7065
- package/dist/tests/test-utils.d.cts +7 -5
- package/dist/tests/test-utils.d.ts +7 -5
- package/dist/tests/test-utils.js +7 -7
- package/dist/{token-transactions-BZoJuvuE.d.ts → token-transactions-Bu023ztN.d.ts} +2 -2
- package/dist/{token-transactions-I_OFIoNH.d.cts → token-transactions-CV8QD3I7.d.cts} +2 -2
- package/dist/types/index.cjs +307 -38
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +2 -2
- package/dist/{spark-wallet-CE5PYiIb.d.ts → wallet-config-Bmk2eAn8.d.ts} +310 -287
- package/dist/{spark-wallet-BuFrUWeE.d.cts → wallet-config-DQw5llqA.d.cts} +310 -287
- package/package.json +3 -3
- package/src/proto/mock.ts +0 -264
- package/src/proto/spark.ts +433 -46
- package/src/services/config.ts +5 -0
- package/src/services/connection/connection.browser.ts +27 -19
- package/src/services/connection/connection.node.ts +79 -24
- package/src/services/connection/connection.ts +395 -233
- package/src/services/coop-exit.ts +26 -107
- package/src/services/deposit.ts +12 -48
- package/src/services/lightning.ts +30 -4
- package/src/services/signing.ts +187 -37
- package/src/services/transfer.ts +553 -723
- package/src/services/wallet-config.ts +6 -0
- package/src/spark-wallet/proto-descriptors.ts +1 -1
- package/src/spark-wallet/spark-wallet.ts +132 -313
- package/src/spark-wallet/types.ts +2 -2
- package/src/spark_descriptors.pb +0 -0
- package/src/tests/connection.test.ts +537 -0
- package/src/tests/integration/connection.test.ts +39 -0
- package/src/tests/integration/lightning.test.ts +32 -16
- package/src/tests/integration/static_deposit.test.ts +13 -11
- package/src/tests/integration/transfer.test.ts +13 -1
- package/src/tests/isHermeticTest.ts +1 -1
- package/src/tests/utils/test-faucet.ts +53 -20
- package/src/utils/htlc-transactions.ts +224 -0
- package/src/utils/transaction.ts +285 -248
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isError } from "@lightsparkdev/core";
|
|
2
2
|
import { sha256 } from "@noble/hashes/sha2";
|
|
3
|
-
import type { Channel
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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(
|
|
81
|
-
client.client.close?.(),
|
|
82
|
-
),
|
|
212
|
+
Array.from(sparkMap.values()).map((entry) => entry.client.close?.()),
|
|
83
213
|
);
|
|
84
|
-
|
|
214
|
+
sparkMap.clear();
|
|
85
215
|
}
|
|
86
216
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
217
|
+
private getDefinitionForClientType(
|
|
218
|
+
type: SparkClientType,
|
|
219
|
+
): SparkServiceDefinition | SparkTokenServiceDefinition {
|
|
220
|
+
return type === "tokens"
|
|
221
|
+
? SparkTokenServiceDefinition
|
|
222
|
+
: SparkServiceDefinition;
|
|
92
223
|
}
|
|
93
224
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
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
|
-
|
|
259
|
+
addressToClientMap.set(address, { client, channelKey: key });
|
|
122
260
|
return client;
|
|
123
261
|
}
|
|
124
262
|
|
|
125
|
-
async
|
|
263
|
+
async createSparkStreamClient(
|
|
126
264
|
address: string,
|
|
127
|
-
certPath?: string,
|
|
128
265
|
): Promise<SparkServiceClient & { close?: () => void }> {
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
|
170
|
-
return this.
|
|
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
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
let lastError: Error | undefined;
|
|
332
|
+
const challengeBytes = Challenge.encode(challenge).finish();
|
|
333
|
+
const hash = sha256(challengeBytes);
|
|
182
334
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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<
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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<
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
}
|