@buildonspark/spark-sdk 0.1.38 → 0.1.40
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 +12 -0
- package/android/build/intermediates/incremental/mergeDebugJniLibFolders/merger.xml +1 -1
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/arm64-v8a/libspark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/arm64-v8a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/armeabi-v7a/libspark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/armeabi-v7a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86/libspark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86_64/libspark_frost.so +0 -0
- package/android/build/intermediates/library_jni/debug/copyDebugJniLibsProjectOnly/jni/x86_64/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/arm64-v8a/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/arm64-v8a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/armeabi-v7a/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/armeabi-v7a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86_64/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_jni_libs/debug/mergeDebugJniLibFolders/out/x86_64/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/arm64-v8a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/armeabi-v7a/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/armeabi-v7a/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86/libuniffi_spark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86_64/libspark_frost.so +0 -0
- package/android/build/intermediates/merged_native_libs/debug/mergeDebugNativeLibs/out/lib/x86_64/libuniffi_spark_frost.so +0 -0
- package/dist/{RequestLightningSendInput-B4JdzclX.d.ts → RequestLightningSendInput-CJtcHOnu.d.ts} +1 -1
- package/dist/{RequestLightningSendInput-39_zGri6.d.cts → RequestLightningSendInput-DfmfqzZo.d.cts} +1 -1
- package/dist/address/index.d.cts +1 -1
- package/dist/address/index.d.ts +1 -1
- package/dist/address/index.js +2 -2
- package/dist/{chunk-W3EC5XSA.js → chunk-5MNQB2T4.js} +2 -2
- package/dist/chunk-ED3ZAFDI.js +784 -0
- package/dist/{chunk-VJTDG4BQ.js → chunk-HK6LPV6Z.js} +10 -1
- package/dist/{chunk-7WRK6WNJ.js → chunk-LHT4QTFK.js} +556 -41
- package/dist/{chunk-RAPBVYJY.js → chunk-RFCXPGDM.js} +26 -4
- package/dist/{chunk-DI7QXUQJ.js → chunk-W2VXS35Y.js} +4 -4
- package/dist/graphql/objects/index.d.cts +5 -4
- package/dist/graphql/objects/index.d.ts +5 -4
- package/dist/{index-CxAi2L8y.d.ts → index-BDEYgYxP.d.ts} +42 -4
- package/dist/{index-Dm17Ggfe.d.cts → index-CLdtdMU4.d.cts} +42 -4
- package/dist/index.cjs +1069 -40
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +33 -17
- package/dist/index.node.cjs +1069 -40
- package/dist/index.node.d.cts +6 -6
- package/dist/index.node.d.ts +6 -6
- package/dist/index.node.js +33 -17
- package/dist/native/index.cjs +1069 -40
- package/dist/native/index.d.cts +108 -5
- package/dist/native/index.d.ts +108 -5
- package/dist/native/index.js +1065 -40
- package/dist/{network-GFGEHkS4.d.cts → network-B10hBoHp.d.cts} +8 -1
- package/dist/{network-DobHpaV6.d.ts → network-CCgyIsGl.d.ts} +8 -1
- package/dist/services/config.cjs +29 -12
- package/dist/services/config.d.cts +4 -4
- package/dist/services/config.d.ts +4 -4
- package/dist/services/config.js +5 -5
- package/dist/services/connection.d.cts +4 -4
- package/dist/services/connection.d.ts +4 -4
- package/dist/services/connection.js +2 -2
- package/dist/services/index.cjs +30 -13
- package/dist/services/index.d.cts +4 -4
- package/dist/services/index.d.ts +4 -4
- package/dist/services/index.js +8 -8
- package/dist/services/lrc-connection.d.cts +4 -4
- package/dist/services/lrc-connection.d.ts +4 -4
- package/dist/services/lrc-connection.js +1 -1
- package/dist/services/token-transactions.cjs +1 -1
- package/dist/services/token-transactions.d.cts +4 -4
- package/dist/services/token-transactions.d.ts +4 -4
- package/dist/services/token-transactions.js +3 -3
- package/dist/services/wallet-config.d.cts +4 -4
- package/dist/services/wallet-config.d.ts +4 -4
- package/dist/signer/signer.cjs +23 -6
- package/dist/signer/signer.d.cts +3 -2
- package/dist/signer/signer.d.ts +3 -2
- package/dist/signer/signer.js +1 -1
- package/dist/{signer-DFGw9RRp.d.ts → signer-C5h1DpjF.d.ts} +4 -1
- package/dist/{signer-C1t40Wus.d.cts → signer-CYwn7h9U.d.cts} +4 -1
- package/dist/types/index.d.cts +4 -3
- package/dist/types/index.d.ts +4 -3
- package/dist/utils/index.cjs +891 -2
- package/dist/utils/index.d.cts +62 -6
- package/dist/utils/index.d.ts +62 -6
- package/dist/utils/index.js +23 -7
- package/package.json +1 -1
- package/src/services/deposit.ts +23 -5
- package/src/services/token-transactions.ts +1 -1
- package/src/services/transfer.ts +218 -11
- package/src/services/tree-creation.ts +29 -14
- package/src/signer/signer.ts +47 -5
- package/src/spark-wallet/spark-wallet.ts +430 -4
- package/src/tests/integration/swap.test.ts +225 -0
- package/src/tests/integration/tree-creation.test.ts +5 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/mempool.ts +26 -1
- package/src/utils/network.ts +15 -0
- package/src/utils/transaction.ts +22 -2
- package/src/utils/unilateral-exit.ts +729 -0
- package/dist/chunk-E5SL7XTO.js +0 -301
- package/dist/{chunk-LIP2K6KR.js → chunk-2CDJZQN4.js} +3 -3
- package/dist/{chunk-RGWBSZIO.js → chunk-I4JI6TYN.js} +4 -4
|
@@ -24,7 +24,10 @@ import {
|
|
|
24
24
|
getTxId,
|
|
25
25
|
} from "../utils/bitcoin.js";
|
|
26
26
|
import { getNetwork, Network } from "../utils/network.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_FEE_SATS,
|
|
29
|
+
getEphemeralAnchorOutput,
|
|
30
|
+
} from "../utils/transaction.js";
|
|
28
31
|
import { WalletConfigService } from "./config.js";
|
|
29
32
|
import { ConnectionManager } from "./connection.js";
|
|
30
33
|
|
|
@@ -42,6 +45,17 @@ export type CreationNodeWithNonces = CreationNode & {
|
|
|
42
45
|
|
|
43
46
|
const INITIAL_TIME_LOCK = 2000;
|
|
44
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Subtracts the default fee from the amount if it's greater than the fee.
|
|
50
|
+
* Returns the original amount if it's less than or equal to the fee.
|
|
51
|
+
*/
|
|
52
|
+
function maybeApplyFee(amount: bigint): bigint {
|
|
53
|
+
if (amount > BigInt(DEFAULT_FEE_SATS)) {
|
|
54
|
+
return amount - BigInt(DEFAULT_FEE_SATS);
|
|
55
|
+
}
|
|
56
|
+
return amount;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
export class TreeCreationService {
|
|
46
60
|
private readonly config: WalletConfigService;
|
|
47
61
|
private readonly connectionManager: ConnectionManager;
|
|
@@ -303,7 +317,7 @@ export class TreeCreationService {
|
|
|
303
317
|
children: [],
|
|
304
318
|
};
|
|
305
319
|
|
|
306
|
-
const tx = new Transaction();
|
|
320
|
+
const tx = new Transaction({ version: 3 });
|
|
307
321
|
tx.addInput({
|
|
308
322
|
txid: getTxId(parentTx),
|
|
309
323
|
index: vout,
|
|
@@ -316,10 +330,9 @@ export class TreeCreationService {
|
|
|
316
330
|
|
|
317
331
|
tx.addOutput({
|
|
318
332
|
script: parentTxOut.script,
|
|
319
|
-
amount: parentTxOut.amount,
|
|
333
|
+
amount: parentTxOut.amount, // maybeApplyFee(parentTxOut.amount),
|
|
320
334
|
});
|
|
321
335
|
|
|
322
|
-
// Add ephemeral anchor
|
|
323
336
|
tx.addOutput(getEphemeralAnchorOutput());
|
|
324
337
|
|
|
325
338
|
const signingNonceCommitment =
|
|
@@ -342,7 +355,7 @@ export class TreeCreationService {
|
|
|
342
355
|
children: [],
|
|
343
356
|
};
|
|
344
357
|
|
|
345
|
-
const childTx = new Transaction();
|
|
358
|
+
const childTx = new Transaction({ version: 3 });
|
|
346
359
|
childTx.addInput({
|
|
347
360
|
txid: getTxId(tx),
|
|
348
361
|
index: 0,
|
|
@@ -351,10 +364,9 @@ export class TreeCreationService {
|
|
|
351
364
|
|
|
352
365
|
childTx.addOutput({
|
|
353
366
|
script: parentTxOut.script,
|
|
354
|
-
amount: parentTxOut.amount,
|
|
367
|
+
amount: parentTxOut.amount, // maybeApplyFee(parentTxOut.amount),
|
|
355
368
|
});
|
|
356
369
|
|
|
357
|
-
// Add ephemeral anchor
|
|
358
370
|
childTx.addOutput(getEphemeralAnchorOutput());
|
|
359
371
|
|
|
360
372
|
const childSigningNonceCommitment =
|
|
@@ -368,7 +380,7 @@ export class TreeCreationService {
|
|
|
368
380
|
childCreationNode.nodeTxSigningCommitment = childSigningNonceCommitment;
|
|
369
381
|
childCreationNode.nodeTxSigningJob = childSigningJob;
|
|
370
382
|
|
|
371
|
-
const refundTx = new Transaction();
|
|
383
|
+
const refundTx = new Transaction({ version: 3 });
|
|
372
384
|
refundTx.addInput({
|
|
373
385
|
txid: getTxId(childTx),
|
|
374
386
|
index: 0,
|
|
@@ -385,9 +397,11 @@ export class TreeCreationService {
|
|
|
385
397
|
const refundPkScript = OutScript.encode(refundAddress);
|
|
386
398
|
refundTx.addOutput({
|
|
387
399
|
script: refundPkScript,
|
|
388
|
-
amount: parentTxOut.amount,
|
|
400
|
+
amount: maybeApplyFee(parentTxOut.amount),
|
|
389
401
|
});
|
|
390
402
|
|
|
403
|
+
refundTx.addOutput(getEphemeralAnchorOutput());
|
|
404
|
+
|
|
391
405
|
const refundSigningNonceCommitment =
|
|
392
406
|
await this.config.signer.getRandomSigningCommitment();
|
|
393
407
|
|
|
@@ -415,7 +429,7 @@ export class TreeCreationService {
|
|
|
415
429
|
if (!parentTxOutput?.script || !parentTxOutput?.amount) {
|
|
416
430
|
throw new Error("parentTxOutput is undefined");
|
|
417
431
|
}
|
|
418
|
-
const rootNodeTx = new Transaction();
|
|
432
|
+
const rootNodeTx = new Transaction({ version: 3 });
|
|
419
433
|
rootNodeTx.addInput({
|
|
420
434
|
txid: getTxId(parentTx),
|
|
421
435
|
index: vout,
|
|
@@ -428,15 +442,16 @@ export class TreeCreationService {
|
|
|
428
442
|
}
|
|
429
443
|
const childAddress = Address(getNetwork(network)).decode(child.address);
|
|
430
444
|
const childPkScript = OutScript.encode(childAddress);
|
|
445
|
+
|
|
446
|
+
// First subtract fee from total amount, then split between children
|
|
447
|
+
// const feeAdjustedAmount = maybeApplyFee(parentTxOutput.amount);
|
|
431
448
|
rootNodeTx.addOutput({
|
|
432
449
|
script: childPkScript,
|
|
433
|
-
amount: parentTxOutput.amount / 2n,
|
|
450
|
+
amount: parentTxOutput.amount / 2n, // feeAdjustedAmount / 2n,
|
|
434
451
|
});
|
|
435
452
|
}
|
|
436
453
|
|
|
437
|
-
|
|
438
|
-
const anchor = getEphemeralAnchorOutput();
|
|
439
|
-
rootNodeTx.addOutput(anchor);
|
|
454
|
+
rootNodeTx.addOutput(getEphemeralAnchorOutput());
|
|
440
455
|
|
|
441
456
|
const rootNodeSigningCommitment =
|
|
442
457
|
await this.config.signer.getRandomSigningCommitment();
|
package/src/signer/signer.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bytesToHex,
|
|
3
3
|
bytesToNumberBE,
|
|
4
|
+
equalBytes,
|
|
4
5
|
hexToBytes,
|
|
5
6
|
} from "@noble/curves/abstract/utils";
|
|
6
7
|
import { schnorr, secp256k1 } from "@noble/curves/secp256k1";
|
|
@@ -35,12 +36,16 @@ const getSparkFrostModule = async () => {
|
|
|
35
36
|
return sparkFrostModule;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
import {
|
|
39
|
-
import
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
import { privateAdd, privateNegate } from "@bitcoinerlab/secp256k1";
|
|
40
|
+
import {
|
|
41
|
+
fromPrivateKey,
|
|
42
|
+
PARITY,
|
|
43
|
+
Receipt,
|
|
44
|
+
TokenSigner,
|
|
45
|
+
} from "@buildonspark/lrc20-sdk";
|
|
42
46
|
import { sha256 } from "@noble/hashes/sha2";
|
|
43
|
-
import {
|
|
47
|
+
import { Transaction } from "@scure/btc-signer";
|
|
48
|
+
import type { Psbt } from "bitcoinjs-lib";
|
|
44
49
|
|
|
45
50
|
export type SigningNonce = {
|
|
46
51
|
binding: Uint8Array;
|
|
@@ -223,6 +228,12 @@ interface SparkSigner extends TokenSigner {
|
|
|
223
228
|
signature: Uint8Array,
|
|
224
229
|
): Promise<boolean>;
|
|
225
230
|
|
|
231
|
+
signTransactionIndex(
|
|
232
|
+
tx: Transaction,
|
|
233
|
+
index: number,
|
|
234
|
+
publicKey: Uint8Array,
|
|
235
|
+
): void;
|
|
236
|
+
|
|
226
237
|
encryptLeafPrivateKeyEcies(
|
|
227
238
|
receiverPublicKey: Uint8Array,
|
|
228
239
|
publicKey: Uint8Array,
|
|
@@ -790,6 +801,37 @@ class DefaultSparkSigner implements SparkSigner {
|
|
|
790
801
|
const receiptProof = privateAdd(privateKey, pxhPubkey)!;
|
|
791
802
|
return Buffer.from(receiptProof);
|
|
792
803
|
}
|
|
804
|
+
|
|
805
|
+
signTransactionIndex(
|
|
806
|
+
tx: Transaction,
|
|
807
|
+
index: number,
|
|
808
|
+
publicKey: Uint8Array,
|
|
809
|
+
): void {
|
|
810
|
+
let privateKey: Uint8Array | undefined | null;
|
|
811
|
+
|
|
812
|
+
if (
|
|
813
|
+
equalBytes(publicKey, this.identityKey?.publicKey ?? new Uint8Array())
|
|
814
|
+
) {
|
|
815
|
+
privateKey = this.identityKey?.privateKey;
|
|
816
|
+
} else if (
|
|
817
|
+
equalBytes(publicKey, this.depositKey?.publicKey ?? new Uint8Array())
|
|
818
|
+
) {
|
|
819
|
+
privateKey = this.depositKey?.privateKey;
|
|
820
|
+
} else {
|
|
821
|
+
privateKey = hexToBytes(
|
|
822
|
+
this.publicKeyToPrivateKeyMap.get(bytesToHex(publicKey)) ?? "",
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!privateKey) {
|
|
827
|
+
throw new ValidationError("Private key not found for public key", {
|
|
828
|
+
field: "privateKey",
|
|
829
|
+
value: bytesToHex(publicKey),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
tx.signIdx(privateKey, index);
|
|
834
|
+
}
|
|
793
835
|
}
|
|
794
836
|
export { DefaultSparkSigner };
|
|
795
837
|
export type { SparkSigner };
|
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
} from "../utils/adaptor-signature.js";
|
|
73
73
|
import {
|
|
74
74
|
computeTaprootKeyNoScript,
|
|
75
|
+
getP2TRScriptFromPublicKey,
|
|
75
76
|
getP2WPKHAddressFromPublicKey,
|
|
76
77
|
getSigHashFromTx,
|
|
77
78
|
getTxFromRawTxBytes,
|
|
@@ -430,9 +431,7 @@ export class SparkWallet extends EventEmitter {
|
|
|
430
431
|
}
|
|
431
432
|
}
|
|
432
433
|
|
|
433
|
-
|
|
434
|
-
isBalanceCheck: boolean = false,
|
|
435
|
-
): Promise<TreeNode[]> {
|
|
434
|
+
public async getLeaves(isBalanceCheck: boolean = false): Promise<TreeNode[]> {
|
|
436
435
|
const leaves = await this.queryNodes({
|
|
437
436
|
source: {
|
|
438
437
|
$case: "ownerIdentityPubkey",
|
|
@@ -987,7 +986,7 @@ export class SparkWallet extends EventEmitter {
|
|
|
987
986
|
);
|
|
988
987
|
}
|
|
989
988
|
|
|
990
|
-
await this.transferService.
|
|
989
|
+
await this.transferService.deliverTransferPackage(
|
|
991
990
|
transfer,
|
|
992
991
|
leafKeyTweaks,
|
|
993
992
|
signatureMap,
|
|
@@ -3116,6 +3115,148 @@ export class SparkWallet extends EventEmitter {
|
|
|
3116
3115
|
return this.config.signer.validateMessageWithIdentityKey(hash, signature);
|
|
3117
3116
|
}
|
|
3118
3117
|
|
|
3118
|
+
/**
|
|
3119
|
+
* Signs a transaction with wallet keys.
|
|
3120
|
+
*
|
|
3121
|
+
* @param {string} txHex - The transaction hex to sign
|
|
3122
|
+
* @param {string} keyType - The type of key to use for signing ("identity", "deposit", or "auto-detect")
|
|
3123
|
+
* @returns {Promise<string>} The signed transaction hex
|
|
3124
|
+
*/
|
|
3125
|
+
public async signTransaction(
|
|
3126
|
+
txHex: string,
|
|
3127
|
+
keyType: string = "auto-detect",
|
|
3128
|
+
): Promise<string> {
|
|
3129
|
+
try {
|
|
3130
|
+
// Parse the transaction
|
|
3131
|
+
const tx = Transaction.fromRaw(hexToBytes(txHex));
|
|
3132
|
+
|
|
3133
|
+
let publicKey: Uint8Array;
|
|
3134
|
+
|
|
3135
|
+
switch (keyType.toLowerCase()) {
|
|
3136
|
+
case "identity":
|
|
3137
|
+
publicKey = await this.config.signer.getIdentityPublicKey();
|
|
3138
|
+
break;
|
|
3139
|
+
case "deposit":
|
|
3140
|
+
publicKey = await this.config.signer.getDepositSigningKey();
|
|
3141
|
+
break;
|
|
3142
|
+
case "auto-detect":
|
|
3143
|
+
default:
|
|
3144
|
+
// Try to auto-detect which key to use by examining the transaction inputs
|
|
3145
|
+
const detectedKey = await this.detectKeyForTransaction(tx);
|
|
3146
|
+
if (detectedKey) {
|
|
3147
|
+
publicKey = detectedKey.publicKey;
|
|
3148
|
+
} else {
|
|
3149
|
+
// Fallback to identity key
|
|
3150
|
+
publicKey = await this.config.signer.getIdentityPublicKey();
|
|
3151
|
+
}
|
|
3152
|
+
break;
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Check each input to determine which ones need signing
|
|
3156
|
+
let inputsSigned = 0;
|
|
3157
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
3158
|
+
const input = tx.getInput(i);
|
|
3159
|
+
if (!input?.witnessUtxo?.script) {
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
const script = input.witnessUtxo.script;
|
|
3164
|
+
|
|
3165
|
+
// Check if this is an ephemeral anchor (OP_TRUE script)
|
|
3166
|
+
// OP_TRUE is represented as a single byte: 0x51
|
|
3167
|
+
if (script.length === 1 && script[0] === 0x51) {
|
|
3168
|
+
continue;
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
// Check if this script matches one of our keys
|
|
3172
|
+
const identityScript = getP2TRScriptFromPublicKey(
|
|
3173
|
+
publicKey,
|
|
3174
|
+
this.config.getNetwork(),
|
|
3175
|
+
);
|
|
3176
|
+
|
|
3177
|
+
if (bytesToHex(script) === bytesToHex(identityScript)) {
|
|
3178
|
+
// Sign this specific input
|
|
3179
|
+
try {
|
|
3180
|
+
this.config.signer.signTransactionIndex(tx, i, publicKey);
|
|
3181
|
+
inputsSigned++;
|
|
3182
|
+
} catch (error) {
|
|
3183
|
+
throw new ValidationError(`Failed to sign input ${i}: ${error}`, {
|
|
3184
|
+
field: "input",
|
|
3185
|
+
value: i,
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
if (inputsSigned === 0) {
|
|
3192
|
+
throw new Error(
|
|
3193
|
+
"No inputs were signed. Check that the transaction contains inputs controlled by this wallet.",
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
tx.finalize();
|
|
3198
|
+
|
|
3199
|
+
const signedTxHex = tx.hex;
|
|
3200
|
+
|
|
3201
|
+
return signedTxHex;
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
console.error("❌ Error signing transaction:", error);
|
|
3204
|
+
throw error;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
/**
|
|
3209
|
+
* Helper method to auto-detect which key should be used for signing a transaction.
|
|
3210
|
+
*/
|
|
3211
|
+
private async detectKeyForTransaction(tx: Transaction): Promise<{
|
|
3212
|
+
publicKey: Uint8Array;
|
|
3213
|
+
keyType: string;
|
|
3214
|
+
} | null> {
|
|
3215
|
+
try {
|
|
3216
|
+
// Get available keys
|
|
3217
|
+
const identityPubKey = await this.config.signer.getIdentityPublicKey();
|
|
3218
|
+
const depositPubKey = await this.config.signer.getDepositSigningKey();
|
|
3219
|
+
|
|
3220
|
+
// Check if any inputs reference outputs that would be controlled by our keys
|
|
3221
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
3222
|
+
const input = tx.getInput(i);
|
|
3223
|
+
if (input?.witnessUtxo?.script) {
|
|
3224
|
+
const script = input.witnessUtxo.script;
|
|
3225
|
+
|
|
3226
|
+
// Check if this script corresponds to one of our keys
|
|
3227
|
+
// This is a simplified check - in practice, you might need more sophisticated script analysis
|
|
3228
|
+
const identityScript = getP2TRScriptFromPublicKey(
|
|
3229
|
+
identityPubKey,
|
|
3230
|
+
this.config.getNetwork(),
|
|
3231
|
+
);
|
|
3232
|
+
const depositScript = getP2TRScriptFromPublicKey(
|
|
3233
|
+
depositPubKey,
|
|
3234
|
+
this.config.getNetwork(),
|
|
3235
|
+
);
|
|
3236
|
+
|
|
3237
|
+
if (bytesToHex(script) === bytesToHex(identityScript)) {
|
|
3238
|
+
return {
|
|
3239
|
+
publicKey: identityPubKey,
|
|
3240
|
+
keyType: "identity",
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
if (bytesToHex(script) === bytesToHex(depositScript)) {
|
|
3245
|
+
return {
|
|
3246
|
+
publicKey: depositPubKey,
|
|
3247
|
+
keyType: "deposit",
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
return null;
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
console.warn("Error during key auto-detection:", error);
|
|
3256
|
+
return null;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3119
3260
|
/**
|
|
3120
3261
|
* Get a Lightning receive request by ID.
|
|
3121
3262
|
*
|
|
@@ -3153,6 +3294,291 @@ export class SparkWallet extends EventEmitter {
|
|
|
3153
3294
|
return await sspClient.getCoopExitRequest(id);
|
|
3154
3295
|
}
|
|
3155
3296
|
|
|
3297
|
+
/**
|
|
3298
|
+
* Check the remaining timelock on a given node.
|
|
3299
|
+
*
|
|
3300
|
+
* @param {string} nodeId - The ID of the node to check
|
|
3301
|
+
* @returns {Promise<{nodeTimelock: number, refundTimelock: number}>} The remaining timelocks in blocks for both node and refund transactions
|
|
3302
|
+
*/
|
|
3303
|
+
public async checkTimelock(nodeId: string): Promise<{
|
|
3304
|
+
nodeTimelock: number;
|
|
3305
|
+
refundTimelock: number;
|
|
3306
|
+
}> {
|
|
3307
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
3308
|
+
this.config.getCoordinatorAddress(),
|
|
3309
|
+
);
|
|
3310
|
+
|
|
3311
|
+
try {
|
|
3312
|
+
const response = await sparkClient.query_nodes({
|
|
3313
|
+
source: {
|
|
3314
|
+
$case: "nodeIds",
|
|
3315
|
+
nodeIds: {
|
|
3316
|
+
nodeIds: [nodeId],
|
|
3317
|
+
},
|
|
3318
|
+
},
|
|
3319
|
+
includeParents: false,
|
|
3320
|
+
network: NetworkToProto[this.config.getNetwork()],
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
const node = response.nodes[nodeId];
|
|
3324
|
+
if (!node) {
|
|
3325
|
+
throw new ValidationError("Node not found", {
|
|
3326
|
+
field: "nodeId",
|
|
3327
|
+
value: nodeId,
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// Check if this is a root node (no parent)
|
|
3332
|
+
const isRootNode = !node.parentNodeId;
|
|
3333
|
+
|
|
3334
|
+
// Validate transaction data exists
|
|
3335
|
+
if (!node.nodeTx || node.nodeTx.length === 0) {
|
|
3336
|
+
throw new ValidationError(
|
|
3337
|
+
`Node transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
|
|
3338
|
+
{
|
|
3339
|
+
field: "nodeTx",
|
|
3340
|
+
value: node.nodeTx?.length || 0,
|
|
3341
|
+
},
|
|
3342
|
+
);
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
if (!node.refundTx || node.refundTx.length === 0) {
|
|
3346
|
+
throw new ValidationError(
|
|
3347
|
+
`Refund transaction data is missing or empty for ${isRootNode ? "root" : "non-root"} node`,
|
|
3348
|
+
{
|
|
3349
|
+
field: "refundTx",
|
|
3350
|
+
value: node.refundTx?.length || 0,
|
|
3351
|
+
},
|
|
3352
|
+
);
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
let nodeTx, refundTx;
|
|
3356
|
+
|
|
3357
|
+
try {
|
|
3358
|
+
// Get the node transaction to check its timelock
|
|
3359
|
+
nodeTx = getTxFromRawTxBytes(node.nodeTx);
|
|
3360
|
+
} catch (error) {
|
|
3361
|
+
throw new ValidationError(
|
|
3362
|
+
`Failed to parse node transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
|
|
3363
|
+
{
|
|
3364
|
+
field: "nodeTx",
|
|
3365
|
+
value: node.nodeTx.length,
|
|
3366
|
+
},
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
try {
|
|
3371
|
+
// Get the refund transaction to check its timelock
|
|
3372
|
+
refundTx = getTxFromRawTxBytes(node.refundTx);
|
|
3373
|
+
} catch (error) {
|
|
3374
|
+
throw new ValidationError(
|
|
3375
|
+
`Failed to parse refund transaction for ${isRootNode ? "root" : "non-root"} node: ${error instanceof Error ? error.message : String(error)}`,
|
|
3376
|
+
{
|
|
3377
|
+
field: "refundTx",
|
|
3378
|
+
value: node.refundTx.length,
|
|
3379
|
+
},
|
|
3380
|
+
);
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
const nodeInput = nodeTx.getInput(0);
|
|
3384
|
+
if (!nodeInput) {
|
|
3385
|
+
throw new ValidationError(
|
|
3386
|
+
`Node transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
|
|
3387
|
+
{
|
|
3388
|
+
field: "nodeInput",
|
|
3389
|
+
value: nodeTx.inputsLength,
|
|
3390
|
+
},
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
if (!nodeInput.sequence) {
|
|
3395
|
+
throw new ValidationError(
|
|
3396
|
+
`Node transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
|
|
3397
|
+
{
|
|
3398
|
+
field: "sequence",
|
|
3399
|
+
value: nodeInput.sequence,
|
|
3400
|
+
},
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
const refundInput = refundTx.getInput(0);
|
|
3405
|
+
if (!refundInput) {
|
|
3406
|
+
throw new ValidationError(
|
|
3407
|
+
`Refund transaction has no inputs for ${isRootNode ? "root" : "non-root"} node`,
|
|
3408
|
+
{
|
|
3409
|
+
field: "refundInput",
|
|
3410
|
+
value: refundTx.inputsLength,
|
|
3411
|
+
},
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
|
|
3415
|
+
if (!refundInput.sequence) {
|
|
3416
|
+
throw new ValidationError(
|
|
3417
|
+
`Refund transaction has no sequence for ${isRootNode ? "root" : "non-root"} node`,
|
|
3418
|
+
{
|
|
3419
|
+
field: "sequence",
|
|
3420
|
+
value: refundInput.sequence,
|
|
3421
|
+
},
|
|
3422
|
+
);
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
// Extract timelock from sequence (lower 16 bits)
|
|
3426
|
+
const nodeTimelock = nodeInput.sequence & 0xffff;
|
|
3427
|
+
const refundTimelock = refundInput.sequence & 0xffff;
|
|
3428
|
+
|
|
3429
|
+
return {
|
|
3430
|
+
nodeTimelock,
|
|
3431
|
+
refundTimelock,
|
|
3432
|
+
};
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
throw new NetworkError(
|
|
3435
|
+
`Failed to check timelock for node ${nodeId}`,
|
|
3436
|
+
{
|
|
3437
|
+
method: "query_nodes",
|
|
3438
|
+
},
|
|
3439
|
+
error as Error,
|
|
3440
|
+
);
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
/**
|
|
3445
|
+
* Refresh the timelock of a specific node.
|
|
3446
|
+
*
|
|
3447
|
+
* @param {string} nodeId - The ID of the node to refresh
|
|
3448
|
+
* @returns {Promise<void>} Promise that resolves when the timelock is refreshed
|
|
3449
|
+
*/
|
|
3450
|
+
public async testOnly_expireTimelock(nodeId: string): Promise<void> {
|
|
3451
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
3452
|
+
this.config.getCoordinatorAddress(),
|
|
3453
|
+
);
|
|
3454
|
+
|
|
3455
|
+
try {
|
|
3456
|
+
// First, get the node and its parent
|
|
3457
|
+
const response = await sparkClient.query_nodes({
|
|
3458
|
+
source: {
|
|
3459
|
+
$case: "nodeIds",
|
|
3460
|
+
nodeIds: {
|
|
3461
|
+
nodeIds: [nodeId],
|
|
3462
|
+
},
|
|
3463
|
+
},
|
|
3464
|
+
includeParents: true,
|
|
3465
|
+
});
|
|
3466
|
+
|
|
3467
|
+
const node = response.nodes[nodeId];
|
|
3468
|
+
if (!node) {
|
|
3469
|
+
throw new ValidationError("Node not found", {
|
|
3470
|
+
field: "nodeId",
|
|
3471
|
+
value: nodeId,
|
|
3472
|
+
});
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
if (!node.parentNodeId) {
|
|
3476
|
+
throw new ValidationError("Node has no parent", {
|
|
3477
|
+
field: "parentNodeId",
|
|
3478
|
+
value: node.parentNodeId,
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
|
|
3482
|
+
const parentNode = response.nodes[node.parentNodeId];
|
|
3483
|
+
if (!parentNode) {
|
|
3484
|
+
throw new ValidationError("Parent node not found", {
|
|
3485
|
+
field: "parentNodeId",
|
|
3486
|
+
value: node.parentNodeId,
|
|
3487
|
+
});
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// Generate signing public key for this node
|
|
3491
|
+
const signingPubKey = await this.config.signer.generatePublicKey(
|
|
3492
|
+
sha256(node.id),
|
|
3493
|
+
);
|
|
3494
|
+
|
|
3495
|
+
// Call the transfer service to refresh the timelock
|
|
3496
|
+
const result = await this.transferService.refreshTimelockNodes(
|
|
3497
|
+
[node],
|
|
3498
|
+
parentNode,
|
|
3499
|
+
signingPubKey,
|
|
3500
|
+
);
|
|
3501
|
+
|
|
3502
|
+
// Update the local leaves if this node is in our wallet
|
|
3503
|
+
const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
|
|
3504
|
+
if (leafIndex !== -1 && result.nodes.length > 0) {
|
|
3505
|
+
const newNode = result.nodes[0];
|
|
3506
|
+
if (newNode) {
|
|
3507
|
+
this.leaves[leafIndex] = newNode;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
} catch (error) {
|
|
3511
|
+
throw new NetworkError(
|
|
3512
|
+
"Failed to refresh timelock",
|
|
3513
|
+
{
|
|
3514
|
+
method: "refresh_timelock",
|
|
3515
|
+
},
|
|
3516
|
+
error as Error,
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
/**
|
|
3522
|
+
* Refresh the timelock of a specific node's refund transaction only.
|
|
3523
|
+
*
|
|
3524
|
+
* @param {string} nodeId - The ID of the node whose refund transaction to refresh
|
|
3525
|
+
* @returns {Promise<void>} Promise that resolves when the refund timelock is refreshed
|
|
3526
|
+
*/
|
|
3527
|
+
public async testOnly_expireTimelockRefundTx(nodeId: string): Promise<void> {
|
|
3528
|
+
const sparkClient = await this.connectionManager.createSparkClient(
|
|
3529
|
+
this.config.getCoordinatorAddress(),
|
|
3530
|
+
);
|
|
3531
|
+
|
|
3532
|
+
try {
|
|
3533
|
+
// Get the node
|
|
3534
|
+
const response = await sparkClient.query_nodes({
|
|
3535
|
+
source: {
|
|
3536
|
+
$case: "nodeIds",
|
|
3537
|
+
nodeIds: {
|
|
3538
|
+
nodeIds: [nodeId],
|
|
3539
|
+
},
|
|
3540
|
+
},
|
|
3541
|
+
includeParents: false,
|
|
3542
|
+
});
|
|
3543
|
+
|
|
3544
|
+
const node = response.nodes[nodeId];
|
|
3545
|
+
if (!node) {
|
|
3546
|
+
throw new ValidationError("Node not found", {
|
|
3547
|
+
field: "nodeId",
|
|
3548
|
+
value: nodeId,
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
// Generate signing public key for this node
|
|
3553
|
+
const signingPubKey = await this.config.signer.generatePublicKey(
|
|
3554
|
+
sha256(node.id),
|
|
3555
|
+
);
|
|
3556
|
+
|
|
3557
|
+
// Call the transfer service to refresh the refund timelock
|
|
3558
|
+
const result = await this.transferService.refreshTimelockRefundTx(
|
|
3559
|
+
node,
|
|
3560
|
+
signingPubKey,
|
|
3561
|
+
);
|
|
3562
|
+
|
|
3563
|
+
// Update the local leaves if this node is in our wallet
|
|
3564
|
+
const leafIndex = this.leaves.findIndex((leaf) => leaf.id === node.id);
|
|
3565
|
+
if (leafIndex !== -1 && result.nodes.length > 0) {
|
|
3566
|
+
const newNode = result.nodes[0];
|
|
3567
|
+
if (newNode) {
|
|
3568
|
+
this.leaves[leafIndex] = newNode;
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
} catch (error) {
|
|
3572
|
+
throw new NetworkError(
|
|
3573
|
+
"Failed to refresh refund timelock",
|
|
3574
|
+
{
|
|
3575
|
+
method: "refresh_timelock_refund_tx",
|
|
3576
|
+
},
|
|
3577
|
+
error as Error,
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3156
3582
|
private cleanup() {
|
|
3157
3583
|
if (this.claimTransfersInterval) {
|
|
3158
3584
|
clearInterval(this.claimTransfersInterval);
|