@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.
- package/dist/services/wallet-config.d.ts +1 -0
- package/dist/services/wallet-config.js +1 -0
- package/dist/services/wallet-config.js.map +1 -1
- package/dist/spark-sdk.d.ts +1 -1
- package/dist/spark-sdk.js +3 -3
- package/dist/spark-sdk.js.map +1 -1
- package/package.json +4 -3
- package/src/examples/example.js +247 -0
- package/src/examples/example.ts +207 -0
- package/src/graphql/client.ts +282 -0
- package/src/graphql/mutations/CompleteCoopExit.ts +19 -0
- package/src/graphql/mutations/CompleteLeavesSwap.ts +17 -0
- package/src/graphql/mutations/RequestCoopExit.ts +20 -0
- package/src/graphql/mutations/RequestLightningReceive.ts +26 -0
- package/src/graphql/mutations/RequestLightningSend.ts +17 -0
- package/src/graphql/mutations/RequestSwapLeaves.ts +24 -0
- package/src/graphql/objects/BitcoinNetwork.ts +22 -0
- package/src/graphql/objects/CompleteCoopExitInput.ts +41 -0
- package/src/graphql/objects/CompleteCoopExitOutput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapInput.ts +45 -0
- package/src/graphql/objects/CompleteLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/CompleteSeedReleaseInput.ts +41 -0
- package/src/graphql/objects/CompleteSeedReleaseOutput.ts +43 -0
- package/src/graphql/objects/Connection.ts +90 -0
- package/src/graphql/objects/CoopExitFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/CoopExitFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/CoopExitRequest.ts +118 -0
- package/src/graphql/objects/CurrencyAmount.ts +74 -0
- package/src/graphql/objects/CurrencyUnit.ts +32 -0
- package/src/graphql/objects/Entity.ts +202 -0
- package/src/graphql/objects/GetChallengeInput.ts +37 -0
- package/src/graphql/objects/GetChallengeOutput.ts +43 -0
- package/src/graphql/objects/Invoice.ts +83 -0
- package/src/graphql/objects/Leaf.ts +59 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LeavesSwapFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LeavesSwapRequest.ts +192 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateInput.ts +41 -0
- package/src/graphql/objects/LightningReceiveFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningReceiveRequest.ts +147 -0
- package/src/graphql/objects/LightningReceiveRequestStatus.ts +34 -0
- package/src/graphql/objects/LightningSendFeeEstimateInput.ts +37 -0
- package/src/graphql/objects/LightningSendFeeEstimateOutput.ts +52 -0
- package/src/graphql/objects/LightningSendRequest.ts +134 -0
- package/src/graphql/objects/LightningSendRequestStatus.ts +28 -0
- package/src/graphql/objects/NotifyReceiverTransferInput.ts +41 -0
- package/src/graphql/objects/PageInfo.ts +58 -0
- package/src/graphql/objects/Provider.ts +41 -0
- package/src/graphql/objects/RequestCoopExitInput.ts +41 -0
- package/src/graphql/objects/RequestCoopExitOutput.ts +45 -0
- package/src/graphql/objects/RequestLeavesSwapInput.ts +55 -0
- package/src/graphql/objects/RequestLeavesSwapOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningReceiveInput.ts +58 -0
- package/src/graphql/objects/RequestLightningReceiveOutput.ts +45 -0
- package/src/graphql/objects/RequestLightningSendInput.ts +41 -0
- package/src/graphql/objects/RequestLightningSendOutput.ts +45 -0
- package/src/graphql/objects/SparkCoopExitRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkLeavesSwapRequestStatus.ts +20 -0
- package/src/graphql/objects/SparkTransferToLeavesConnection.ts +79 -0
- package/src/graphql/objects/SparkWalletUser.ts +86 -0
- package/src/graphql/objects/StartSeedReleaseInput.ts +37 -0
- package/src/graphql/objects/SwapLeaf.ts +53 -0
- package/src/graphql/objects/Transfer.ts +98 -0
- package/src/graphql/objects/UserLeafInput.ts +28 -0
- package/src/graphql/objects/VerifyChallengeInput.ts +51 -0
- package/src/graphql/objects/VerifyChallengeOutput.ts +43 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyInput.ts +37 -0
- package/src/graphql/objects/WalletUserIdentityPublicKeyOutput.ts +43 -0
- package/src/graphql/objects/index.ts +67 -0
- package/src/graphql/queries/CoopExitFeeEstimate.ts +18 -0
- package/src/graphql/queries/CurrentUser.ts +10 -0
- package/src/graphql/queries/LightningReceiveFeeEstimate.ts +18 -0
- package/src/graphql/queries/LightningSendFeeEstimate.ts +16 -0
- package/src/proto/common.ts +431 -0
- package/src/proto/google/protobuf/descriptor.ts +6625 -0
- package/src/proto/google/protobuf/duration.ts +197 -0
- package/src/proto/google/protobuf/empty.ts +83 -0
- package/src/proto/google/protobuf/timestamp.ts +226 -0
- package/src/proto/mock.ts +151 -0
- package/src/proto/spark.ts +12727 -0
- package/src/proto/spark_authn.ts +673 -0
- package/src/proto/validate/validate.ts +6047 -0
- package/src/services/config.ts +71 -0
- package/src/services/connection.ts +264 -0
- package/src/services/coop-exit.ts +190 -0
- package/src/services/deposit.ts +327 -0
- package/src/services/lightning.ts +341 -0
- package/src/services/lrc20.ts +42 -0
- package/src/services/token-transactions.ts +499 -0
- package/src/services/transfer.ts +1188 -0
- package/src/services/tree-creation.ts +618 -0
- package/src/services/wallet-config.ts +141 -0
- package/src/signer/signer.ts +531 -0
- package/src/spark-sdk.ts +1644 -0
- package/src/tests/adaptor-signature.test.ts +64 -0
- package/src/tests/bitcoin.test.ts +122 -0
- package/src/tests/coop-exit.test.ts +233 -0
- package/src/tests/deposit.test.ts +98 -0
- package/src/tests/keys.test.ts +82 -0
- package/src/tests/lightning.test.ts +307 -0
- package/src/tests/secret-sharing.test.ts +63 -0
- package/src/tests/swap.test.ts +252 -0
- package/src/tests/test-util.ts +92 -0
- package/src/tests/tokens.test.ts +47 -0
- package/src/tests/transfer.test.ts +371 -0
- package/src/tests/tree-creation.test.ts +56 -0
- package/src/tests/utils/spark-testing-wallet.ts +37 -0
- package/src/tests/utils/test-faucet.ts +257 -0
- package/src/types/grpc.ts +8 -0
- package/src/types/index.ts +3 -0
- package/src/utils/adaptor-signature.ts +189 -0
- package/src/utils/bitcoin.ts +138 -0
- package/src/utils/crypto.ts +14 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/keys.ts +92 -0
- package/src/utils/mempool.ts +42 -0
- package/src/utils/network.ts +70 -0
- package/src/utils/proof.ts +17 -0
- package/src/utils/response-validation.ts +26 -0
- package/src/utils/secret-sharing.ts +263 -0
- package/src/utils/signing.ts +96 -0
- package/src/utils/token-hashing.ts +163 -0
- package/src/utils/token-keyshares.ts +31 -0
- package/src/utils/token-transactions.ts +71 -0
- package/src/utils/transaction.ts +45 -0
- package/src/utils/wasm-wrapper.ts +57 -0
- package/src/utils/wasm.ts +154 -0
- package/src/wasm/spark_bindings.d.ts +208 -0
- package/src/wasm/spark_bindings.js +1161 -0
- package/src/wasm/spark_bindings_bg.wasm +0 -0
- package/src/wasm/spark_bindings_bg.wasm.d.ts +136 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bytesToHex,
|
|
3
|
+
bytesToNumberBE,
|
|
4
|
+
numberToBytesBE,
|
|
5
|
+
} from "@noble/curves/abstract/utils";
|
|
6
|
+
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
7
|
+
import {
|
|
8
|
+
LeafWithPreviousTransactionData,
|
|
9
|
+
OperatorSpecificTokenTransactionSignablePayload,
|
|
10
|
+
OperatorSpecificTokenTransactionSignature,
|
|
11
|
+
TokenTransaction,
|
|
12
|
+
} from "../proto/spark.js";
|
|
13
|
+
import { SparkCallOptions } from "../types/grpc.js";
|
|
14
|
+
import { validateResponses } from "../utils/response-validation.js";
|
|
15
|
+
import {
|
|
16
|
+
hashOperatorSpecificTokenTransactionSignablePayload,
|
|
17
|
+
hashTokenTransaction,
|
|
18
|
+
} from "../utils/token-hashing.js";
|
|
19
|
+
import {
|
|
20
|
+
KeyshareWithOperatorIndex,
|
|
21
|
+
recoverPrivateKeyFromKeyshares,
|
|
22
|
+
} from "../utils/token-keyshares.js";
|
|
23
|
+
import { calculateAvailableTokenAmount } from "../utils/token-transactions.js";
|
|
24
|
+
import { WalletConfigService } from "./config.js";
|
|
25
|
+
import { ConnectionManager } from "./connection.js";
|
|
26
|
+
|
|
27
|
+
export class TokenTransactionService {
|
|
28
|
+
protected readonly config: WalletConfigService;
|
|
29
|
+
protected readonly connectionManager: ConnectionManager;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
config: WalletConfigService,
|
|
33
|
+
connectionManager: ConnectionManager,
|
|
34
|
+
) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.connectionManager = connectionManager;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async constructTransferTokenTransaction(
|
|
40
|
+
selectedLeaves: LeafWithPreviousTransactionData[],
|
|
41
|
+
receiverSparkAddress: Uint8Array,
|
|
42
|
+
tokenPublicKey: Uint8Array,
|
|
43
|
+
tokenAmount: bigint,
|
|
44
|
+
): Promise<TokenTransaction> {
|
|
45
|
+
let availableTokenAmount = calculateAvailableTokenAmount(selectedLeaves);
|
|
46
|
+
|
|
47
|
+
if (availableTokenAmount === tokenAmount) {
|
|
48
|
+
return {
|
|
49
|
+
tokenInput: {
|
|
50
|
+
$case: "transferInput",
|
|
51
|
+
transferInput: {
|
|
52
|
+
leavesToSpend: selectedLeaves.map((leaf) => ({
|
|
53
|
+
prevTokenTransactionHash: leaf.previousTransactionHash,
|
|
54
|
+
prevTokenTransactionLeafVout: leaf.previousTransactionVout,
|
|
55
|
+
})),
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
outputLeaves: [
|
|
59
|
+
{
|
|
60
|
+
ownerPublicKey: receiverSparkAddress,
|
|
61
|
+
tokenPublicKey: tokenPublicKey,
|
|
62
|
+
tokenAmount: numberToBytesBE(tokenAmount, 16),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
sparkOperatorIdentityPublicKeys:
|
|
66
|
+
this.collectOperatorIdentityPublicKeys(),
|
|
67
|
+
};
|
|
68
|
+
} else {
|
|
69
|
+
const tokenAmountDifference = availableTokenAmount - tokenAmount;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
tokenInput: {
|
|
73
|
+
$case: "transferInput",
|
|
74
|
+
transferInput: {
|
|
75
|
+
leavesToSpend: selectedLeaves.map((leaf) => ({
|
|
76
|
+
prevTokenTransactionHash: leaf.previousTransactionHash,
|
|
77
|
+
prevTokenTransactionLeafVout: leaf.previousTransactionVout,
|
|
78
|
+
})),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
outputLeaves: [
|
|
82
|
+
{
|
|
83
|
+
ownerPublicKey: receiverSparkAddress,
|
|
84
|
+
tokenPublicKey: tokenPublicKey,
|
|
85
|
+
tokenAmount: numberToBytesBE(tokenAmount, 16),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
ownerPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
89
|
+
tokenPublicKey: tokenPublicKey,
|
|
90
|
+
tokenAmount: numberToBytesBE(tokenAmountDifference, 16),
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
sparkOperatorIdentityPublicKeys:
|
|
94
|
+
this.collectOperatorIdentityPublicKeys(),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public collectOperatorIdentityPublicKeys(): Uint8Array[] {
|
|
100
|
+
const operatorKeys: Uint8Array[] = [];
|
|
101
|
+
for (const [_, operator] of Object.entries(
|
|
102
|
+
this.config.getSigningOperators(),
|
|
103
|
+
)) {
|
|
104
|
+
operatorKeys.push(operator.identityPublicKey);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return operatorKeys;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public async broadcastTokenTransaction(
|
|
111
|
+
tokenTransaction: TokenTransaction,
|
|
112
|
+
leafToSpendSigningPublicKeys?: Uint8Array[],
|
|
113
|
+
leafToSpendRevocationPublicKeys?: Uint8Array[],
|
|
114
|
+
): Promise<string> {
|
|
115
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
116
|
+
this.config.getCoordinatorAddress(),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const signingOperators = this.config.getSigningOperators();
|
|
120
|
+
|
|
121
|
+
const partialTokenTransactionHash = hashTokenTransaction(
|
|
122
|
+
tokenTransaction,
|
|
123
|
+
true,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const ownerSignatures: Uint8Array[] = [];
|
|
127
|
+
if (tokenTransaction.tokenInput!.$case === "mintInput") {
|
|
128
|
+
const issuerPublicKey =
|
|
129
|
+
tokenTransaction.tokenInput!.mintInput.issuerPublicKey;
|
|
130
|
+
if (!issuerPublicKey) {
|
|
131
|
+
throw new Error("issuer public key cannot be nil");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const ownerSignature = await this.signMessageWithKey(
|
|
135
|
+
partialTokenTransactionHash,
|
|
136
|
+
issuerPublicKey,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
ownerSignatures.push(ownerSignature);
|
|
140
|
+
} else if (tokenTransaction.tokenInput!.$case === "transferInput") {
|
|
141
|
+
const transferInput = tokenTransaction.tokenInput!.transferInput;
|
|
142
|
+
|
|
143
|
+
if (!leafToSpendSigningPublicKeys || !leafToSpendRevocationPublicKeys) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"leafToSpendSigningPublicKeys and leafToSpendRevocationPublicKeys are required",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < transferInput.leavesToSpend.length; i++) {
|
|
150
|
+
const key = leafToSpendSigningPublicKeys![i];
|
|
151
|
+
if (!key) {
|
|
152
|
+
throw new Error("key not found");
|
|
153
|
+
}
|
|
154
|
+
const ownerSignature = await this.signMessageWithKey(
|
|
155
|
+
partialTokenTransactionHash,
|
|
156
|
+
key,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
ownerSignatures.push(ownerSignature);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Start the token transaction
|
|
164
|
+
const startResponse = await sparkClient.start_token_transaction({
|
|
165
|
+
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
166
|
+
partialTokenTransaction: tokenTransaction,
|
|
167
|
+
tokenTransactionSignatures: {
|
|
168
|
+
ownerSignatures: ownerSignatures,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Validate keyshare configuration
|
|
173
|
+
if (
|
|
174
|
+
startResponse.keyshareInfo?.ownerIdentifiers.length !==
|
|
175
|
+
Object.keys(signingOperators).length
|
|
176
|
+
) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Keyshare operator count (${
|
|
179
|
+
startResponse.keyshareInfo?.ownerIdentifiers.length
|
|
180
|
+
}) does not match signing operator count (${
|
|
181
|
+
Object.keys(signingOperators).length
|
|
182
|
+
})`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const identifier of startResponse.keyshareInfo?.ownerIdentifiers ||
|
|
187
|
+
[]) {
|
|
188
|
+
if (!signingOperators[identifier]) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Keyshare operator ${identifier} not found in signing operator list`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const finalTokenTransaction = startResponse.finalTokenTransaction!;
|
|
196
|
+
const finalTokenTransactionHash = hashTokenTransaction(
|
|
197
|
+
finalTokenTransaction,
|
|
198
|
+
false,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const payload: OperatorSpecificTokenTransactionSignablePayload = {
|
|
202
|
+
finalTokenTransactionHash: finalTokenTransactionHash,
|
|
203
|
+
operatorIdentityPublicKey:
|
|
204
|
+
await this.config.signer.getIdentityPublicKey(),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const payloadHash =
|
|
208
|
+
await hashOperatorSpecificTokenTransactionSignablePayload(payload);
|
|
209
|
+
|
|
210
|
+
const operatorSpecificSignatures: OperatorSpecificTokenTransactionSignature[] =
|
|
211
|
+
[];
|
|
212
|
+
if (tokenTransaction.tokenInput!.$case === "mintInput") {
|
|
213
|
+
const issuerPublicKey =
|
|
214
|
+
tokenTransaction.tokenInput!.mintInput.issuerPublicKey;
|
|
215
|
+
if (!issuerPublicKey) {
|
|
216
|
+
throw new Error("issuer public key cannot be nil");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const ownerSignature = await this.signMessageWithKey(
|
|
220
|
+
payloadHash,
|
|
221
|
+
issuerPublicKey,
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
operatorSpecificSignatures.push({
|
|
225
|
+
ownerPublicKey: issuerPublicKey,
|
|
226
|
+
ownerSignature: ownerSignature,
|
|
227
|
+
payload: payload,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (tokenTransaction.tokenInput!.$case === "transferInput") {
|
|
232
|
+
const transferInput = tokenTransaction.tokenInput!.transferInput;
|
|
233
|
+
for (let i = 0; i < transferInput.leavesToSpend.length; i++) {
|
|
234
|
+
let ownerSignature: Uint8Array;
|
|
235
|
+
if (this.config.shouldSignTokenTransactionsWithSchnorr()) {
|
|
236
|
+
ownerSignature =
|
|
237
|
+
await this.config.signer.signSchnorrWithIdentityKey(payloadHash);
|
|
238
|
+
} else {
|
|
239
|
+
ownerSignature =
|
|
240
|
+
await this.config.signer.signMessageWithIdentityKey(payloadHash);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
operatorSpecificSignatures.push({
|
|
244
|
+
ownerPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
245
|
+
ownerSignature: ownerSignature,
|
|
246
|
+
payload: payload,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Submit sign_token_transaction to all SOs in parallel and track their indices
|
|
252
|
+
const soSignatures = await Promise.allSettled(
|
|
253
|
+
Object.entries(signingOperators).map(
|
|
254
|
+
async ([identifier, operator], index) => {
|
|
255
|
+
const internalSparkClient =
|
|
256
|
+
await this.connectionManager.createSparkClient(operator.address);
|
|
257
|
+
const identityPublicKey =
|
|
258
|
+
await this.config.signer.getIdentityPublicKey();
|
|
259
|
+
|
|
260
|
+
const response = await internalSparkClient.sign_token_transaction(
|
|
261
|
+
{
|
|
262
|
+
finalTokenTransaction,
|
|
263
|
+
operatorSpecificSignatures,
|
|
264
|
+
identityPublicKey,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
retry: true,
|
|
268
|
+
retryMaxAttempts: 5,
|
|
269
|
+
} as SparkCallOptions,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
index,
|
|
274
|
+
identifier,
|
|
275
|
+
response,
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const threshold = startResponse.keyshareInfo.threshold;
|
|
282
|
+
const successfulSignatures = validateResponses(soSignatures);
|
|
283
|
+
|
|
284
|
+
if (tokenTransaction.tokenInput!.$case === "transferInput") {
|
|
285
|
+
const leavesToSpend =
|
|
286
|
+
tokenTransaction.tokenInput!.transferInput.leavesToSpend;
|
|
287
|
+
|
|
288
|
+
let revocationKeys: Uint8Array[] = [];
|
|
289
|
+
|
|
290
|
+
for (let leafIndex = 0; leafIndex < leavesToSpend.length; leafIndex++) {
|
|
291
|
+
// For each leaf, collect keyshares from all SOs that responded successfully
|
|
292
|
+
const leafKeyshares = successfulSignatures.map(
|
|
293
|
+
({ identifier, response }) => ({
|
|
294
|
+
index: parseInt(identifier, 16),
|
|
295
|
+
keyshare: response.tokenTransactionRevocationKeyshares[leafIndex],
|
|
296
|
+
}),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (leafKeyshares.length < threshold) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Insufficient keyshares for leaf ${leafIndex}: got ${leafKeyshares.length}, need ${threshold}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check for duplicate operator indices
|
|
306
|
+
const seenIndices = new Set<number>();
|
|
307
|
+
for (const { index } of leafKeyshares) {
|
|
308
|
+
if (seenIndices.has(index)) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Duplicate operator index ${index} for leaf ${leafIndex}`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
seenIndices.add(index);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const recoveredPrivateKey = recoverPrivateKeyFromKeyshares(
|
|
317
|
+
leafKeyshares as KeyshareWithOperatorIndex[],
|
|
318
|
+
threshold,
|
|
319
|
+
);
|
|
320
|
+
const recoveredPublicKey = secp256k1.getPublicKey(
|
|
321
|
+
recoveredPrivateKey,
|
|
322
|
+
true,
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (
|
|
326
|
+
!leafToSpendRevocationPublicKeys ||
|
|
327
|
+
!leafToSpendRevocationPublicKeys[leafIndex] ||
|
|
328
|
+
!recoveredPublicKey.every(
|
|
329
|
+
(byte, i) =>
|
|
330
|
+
byte === leafToSpendRevocationPublicKeys[leafIndex]![i],
|
|
331
|
+
)
|
|
332
|
+
) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Recovered public key does not match expected revocation public key for leaf ${leafIndex}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
revocationKeys.push(recoveredPrivateKey);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Finalize the token transaction with the keyshares
|
|
342
|
+
await this.finalizeTokenTransaction(
|
|
343
|
+
finalTokenTransaction,
|
|
344
|
+
revocationKeys,
|
|
345
|
+
threshold,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return bytesToHex(
|
|
350
|
+
hashTokenTransaction(startResponse.finalTokenTransaction!),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
public async finalizeTokenTransaction(
|
|
355
|
+
finalTokenTransaction: TokenTransaction,
|
|
356
|
+
leafToSpendRevocationKeys: Uint8Array[],
|
|
357
|
+
threshold: number,
|
|
358
|
+
): Promise<TokenTransaction> {
|
|
359
|
+
const signingOperators = this.config.getSigningOperators();
|
|
360
|
+
// Submit finalize_token_transaction to all SOs in parallel
|
|
361
|
+
const soResponses = await Promise.allSettled(
|
|
362
|
+
Object.entries(signingOperators).map(async ([identifier, operator]) => {
|
|
363
|
+
const internalSparkClient =
|
|
364
|
+
await this.connectionManager.createSparkClient(operator.address);
|
|
365
|
+
const identityPublicKey =
|
|
366
|
+
await this.config.signer.getIdentityPublicKey();
|
|
367
|
+
|
|
368
|
+
const response = await internalSparkClient.finalize_token_transaction(
|
|
369
|
+
{
|
|
370
|
+
finalTokenTransaction,
|
|
371
|
+
leafToSpendRevocationKeys,
|
|
372
|
+
identityPublicKey,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
retry: true,
|
|
376
|
+
retryMaxAttempts: 5,
|
|
377
|
+
} as SparkCallOptions,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
identifier,
|
|
382
|
+
response,
|
|
383
|
+
};
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
validateResponses(soResponses);
|
|
388
|
+
|
|
389
|
+
return finalTokenTransaction;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
public async fetchOwnedTokenLeaves(
|
|
393
|
+
ownerPublicKeys: Uint8Array[],
|
|
394
|
+
tokenPublicKeys: Uint8Array[],
|
|
395
|
+
): Promise<LeafWithPreviousTransactionData[]> {
|
|
396
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
397
|
+
this.config.getCoordinatorAddress(),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
const result = await sparkClient.get_owned_token_leaves({
|
|
401
|
+
ownerPublicKeys,
|
|
402
|
+
tokenPublicKeys,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
return result.leavesWithPreviousTransactionData;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
public async syncTokenLeaves(
|
|
409
|
+
tokenLeaves: Map<string, LeafWithPreviousTransactionData[]>,
|
|
410
|
+
) {
|
|
411
|
+
const unsortedTokenLeaves = await this.fetchOwnedTokenLeaves(
|
|
412
|
+
await this.config.signer.getTrackedPublicKeys(),
|
|
413
|
+
[],
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
unsortedTokenLeaves.forEach((leaf) => {
|
|
417
|
+
const tokenKey = bytesToHex(leaf.leaf!.tokenPublicKey!);
|
|
418
|
+
const index = leaf.previousTransactionVout!;
|
|
419
|
+
|
|
420
|
+
tokenLeaves.set(tokenKey, [{ ...leaf, previousTransactionVout: index }]);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
public selectTokenLeaves(
|
|
425
|
+
tokenLeaves: LeafWithPreviousTransactionData[],
|
|
426
|
+
tokenAmount: bigint,
|
|
427
|
+
): LeafWithPreviousTransactionData[] {
|
|
428
|
+
if (calculateAvailableTokenAmount(tokenLeaves) < tokenAmount) {
|
|
429
|
+
throw new Error("Insufficient available token amount");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// First try to find an exact match
|
|
433
|
+
const exactMatch: LeafWithPreviousTransactionData | undefined =
|
|
434
|
+
tokenLeaves.find(
|
|
435
|
+
(item) => bytesToNumberBE(item.leaf!.tokenAmount!) === tokenAmount,
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
if (exactMatch) {
|
|
439
|
+
return [exactMatch];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Sort by amount ascending for optimal selection.
|
|
443
|
+
// It's in user's interest to hold as little leaves as possible,
|
|
444
|
+
// so that in the event of a unilateral exit the fees are as low as possible
|
|
445
|
+
tokenLeaves.sort((a, b) =>
|
|
446
|
+
Number(
|
|
447
|
+
bytesToNumberBE(a.leaf!.tokenAmount!) -
|
|
448
|
+
bytesToNumberBE(b.leaf!.tokenAmount!),
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
let remainingAmount = tokenAmount;
|
|
453
|
+
const selectedLeaves: typeof tokenLeaves = [];
|
|
454
|
+
|
|
455
|
+
// Select leaves using a greedy approach
|
|
456
|
+
for (const leafInfo of tokenLeaves) {
|
|
457
|
+
if (remainingAmount <= 0n) break;
|
|
458
|
+
|
|
459
|
+
selectedLeaves.push(leafInfo);
|
|
460
|
+
remainingAmount -= bytesToNumberBE(leafInfo.leaf!.tokenAmount!);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (remainingAmount > 0n) {
|
|
464
|
+
throw new Error(
|
|
465
|
+
"You do not have enough funds to complete the specified operation",
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return selectedLeaves;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Helper function for deciding if the signer public key is the identity public key
|
|
473
|
+
private async signMessageWithKey(
|
|
474
|
+
message: Uint8Array,
|
|
475
|
+
publicKey: Uint8Array,
|
|
476
|
+
): Promise<Uint8Array> {
|
|
477
|
+
const signWithSchnorr =
|
|
478
|
+
this.config.shouldSignTokenTransactionsWithSchnorr();
|
|
479
|
+
if (
|
|
480
|
+
bytesToHex(publicKey) ===
|
|
481
|
+
bytesToHex(await this.config.signer.getIdentityPublicKey())
|
|
482
|
+
) {
|
|
483
|
+
if (signWithSchnorr) {
|
|
484
|
+
return await this.config.signer.signSchnorrWithIdentityKey(message);
|
|
485
|
+
} else {
|
|
486
|
+
return await this.config.signer.signMessageWithIdentityKey(message);
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
if (signWithSchnorr) {
|
|
490
|
+
return await this.config.signer.signSchnorr(message, publicKey);
|
|
491
|
+
} else {
|
|
492
|
+
return await this.config.signer.signMessageWithPublicKey(
|
|
493
|
+
message,
|
|
494
|
+
publicKey,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|