@buildonspark/spark-sdk 0.0.15 → 0.0.16

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 (131) hide show
  1. package/dist/services/wallet-config.d.ts +1 -0
  2. package/dist/services/wallet-config.js +1 -0
  3. package/dist/services/wallet-config.js.map +1 -1
  4. package/dist/spark-sdk.d.ts +1 -1
  5. package/dist/spark-sdk.js +3 -3
  6. package/dist/spark-sdk.js.map +1 -1
  7. package/package.json +4 -3
  8. package/src/examples/example.js +247 -0
  9. package/src/examples/example.ts +207 -0
  10. package/src/graphql/client.ts +282 -0
  11. package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
  12. package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
  13. package/src/graphql/mutations/RequestCoopExit.ts +20 -0
  14. package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
  15. package/src/graphql/mutations/RequestLightningSend.ts +17 -0
  16. package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
  17. package/src/graphql/objects/BitcoinNetwork.ts +22 -0
  18. package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
  19. package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
  20. package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
  21. package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
  22. package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
  23. package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
  24. package/src/graphql/objects/Connection.ts +90 -0
  25. package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
  26. package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
  27. package/src/graphql/objects/CoopExitRequest.ts +118 -0
  28. package/src/graphql/objects/CurrencyAmount.ts +74 -0
  29. package/src/graphql/objects/CurrencyUnit.ts +32 -0
  30. package/src/graphql/objects/Entity.ts +202 -0
  31. package/src/graphql/objects/GetChallengeInput.ts +37 -0
  32. package/src/graphql/objects/GetChallengeOutput.ts +43 -0
  33. package/src/graphql/objects/Invoice.ts +83 -0
  34. package/src/graphql/objects/Leaf.ts +59 -0
  35. package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
  36. package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
  37. package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
  38. package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
  39. package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
  40. package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
  41. package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
  42. package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
  43. package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
  44. package/src/graphql/objects/LightningSendRequest.ts +134 -0
  45. package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
  46. package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
  47. package/src/graphql/objects/PageInfo.ts +58 -0
  48. package/src/graphql/objects/Provider.ts +41 -0
  49. package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
  50. package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
  51. package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
  52. package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
  53. package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
  54. package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
  55. package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
  56. package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
  57. package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
  58. package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
  59. package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
  60. package/src/graphql/objects/SparkWalletUser.ts +86 -0
  61. package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
  62. package/src/graphql/objects/SwapLeaf.ts +53 -0
  63. package/src/graphql/objects/Transfer.ts +98 -0
  64. package/src/graphql/objects/UserLeafInput.ts +28 -0
  65. package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
  66. package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
  67. package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
  68. package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
  69. package/src/graphql/objects/index.ts +67 -0
  70. package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
  71. package/src/graphql/queries/CurrentUser.ts +10 -0
  72. package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
  73. package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
  74. package/src/proto/common.ts +431 -0
  75. package/src/proto/google/protobuf/descriptor.ts +6625 -0
  76. package/src/proto/google/protobuf/duration.ts +197 -0
  77. package/src/proto/google/protobuf/empty.ts +83 -0
  78. package/src/proto/google/protobuf/timestamp.ts +226 -0
  79. package/src/proto/mock.ts +151 -0
  80. package/src/proto/spark.ts +12727 -0
  81. package/src/proto/spark_authn.ts +673 -0
  82. package/src/proto/validate/validate.ts +6047 -0
  83. package/src/services/config.ts +71 -0
  84. package/src/services/connection.ts +264 -0
  85. package/src/services/coop-exit.ts +190 -0
  86. package/src/services/deposit.ts +327 -0
  87. package/src/services/lightning.ts +341 -0
  88. package/src/services/lrc20.ts +42 -0
  89. package/src/services/token-transactions.ts +499 -0
  90. package/src/services/transfer.ts +1188 -0
  91. package/src/services/tree-creation.ts +618 -0
  92. package/src/services/wallet-config.ts +141 -0
  93. package/src/signer/signer.ts +531 -0
  94. package/src/spark-sdk.ts +1644 -0
  95. package/src/tests/adaptor-signature.test.ts +64 -0
  96. package/src/tests/bitcoin.test.ts +122 -0
  97. package/src/tests/coop-exit.test.ts +233 -0
  98. package/src/tests/deposit.test.ts +98 -0
  99. package/src/tests/keys.test.ts +82 -0
  100. package/src/tests/lightning.test.ts +307 -0
  101. package/src/tests/secret-sharing.test.ts +63 -0
  102. package/src/tests/swap.test.ts +252 -0
  103. package/src/tests/test-util.ts +92 -0
  104. package/src/tests/tokens.test.ts +47 -0
  105. package/src/tests/transfer.test.ts +371 -0
  106. package/src/tests/tree-creation.test.ts +56 -0
  107. package/src/tests/utils/spark-testing-wallet.ts +37 -0
  108. package/src/tests/utils/test-faucet.ts +257 -0
  109. package/src/types/grpc.ts +8 -0
  110. package/src/types/index.ts +3 -0
  111. package/src/utils/adaptor-signature.ts +189 -0
  112. package/src/utils/bitcoin.ts +138 -0
  113. package/src/utils/crypto.ts +14 -0
  114. package/src/utils/index.ts +12 -0
  115. package/src/utils/keys.ts +92 -0
  116. package/src/utils/mempool.ts +42 -0
  117. package/src/utils/network.ts +70 -0
  118. package/src/utils/proof.ts +17 -0
  119. package/src/utils/response-validation.ts +26 -0
  120. package/src/utils/secret-sharing.ts +263 -0
  121. package/src/utils/signing.ts +96 -0
  122. package/src/utils/token-hashing.ts +163 -0
  123. package/src/utils/token-keyshares.ts +31 -0
  124. package/src/utils/token-transactions.ts +71 -0
  125. package/src/utils/transaction.ts +45 -0
  126. package/src/utils/wasm-wrapper.ts +57 -0
  127. package/src/utils/wasm.ts +154 -0
  128. package/src/wasm/spark_bindings.d.ts +208 -0
  129. package/src/wasm/spark_bindings.js +1161 -0
  130. package/src/wasm/spark_bindings_bg.wasm +0 -0
  131. package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
@@ -0,0 +1,71 @@
1
+ import { DefaultSparkSigner, SparkSigner } from "../signer/signer.js";
2
+ import { Network, NetworkToProto } from "../utils/network.js";
3
+ import {
4
+ ConfigOptions,
5
+ LOCAL_WALLET_CONFIG,
6
+ MAINNET_WALLET_CONFIG,
7
+ REGTEST_WALLET_CONFIG,
8
+ SigningOperator,
9
+ } from "./wallet-config.js";
10
+
11
+ export class WalletConfigService {
12
+ private readonly config: Required<ConfigOptions>;
13
+ public readonly signer: SparkSigner;
14
+
15
+ constructor(options?: ConfigOptions, signer?: SparkSigner) {
16
+ const network = options?.network ?? "REGTEST";
17
+
18
+ this.config = {
19
+ ...this.getDefaultConfig(Network[network]),
20
+ ...options,
21
+ };
22
+
23
+ this.signer = signer ?? new DefaultSparkSigner();
24
+ }
25
+
26
+ private getDefaultConfig(network: Network): Required<ConfigOptions> {
27
+ switch (network) {
28
+ case Network.MAINNET:
29
+ return MAINNET_WALLET_CONFIG;
30
+ case Network.REGTEST:
31
+ return REGTEST_WALLET_CONFIG;
32
+ default:
33
+ return LOCAL_WALLET_CONFIG;
34
+ }
35
+ }
36
+
37
+ public getCoordinatorAddress(): string {
38
+ const coordinator =
39
+ this.config.signingOperators[this.config.coodinatorIdentifier];
40
+ if (!coordinator) {
41
+ throw new Error(
42
+ `Coordinator ${this.config.coodinatorIdentifier} not found`,
43
+ );
44
+ }
45
+ return coordinator.address;
46
+ }
47
+
48
+ public getSigningOperators(): Readonly<Record<string, SigningOperator>> {
49
+ return this.config.signingOperators;
50
+ }
51
+
52
+ public getThreshold(): number {
53
+ return this.config.threshold;
54
+ }
55
+
56
+ public getCoordinatorIdentifier(): string {
57
+ return this.config.coodinatorIdentifier;
58
+ }
59
+
60
+ public getNetwork(): Network {
61
+ return Network[this.config.network];
62
+ }
63
+
64
+ public getNetworkProto(): number {
65
+ return NetworkToProto[this.config.network];
66
+ }
67
+
68
+ public shouldSignTokenTransactionsWithSchnorr(): boolean {
69
+ return this.config.useTokenTransactionSchnorrSignatures;
70
+ }
71
+ }
@@ -0,0 +1,264 @@
1
+ import { sha256 } from "@scure/btc-signer/utils";
2
+
3
+ import {
4
+ Channel,
5
+ ChannelCredentials,
6
+ ClientMiddlewareCall,
7
+ createChannel,
8
+ createClient,
9
+ createClientFactory,
10
+ Metadata,
11
+ } from "nice-grpc";
12
+ import { retryMiddleware } from "nice-grpc-client-middleware-retry";
13
+ import { MockServiceClient, MockServiceDefinition } from "../proto/mock.js";
14
+ import { SparkServiceClient, SparkServiceDefinition } from "../proto/spark.js";
15
+ import {
16
+ Challenge,
17
+ SparkAuthnServiceClient,
18
+ SparkAuthnServiceDefinition,
19
+ } from "../proto/spark_authn.js";
20
+ import { SparkCallOptions } from "../types/grpc.js";
21
+ import { WalletConfigService } from "./config.js";
22
+
23
+ // TODO: Some sort of client cleanup
24
+ export class ConnectionManager {
25
+ private config: WalletConfigService;
26
+ private clients: Map<
27
+ string,
28
+ {
29
+ client: SparkServiceClient & { close?: () => void };
30
+ authToken: string;
31
+ }
32
+ > = new Map();
33
+
34
+ constructor(config: WalletConfigService) {
35
+ this.config = config;
36
+ }
37
+
38
+ // When initializing wallet, go ahead and instantiate all clients
39
+ public async createClients() {
40
+ await Promise.all(
41
+ Object.values(this.config.getSigningOperators()).map((operator) => {
42
+ this.createSparkClient(operator.address);
43
+ }),
44
+ );
45
+ }
46
+
47
+ static createMockClient(address: string): MockServiceClient & {
48
+ close: () => void;
49
+ } {
50
+ const channel = this.createChannelWithTLS(address);
51
+
52
+ const client = createClient(MockServiceDefinition, channel);
53
+ return { ...client, close: () => channel.close() };
54
+ }
55
+
56
+ // TODO: Web transport handles TLS differently, verify that we don't need to do anything
57
+ private static createChannelWithTLS(address: string, certPath?: string) {
58
+ try {
59
+ if (typeof window === "undefined") {
60
+ // Node.js environment
61
+ if (certPath) {
62
+ try {
63
+ // Dynamic import for Node.js only
64
+ const fs = require('fs');
65
+ const cert = fs.readFileSync(certPath);
66
+ return createChannel(address, ChannelCredentials.createSsl(cert));
67
+ } catch (error) {
68
+ console.error("Error reading certificate:", error);
69
+ // Fallback to insecure for development
70
+ return createChannel(
71
+ address,
72
+ ChannelCredentials.createSsl(null, null, null, {
73
+ rejectUnauthorized: false,
74
+ })
75
+ );
76
+ }
77
+ } else {
78
+ // No cert provided, use insecure SSL for development
79
+ return createChannel(
80
+ address,
81
+ ChannelCredentials.createSsl(null, null, null, {
82
+ rejectUnauthorized: false,
83
+ })
84
+ );
85
+ }
86
+ } else {
87
+ // Browser environment - nice-grpc-web handles TLS automatically
88
+ return createChannel(address);
89
+ }
90
+ } catch (error) {
91
+ console.error("Channel creation error:", error);
92
+ throw new Error("Failed to create channel");
93
+ }
94
+ }
95
+
96
+ async createSparkClient(
97
+ address: string,
98
+ certPath?: string,
99
+ ): Promise<SparkServiceClient & { close?: () => void }> {
100
+ if (this.clients.has(address)) {
101
+ return this.clients.get(address)!.client;
102
+ }
103
+
104
+ const authToken = await this.authenticate(address);
105
+ const channel = ConnectionManager.createChannelWithTLS(address, certPath);
106
+
107
+ const authMiddleware = this.createAuthMiddleWare(address, authToken);
108
+ const client = this.createGrpcClient<SparkServiceClient>(
109
+ SparkServiceDefinition,
110
+ channel,
111
+ authMiddleware,
112
+ );
113
+
114
+ this.clients.set(address, { client, authToken });
115
+ return client;
116
+ }
117
+
118
+ private async authenticate(address: string) {
119
+ try {
120
+ const identityPublicKey = await this.config.signer.getIdentityPublicKey();
121
+ const sparkAuthnClient = this.createSparkAuthnGrpcConnection(address);
122
+
123
+ const challengeResp = await sparkAuthnClient.get_challenge({
124
+ publicKey: identityPublicKey,
125
+ });
126
+
127
+ if (!challengeResp.protectedChallenge?.challenge) {
128
+ throw new Error("Invalid challenge response");
129
+ }
130
+
131
+ const challengeBytes = Challenge.encode(
132
+ challengeResp.protectedChallenge.challenge,
133
+ ).finish();
134
+ const hash = sha256(challengeBytes);
135
+
136
+ const derSignatureBytes =
137
+ await this.config.signer.signMessageWithIdentityKey(hash);
138
+
139
+ const verifyResp = await sparkAuthnClient.verify_challenge({
140
+ protectedChallenge: challengeResp.protectedChallenge,
141
+ signature: derSignatureBytes,
142
+ publicKey: identityPublicKey,
143
+ });
144
+
145
+ sparkAuthnClient.close?.();
146
+ return verifyResp.sessionToken;
147
+ } catch (error: any) {
148
+ console.error("Authentication error:", error);
149
+ throw new Error(`Authentication failed: ${error.message}`);
150
+ }
151
+ }
152
+
153
+ private createSparkAuthnGrpcConnection(
154
+ address: string,
155
+ certPath?: string,
156
+ ): SparkAuthnServiceClient & { close?: () => void } {
157
+ const channel = ConnectionManager.createChannelWithTLS(address, certPath);
158
+ return this.createGrpcClient<SparkAuthnServiceClient>(
159
+ SparkAuthnServiceDefinition,
160
+ channel,
161
+ );
162
+ }
163
+
164
+ private createAuthMiddleWare(address: string, authToken: string) {
165
+ if (typeof window === "undefined") {
166
+ return this.createNodeMiddleware(address, authToken);
167
+ } else {
168
+ return this.createBrowserMiddleware(address, authToken);
169
+ }
170
+ }
171
+
172
+ private createNodeMiddleware(address: string, initialAuthToken: string) {
173
+ return async function* (
174
+ this: ConnectionManager,
175
+ call: ClientMiddlewareCall<any, any>,
176
+ options: SparkCallOptions,
177
+ ) {
178
+ try {
179
+ yield* call.next(call.request, {
180
+ ...options,
181
+ metadata: Metadata(options.metadata).set(
182
+ "Authorization",
183
+ `Bearer ${this.clients.get(address)?.authToken || initialAuthToken}`,
184
+ ),
185
+ });
186
+ } catch (error: any) {
187
+ if (error.message?.includes("token has expired")) {
188
+ const newAuthToken = await this.authenticate(address);
189
+ // @ts-ignore - We can only get here if the client exists
190
+ this.clients.get(address).authToken = newAuthToken;
191
+
192
+ yield* call.next(call.request, {
193
+ ...options,
194
+ metadata: Metadata(options.metadata).set(
195
+ "Authorization",
196
+ `Bearer ${newAuthToken}`,
197
+ ),
198
+ });
199
+ }
200
+ throw error;
201
+ }
202
+ }.bind(this);
203
+ }
204
+
205
+ private createBrowserMiddleware(address: string, initialAuthToken: string) {
206
+ return async function* (
207
+ this: ConnectionManager,
208
+ call: ClientMiddlewareCall<any, any>,
209
+ options: SparkCallOptions,
210
+ ) {
211
+ try {
212
+ yield* call.next(call.request, {
213
+ ...options,
214
+ metadata: Metadata(options.metadata)
215
+ .set(
216
+ "Authorization",
217
+ `Bearer ${this.clients.get(address)?.authToken || initialAuthToken}`,
218
+ )
219
+ .set("X-Requested-With", "XMLHttpRequest")
220
+ .set("X-Grpc-Web", "1")
221
+ .set("Content-Type", "application/grpc-web+proto"),
222
+ });
223
+ } catch (error: any) {
224
+ if (error.message?.includes("token has expired")) {
225
+ const newAuthToken = await this.authenticate(address);
226
+ // @ts-ignore - We can only get here if the client exists
227
+ this.clients.get(address).authToken = newAuthToken;
228
+
229
+ yield* call.next(call.request, {
230
+ ...options,
231
+ metadata: Metadata(options.metadata)
232
+ .set("Authorization", `Bearer ${newAuthToken}`)
233
+ .set("X-Requested-With", "XMLHttpRequest")
234
+ .set("X-Grpc-Web", "1")
235
+ .set("Content-Type", "application/grpc-web+proto"),
236
+ });
237
+ }
238
+ throw error;
239
+ }
240
+ }.bind(this);
241
+ }
242
+
243
+ private createGrpcClient<T>(
244
+ defintion: SparkAuthnServiceDefinition | SparkServiceDefinition,
245
+ channel: Channel,
246
+ middleware?: any,
247
+ ): T & { close?: () => void } {
248
+ const clientFactory = createClientFactory().use(retryMiddleware);
249
+ if (middleware) {
250
+ clientFactory.use(middleware);
251
+ }
252
+
253
+ const client = clientFactory.create(defintion, channel, {
254
+ "*": {
255
+ retry: true,
256
+ retryMaxAttempts: 3,
257
+ },
258
+ }) as T;
259
+ return {
260
+ ...client,
261
+ close: channel.close?.bind(channel),
262
+ };
263
+ }
264
+ }
@@ -0,0 +1,190 @@
1
+ import { Transaction } from "@scure/btc-signer";
2
+ import { TransactionInput } from "@scure/btc-signer/psbt";
3
+ import {
4
+ CooperativeExitResponse,
5
+ LeafRefundTxSigningJob,
6
+ Transfer,
7
+ } from "../proto/spark.js";
8
+ import {
9
+ getP2TRScriptFromPublicKey,
10
+ getTxFromRawTxBytes,
11
+ } from "../utils/bitcoin.js";
12
+ import { getCrypto } from "../utils/crypto.js";
13
+ import { getNextTransactionSequence } from "../utils/transaction.js";
14
+ import { WalletConfigService } from "./config.js";
15
+ import { ConnectionManager } from "./connection.js";
16
+ import {
17
+ BaseTransferService,
18
+ LeafKeyTweak,
19
+ LeafRefundSigningData,
20
+ } from "./transfer.js";
21
+
22
+ const crypto = getCrypto();
23
+
24
+ export type GetConnectorRefundSignaturesParams = {
25
+ leaves: LeafKeyTweak[];
26
+ exitTxId: Uint8Array;
27
+ connectorOutputs: TransactionInput[];
28
+ receiverPubKey: Uint8Array;
29
+ };
30
+
31
+ export class CoopExitService extends BaseTransferService {
32
+ constructor(
33
+ config: WalletConfigService,
34
+ connectionManager: ConnectionManager,
35
+ ) {
36
+ super(config, connectionManager);
37
+ }
38
+
39
+ async getConnectorRefundSignatures({
40
+ leaves,
41
+ exitTxId,
42
+ connectorOutputs,
43
+ receiverPubKey,
44
+ }: GetConnectorRefundSignaturesParams): Promise<{
45
+ transfer: Transfer;
46
+ signaturesMap: Map<string, Uint8Array>;
47
+ }> {
48
+ const { transfer, signaturesMap } = await this.signCoopExitRefunds(
49
+ leaves,
50
+ exitTxId,
51
+ connectorOutputs,
52
+ receiverPubKey,
53
+ );
54
+ const transferTweak = await this.sendTransferTweakKey(
55
+ transfer,
56
+ leaves,
57
+ signaturesMap,
58
+ );
59
+
60
+ return { transfer: transferTweak, signaturesMap };
61
+ }
62
+
63
+ private createConnectorRefundTransaction(
64
+ sequence: number,
65
+ nodeOutPoint: TransactionInput,
66
+ connectorOutput: TransactionInput,
67
+ amountSats: bigint,
68
+ receiverPubKey: Uint8Array,
69
+ ): Transaction {
70
+ const refundTx = new Transaction();
71
+ if (!nodeOutPoint.txid || nodeOutPoint.index === undefined) {
72
+ throw new Error("Node outpoint txid or index is undefined");
73
+ }
74
+ refundTx.addInput({
75
+ txid: nodeOutPoint.txid,
76
+ index: nodeOutPoint.index,
77
+ sequence,
78
+ });
79
+
80
+ refundTx.addInput(connectorOutput);
81
+ const receiverScript = getP2TRScriptFromPublicKey(
82
+ receiverPubKey,
83
+ this.config.getNetwork(),
84
+ );
85
+
86
+ refundTx.addOutput({
87
+ script: receiverScript,
88
+ amount: amountSats,
89
+ });
90
+
91
+ return refundTx;
92
+ }
93
+ private async signCoopExitRefunds(
94
+ leaves: LeafKeyTweak[],
95
+ exitTxId: Uint8Array,
96
+ connectorOutputs: TransactionInput[],
97
+ receiverPubKey: Uint8Array,
98
+ ): Promise<{ transfer: Transfer; signaturesMap: Map<string, Uint8Array> }> {
99
+ if (leaves.length !== connectorOutputs.length) {
100
+ throw new Error("Number of leaves and connector outputs must match");
101
+ }
102
+
103
+ const signingJobs: LeafRefundTxSigningJob[] = [];
104
+ const leafDataMap: Map<string, LeafRefundSigningData> = new Map();
105
+
106
+ for (let i = 0; i < leaves.length; i++) {
107
+ const leaf = leaves[i];
108
+ const connectorOutput = connectorOutputs[i];
109
+ if (!leaf?.leaf) {
110
+ throw new Error("Leaf not found");
111
+ }
112
+ if (!connectorOutput) {
113
+ throw new Error("Connector output not found");
114
+ }
115
+ const currentRefundTx = getTxFromRawTxBytes(leaf.leaf.refundTx);
116
+
117
+ const sequence = getNextTransactionSequence(
118
+ currentRefundTx.getInput(0).sequence,
119
+ );
120
+
121
+ const refundTx = this.createConnectorRefundTransaction(
122
+ sequence,
123
+ currentRefundTx.getInput(0),
124
+ connectorOutput,
125
+ BigInt(leaf.leaf.value),
126
+ receiverPubKey,
127
+ );
128
+
129
+ const signingNonceCommitment =
130
+ await this.config.signer.getRandomSigningCommitment();
131
+ const signingJob: LeafRefundTxSigningJob = {
132
+ leafId: leaf.leaf.id,
133
+ refundTxSigningJob: {
134
+ signingPublicKey: leaf.signingPubKey,
135
+ rawTx: refundTx.toBytes(),
136
+ signingNonceCommitment: signingNonceCommitment,
137
+ },
138
+ };
139
+
140
+ signingJobs.push(signingJob);
141
+ const tx = getTxFromRawTxBytes(leaf.leaf.nodeTx);
142
+ leafDataMap.set(leaf.leaf.id, {
143
+ signingPubKey: leaf.signingPubKey,
144
+ refundTx,
145
+ signingNonceCommitment,
146
+ tx,
147
+ vout: leaf.leaf.vout,
148
+ receivingPubkey: receiverPubKey,
149
+ });
150
+ }
151
+
152
+ const sparkClient = await this.connectionManager.createSparkClient(
153
+ this.config.getCoordinatorAddress(),
154
+ );
155
+
156
+ let response: CooperativeExitResponse;
157
+ try {
158
+ response = await sparkClient.cooperative_exit({
159
+ transfer: {
160
+ transferId: crypto.randomUUID(),
161
+ leavesToSend: signingJobs,
162
+ ownerIdentityPublicKey:
163
+ await this.config.signer.getIdentityPublicKey(),
164
+ receiverIdentityPublicKey: receiverPubKey,
165
+ expiryTime: new Date(Date.now() + 3 * 60 * 60 * 1000), // 3 hours
166
+ },
167
+ exitId: crypto.randomUUID(),
168
+ exitTxid: exitTxId,
169
+ });
170
+ } catch (error) {
171
+ throw new Error(`Error initiating cooperative exit: ${error}`);
172
+ }
173
+
174
+ if (!response.transfer) {
175
+ throw new Error("Failed to initiate cooperative exit");
176
+ }
177
+
178
+ const signatures = await this.signRefunds(
179
+ leafDataMap,
180
+ response.signingResults,
181
+ );
182
+
183
+ const signaturesMap: Map<string, Uint8Array> = new Map();
184
+ for (const signature of signatures) {
185
+ signaturesMap.set(signature.nodeId, signature.refundTxSignature);
186
+ }
187
+
188
+ return { transfer: response.transfer, signaturesMap };
189
+ }
190
+ }