@buildonspark/spark-sdk 0.1.43 → 0.1.44
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 +7 -0
- package/dist/{RequestLightningSendInput-Na1mHdWg.d.cts → RequestLightningSendInput-BxbCtwpV.d.cts} +43 -4
- package/dist/{RequestLightningSendInput-D7fZdT4A.d.ts → RequestLightningSendInput-RGel43ks.d.ts} +43 -4
- package/dist/address/index.d.cts +2 -2
- package/dist/address/index.d.ts +2 -2
- package/dist/address/index.js +2 -2
- package/dist/{chunk-M6A4KFIG.js → chunk-4Q2ZDYYU.js} +332 -221
- package/dist/{chunk-WWOTVNPP.js → chunk-A2ZLMH6I.js} +323 -142
- package/dist/{chunk-VA7MV4MZ.js → chunk-B3AMIGJG.js} +1 -1
- package/dist/{chunk-DQYKQJRZ.js → chunk-CIZNCBKE.js} +29 -9
- package/dist/{chunk-BUTZWYBW.js → chunk-DAXGVPVM.js} +2 -2
- package/dist/{chunk-DOA6QXYQ.js → chunk-EKFD62HN.js} +2 -1
- package/dist/{chunk-TIUBYNN5.js → chunk-HTMXTJRK.js} +1 -1
- package/dist/{chunk-TOSP3INR.js → chunk-I54FARY2.js} +4 -2
- package/dist/{chunk-MIVX3GHD.js → chunk-K4BJARWM.js} +1 -1
- package/dist/{chunk-O4RYNJNB.js → chunk-KEKGSH7B.js} +1 -1
- package/dist/{chunk-GYQR4B4P.js → chunk-NBCNYDWJ.js} +2 -2
- package/dist/{chunk-ABZA6R5S.js → chunk-SQKXGAIR.js} +1 -1
- package/dist/{chunk-VFJQNBFX.js → chunk-UBT6EDVJ.js} +5 -2
- package/dist/{chunk-HRQRRDSS.js → chunk-WPTRVD2V.js} +3 -3
- package/dist/{chunk-IRW5TWMH.js → chunk-XX4RRWOX.js} +5 -5
- package/dist/graphql/objects/index.d.cts +5 -43
- package/dist/graphql/objects/index.d.ts +5 -43
- package/dist/graphql/objects/index.js +1 -1
- package/dist/{index-BJOc8Ur-.d.cts → index-CZmDdSts.d.cts} +24 -10
- package/dist/{index-7RYRH5wc.d.ts → index-ClIRO_3y.d.ts} +24 -10
- package/dist/index.cjs +827 -414
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +15 -15
- package/dist/index.node.cjs +830 -417
- package/dist/index.node.d.cts +7 -7
- package/dist/index.node.d.ts +7 -7
- package/dist/index.node.js +18 -18
- package/dist/native/index.cjs +827 -414
- package/dist/native/index.d.cts +88 -23
- package/dist/native/index.d.ts +88 -23
- package/dist/native/index.js +819 -407
- package/dist/{network-D5lKssVl.d.cts → network-CfxLnaot.d.cts} +1 -1
- package/dist/{network-xkBSpaTn.d.ts → network-CroCOQ0B.d.ts} +1 -1
- package/dist/proto/lrc20.d.cts +1 -1
- package/dist/proto/lrc20.d.ts +1 -1
- package/dist/proto/lrc20.js +2 -2
- package/dist/proto/spark.cjs +332 -221
- package/dist/proto/spark.d.cts +1 -1
- package/dist/proto/spark.d.ts +1 -1
- package/dist/proto/spark.js +3 -5
- package/dist/proto/spark_token.cjs +149 -9
- 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/{sdk-types-B-q9py_P.d.cts → sdk-types-BeCBoozO.d.cts} +1 -1
- package/dist/{sdk-types-BPoPgzda.d.ts → sdk-types-CTbTdDbE.d.ts} +1 -1
- package/dist/services/config.cjs +7 -3
- 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.cjs +334 -218
- package/dist/services/connection.d.cts +4 -4
- package/dist/services/connection.d.ts +4 -4
- package/dist/services/connection.js +4 -4
- package/dist/services/index.cjs +364 -227
- package/dist/services/index.d.cts +4 -4
- package/dist/services/index.d.ts +4 -4
- package/dist/services/index.js +9 -9
- package/dist/services/lrc-connection.cjs +5 -2
- package/dist/services/lrc-connection.d.cts +4 -4
- package/dist/services/lrc-connection.d.ts +4 -4
- package/dist/services/lrc-connection.js +4 -4
- package/dist/services/token-transactions.cjs +28 -8
- 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.cjs +2 -1
- package/dist/services/wallet-config.d.cts +4 -4
- package/dist/services/wallet-config.d.ts +4 -4
- package/dist/services/wallet-config.js +1 -1
- package/dist/signer/signer.cjs +5 -2
- package/dist/signer/signer.d.cts +2 -2
- package/dist/signer/signer.d.ts +2 -2
- package/dist/signer/signer.js +2 -2
- package/dist/{signer-wqesWifN.d.ts → signer-D7vfYik9.d.ts} +1 -1
- package/dist/{signer-IO3oMRNj.d.cts → signer-DaY8c60s.d.cts} +1 -1
- package/dist/{spark-CDm4gqS6.d.ts → spark-C4ZrsgjC.d.cts} +47 -29
- package/dist/{spark-CDm4gqS6.d.cts → spark-C4ZrsgjC.d.ts} +47 -29
- package/dist/types/index.cjs +331 -219
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.js +3 -3
- package/dist/utils/index.cjs +298 -28
- package/dist/utils/index.d.cts +5 -5
- package/dist/utils/index.d.ts +5 -5
- package/dist/utils/index.js +6 -6
- package/package.json +1 -1
- package/src/constants.ts +5 -1
- package/src/graphql/client.ts +28 -0
- package/src/graphql/mutations/RequestCoopExit.ts +6 -0
- package/src/graphql/mutations/RequestSwapLeaves.ts +2 -0
- package/src/graphql/queries/GetCoopExitFeeQuote.ts +20 -0
- package/src/proto/spark.ts +454 -322
- package/src/services/token-transactions.ts +12 -0
- package/src/services/transfer.ts +0 -3
- package/src/services/wallet-config.ts +1 -0
- package/src/spark-wallet/spark-wallet.node.ts +3 -3
- package/src/spark-wallet/spark-wallet.ts +301 -125
- package/src/tests/integration/deposit.test.ts +16 -0
- package/src/tests/integration/ssp/coop-exit.test.ts +85 -21
- package/src/tests/integration/ssp/swap.test.ts +47 -0
- package/src/tests/tokens.test.ts +3 -2
- package/src/utils/token-hashing.ts +28 -10
- package/src/utils/transaction.ts +2 -2
- package/src/logger.ts +0 -3
|
@@ -23,7 +23,7 @@ import SspClient from "../graphql/client.js";
|
|
|
23
23
|
import {
|
|
24
24
|
BitcoinNetwork,
|
|
25
25
|
ClaimStaticDepositOutput,
|
|
26
|
-
|
|
26
|
+
CoopExitFeeQuote,
|
|
27
27
|
CoopExitRequest,
|
|
28
28
|
ExitSpeed,
|
|
29
29
|
LeavesSwapFeeEstimateOutput,
|
|
@@ -31,8 +31,8 @@ import {
|
|
|
31
31
|
LightningReceiveRequest,
|
|
32
32
|
LightningSendFeeEstimateInput,
|
|
33
33
|
LightningSendRequest,
|
|
34
|
+
RequestCoopExitInput,
|
|
34
35
|
StaticDepositQuoteOutput,
|
|
35
|
-
SwapLeaf,
|
|
36
36
|
UserLeafInput,
|
|
37
37
|
} from "../graphql/objects/index.js";
|
|
38
38
|
import GraphQLTransferObj from "../graphql/objects/Transfer.js";
|
|
@@ -102,7 +102,7 @@ import {
|
|
|
102
102
|
SparkAddressFormat,
|
|
103
103
|
} from "../address/index.js";
|
|
104
104
|
import { isReactNative } from "../constants.js";
|
|
105
|
-
import { networkToJSON } from "../proto/spark.js";
|
|
105
|
+
import { networkToJSON, Network as NetworkProto } from "../proto/spark.js";
|
|
106
106
|
import {
|
|
107
107
|
decodeInvoice,
|
|
108
108
|
getNetworkFromInvoice,
|
|
@@ -524,14 +524,40 @@ export class SparkWallet extends EventEmitter {
|
|
|
524
524
|
.map(([_, node]) => node);
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
private async selectLeaves(
|
|
528
|
-
|
|
527
|
+
private async selectLeaves(
|
|
528
|
+
targetAmounts: number[],
|
|
529
|
+
): Promise<Map<number, TreeNode[]>> {
|
|
530
|
+
if (targetAmounts.length === 0) {
|
|
531
|
+
throw new ValidationError("Target amounts must be non-empty", {
|
|
532
|
+
field: "targetAmounts",
|
|
533
|
+
value: targetAmounts,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (targetAmounts.some((amount) => amount <= 0)) {
|
|
529
538
|
throw new ValidationError("Target amount must be positive", {
|
|
530
|
-
field: "
|
|
531
|
-
value:
|
|
539
|
+
field: "targetAmounts",
|
|
540
|
+
value: targetAmounts,
|
|
532
541
|
});
|
|
533
542
|
}
|
|
534
543
|
|
|
544
|
+
const totalTargetAmount = targetAmounts.reduce(
|
|
545
|
+
(acc, amount) => acc + amount,
|
|
546
|
+
0,
|
|
547
|
+
);
|
|
548
|
+
const totalBalance = this.getInternalBalance();
|
|
549
|
+
|
|
550
|
+
if (totalTargetAmount > totalBalance) {
|
|
551
|
+
throw new ValidationError(
|
|
552
|
+
"Total target amount exceeds available balance",
|
|
553
|
+
{
|
|
554
|
+
field: "targetAmounts",
|
|
555
|
+
value: totalTargetAmount,
|
|
556
|
+
expected: `less than or equal to ${totalBalance}`,
|
|
557
|
+
},
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
535
561
|
const leaves = await this.getLeaves();
|
|
536
562
|
if (leaves.length === 0) {
|
|
537
563
|
throw new ValidationError("No owned leaves found", {
|
|
@@ -541,37 +567,63 @@ export class SparkWallet extends EventEmitter {
|
|
|
541
567
|
|
|
542
568
|
leaves.sort((a, b) => b.value - a.value);
|
|
543
569
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
}
|
|
570
|
+
const selectLeavesForTargets = (
|
|
571
|
+
targetAmounts: number[],
|
|
572
|
+
leaves: TreeNode[],
|
|
573
|
+
) => {
|
|
574
|
+
const usedLeaves = new Set<string>();
|
|
575
|
+
const results: Map<number, TreeNode[]> = new Map();
|
|
576
|
+
let totalAmount = 0;
|
|
552
577
|
|
|
553
|
-
|
|
554
|
-
|
|
578
|
+
for (const targetAmount of targetAmounts) {
|
|
579
|
+
const nodes: TreeNode[] = [];
|
|
580
|
+
let amount = 0;
|
|
555
581
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
582
|
+
for (const leaf of leaves) {
|
|
583
|
+
if (usedLeaves.has(leaf.id)) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (targetAmount - amount >= leaf.value) {
|
|
588
|
+
amount += leaf.value;
|
|
589
|
+
nodes.push(leaf);
|
|
590
|
+
usedLeaves.add(leaf.id);
|
|
591
|
+
}
|
|
564
592
|
}
|
|
593
|
+
|
|
594
|
+
totalAmount += amount;
|
|
595
|
+
results.set(targetAmount, nodes);
|
|
565
596
|
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
results,
|
|
600
|
+
foundSelections: totalAmount === totalTargetAmount,
|
|
601
|
+
};
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
let { results, foundSelections } = selectLeavesForTargets(
|
|
605
|
+
targetAmounts,
|
|
606
|
+
leaves,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
if (!foundSelections) {
|
|
610
|
+
const newLeaves = await this.requestLeavesSwap({ targetAmounts });
|
|
611
|
+
|
|
612
|
+
newLeaves.sort((a, b) => b.value - a.value);
|
|
613
|
+
|
|
614
|
+
({ results, foundSelections } = selectLeavesForTargets(
|
|
615
|
+
targetAmounts,
|
|
616
|
+
newLeaves,
|
|
617
|
+
));
|
|
566
618
|
}
|
|
567
619
|
|
|
568
|
-
if (
|
|
620
|
+
if (!foundSelections) {
|
|
569
621
|
throw new Error(
|
|
570
|
-
`Failed to select leaves for target amount ${
|
|
622
|
+
`Failed to select leaves for target amount ${totalTargetAmount}`,
|
|
571
623
|
);
|
|
572
624
|
}
|
|
573
625
|
|
|
574
|
-
return
|
|
626
|
+
return results;
|
|
575
627
|
}
|
|
576
628
|
|
|
577
629
|
private async selectLeavesForSwap(targetAmount: number) {
|
|
@@ -853,32 +905,57 @@ export class SparkWallet extends EventEmitter {
|
|
|
853
905
|
* @private
|
|
854
906
|
*/
|
|
855
907
|
private async requestLeavesSwap({
|
|
856
|
-
|
|
908
|
+
targetAmounts,
|
|
857
909
|
leaves,
|
|
858
910
|
}: {
|
|
859
|
-
|
|
911
|
+
targetAmounts?: number[];
|
|
860
912
|
leaves?: TreeNode[];
|
|
861
|
-
}) {
|
|
862
|
-
if (
|
|
863
|
-
throw new Error("targetAmount must be positive");
|
|
913
|
+
}): Promise<TreeNode[]> {
|
|
914
|
+
if (targetAmounts && targetAmounts.some((amount) => amount <= 0)) {
|
|
915
|
+
throw new Error("specified targetAmount must be positive");
|
|
864
916
|
}
|
|
865
917
|
|
|
866
|
-
if (
|
|
918
|
+
if (
|
|
919
|
+
targetAmounts &&
|
|
920
|
+
targetAmounts.some((amount) => !Number.isSafeInteger(amount))
|
|
921
|
+
) {
|
|
867
922
|
throw new ValidationError("targetAmount must be less than 2^53", {
|
|
868
|
-
field: "
|
|
869
|
-
value:
|
|
923
|
+
field: "targetAmounts",
|
|
924
|
+
value: targetAmounts,
|
|
870
925
|
expected: "smaller or equal to " + Number.MAX_SAFE_INTEGER,
|
|
871
926
|
});
|
|
872
927
|
}
|
|
873
928
|
|
|
874
929
|
let leavesToSwap: TreeNode[];
|
|
875
|
-
|
|
876
|
-
|
|
930
|
+
const totalTargetAmount = targetAmounts?.reduce(
|
|
931
|
+
(acc, amount) => acc + amount,
|
|
932
|
+
0,
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
if (totalTargetAmount) {
|
|
936
|
+
const totalBalance = this.getInternalBalance();
|
|
937
|
+
|
|
938
|
+
if (totalTargetAmount > totalBalance) {
|
|
939
|
+
throw new ValidationError(
|
|
940
|
+
"Total target amount exceeds available balance",
|
|
941
|
+
{
|
|
942
|
+
field: "targetAmounts",
|
|
943
|
+
value: totalTargetAmount,
|
|
944
|
+
expected: `less than or equal to ${totalBalance}`,
|
|
945
|
+
},
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (totalTargetAmount && leaves && leaves.length > 0) {
|
|
951
|
+
if (
|
|
952
|
+
totalTargetAmount < leaves.reduce((acc, leaf) => acc + leaf.value, 0)
|
|
953
|
+
) {
|
|
877
954
|
throw new Error("targetAmount is less than the sum of leaves");
|
|
878
955
|
}
|
|
879
956
|
leavesToSwap = leaves;
|
|
880
|
-
} else if (
|
|
881
|
-
leavesToSwap = await this.selectLeavesForSwap(
|
|
957
|
+
} else if (totalTargetAmount) {
|
|
958
|
+
leavesToSwap = await this.selectLeavesForSwap(totalTargetAmount);
|
|
882
959
|
} else if (leaves && leaves.length > 0) {
|
|
883
960
|
leavesToSwap = leaves;
|
|
884
961
|
} else {
|
|
@@ -889,10 +966,10 @@ export class SparkWallet extends EventEmitter {
|
|
|
889
966
|
|
|
890
967
|
const batches = chunkArray(leavesToSwap, 100);
|
|
891
968
|
|
|
892
|
-
const results:
|
|
969
|
+
const results: TreeNode[] = [];
|
|
893
970
|
for (const batch of batches) {
|
|
894
|
-
const result = await this.processSwapBatch(batch,
|
|
895
|
-
results.push(...result
|
|
971
|
+
const result = await this.processSwapBatch(batch, targetAmounts);
|
|
972
|
+
results.push(...result);
|
|
896
973
|
}
|
|
897
974
|
|
|
898
975
|
return results;
|
|
@@ -903,8 +980,8 @@ export class SparkWallet extends EventEmitter {
|
|
|
903
980
|
*/
|
|
904
981
|
private async processSwapBatch(
|
|
905
982
|
leavesBatch: TreeNode[],
|
|
906
|
-
|
|
907
|
-
): Promise<
|
|
983
|
+
targetAmounts?: number[],
|
|
984
|
+
): Promise<TreeNode[]> {
|
|
908
985
|
const leafKeyTweaks = await Promise.all(
|
|
909
986
|
leavesBatch.map(async (leaf) => ({
|
|
910
987
|
leaf,
|
|
@@ -981,9 +1058,10 @@ export class SparkWallet extends EventEmitter {
|
|
|
981
1058
|
userLeaves,
|
|
982
1059
|
adaptorPubkey,
|
|
983
1060
|
targetAmountSats:
|
|
984
|
-
|
|
1061
|
+
targetAmounts?.reduce((acc, amount) => acc + amount, 0) ||
|
|
985
1062
|
leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0),
|
|
986
1063
|
totalAmountSats: leavesBatch.reduce((acc, leaf) => acc + leaf.value, 0),
|
|
1064
|
+
targetAmountSatsList: targetAmounts,
|
|
987
1065
|
// TODO: Request fee from SSP
|
|
988
1066
|
feeSats: 0,
|
|
989
1067
|
idempotencyKey: uuidv7(),
|
|
@@ -1051,13 +1129,24 @@ export class SparkWallet extends EventEmitter {
|
|
|
1051
1129
|
leavesSwapRequestId: request.id,
|
|
1052
1130
|
});
|
|
1053
1131
|
|
|
1054
|
-
if (!completeResponse) {
|
|
1132
|
+
if (!completeResponse || !completeResponse.inboundTransfer?.sparkId) {
|
|
1055
1133
|
throw new Error("Failed to complete leaves swap");
|
|
1056
1134
|
}
|
|
1057
1135
|
|
|
1058
|
-
await this.
|
|
1136
|
+
const incomingTransfer = await this.transferService.queryTransfer(
|
|
1137
|
+
completeResponse.inboundTransfer.sparkId,
|
|
1138
|
+
);
|
|
1139
|
+
|
|
1140
|
+
if (!incomingTransfer) {
|
|
1141
|
+
throw new Error("Failed to get incoming transfer");
|
|
1142
|
+
}
|
|
1059
1143
|
|
|
1060
|
-
return
|
|
1144
|
+
return await this.claimTransfer({
|
|
1145
|
+
transfer: incomingTransfer,
|
|
1146
|
+
emit: false,
|
|
1147
|
+
retryCount: 0,
|
|
1148
|
+
optimize: false,
|
|
1149
|
+
});
|
|
1061
1150
|
} catch (e) {
|
|
1062
1151
|
await this.cancelAllSenderInitiatedTransfers();
|
|
1063
1152
|
throw new Error(`Failed to request leaves swap: ${e}`);
|
|
@@ -1750,16 +1839,60 @@ export class SparkWallet extends EventEmitter {
|
|
|
1750
1839
|
* @returns {Promise<string[]>} The unused deposit addresses
|
|
1751
1840
|
*/
|
|
1752
1841
|
public async getUnusedDepositAddresses(): Promise<string[]> {
|
|
1842
|
+
return (await this.queryAllUnusedDepositAddresses({})).map(
|
|
1843
|
+
(addr) => addr.depositAddress,
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/**
|
|
1848
|
+
* Gets all unused deposit addresses for the wallet.
|
|
1849
|
+
*
|
|
1850
|
+
* @param {Object} params - Parameters for querying unused deposit addresses
|
|
1851
|
+
* @param {Uint8Array<ArrayBufferLike>} [params.identityPublicKey] - The identity public key
|
|
1852
|
+
* @param {NetworkProto} [params.network] - The network
|
|
1853
|
+
* @returns {Promise<DepositAddressQueryResult[]>} The unused deposit addresses
|
|
1854
|
+
*/
|
|
1855
|
+
private async queryAllUnusedDepositAddresses({
|
|
1856
|
+
identityPublicKey,
|
|
1857
|
+
network,
|
|
1858
|
+
}: {
|
|
1859
|
+
identityPublicKey?: Uint8Array<ArrayBufferLike>;
|
|
1860
|
+
network?: NetworkProto | undefined;
|
|
1861
|
+
}): Promise<DepositAddressQueryResult[]> {
|
|
1753
1862
|
const sparkClient = await this.connectionManager.createSparkClient(
|
|
1754
1863
|
this.config.getCoordinatorAddress(),
|
|
1755
1864
|
);
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1865
|
+
|
|
1866
|
+
let limit = 100;
|
|
1867
|
+
let offset = 0;
|
|
1868
|
+
const pastOffsets = new Set<number>();
|
|
1869
|
+
const depositAddresses: DepositAddressQueryResult[] = [];
|
|
1870
|
+
|
|
1871
|
+
while (offset >= 0) {
|
|
1872
|
+
// Prevent infinite loop in case error with coordinator
|
|
1873
|
+
if (pastOffsets.has(offset)) {
|
|
1874
|
+
console.warn("Offset has already been seen, stopping");
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
const response = await sparkClient.query_unused_deposit_addresses({
|
|
1879
|
+
identityPublicKey:
|
|
1880
|
+
identityPublicKey ??
|
|
1881
|
+
(await this.config.signer.getIdentityPublicKey()),
|
|
1882
|
+
network: network ?? NetworkToProto[this.config.getNetwork()],
|
|
1883
|
+
limit,
|
|
1884
|
+
offset,
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
depositAddresses.push(...response.depositAddresses);
|
|
1888
|
+
|
|
1889
|
+
pastOffsets.add(offset);
|
|
1890
|
+
offset = response.offset;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
return depositAddresses;
|
|
1762
1894
|
}
|
|
1895
|
+
|
|
1763
1896
|
/**
|
|
1764
1897
|
* Claims a deposit to the wallet.
|
|
1765
1898
|
* Note that if you used advancedDeposit, you don't need to call this function.
|
|
@@ -1816,19 +1949,15 @@ export class SparkWallet extends EventEmitter {
|
|
|
1816
1949
|
}
|
|
1817
1950
|
const depositTx = getTxFromRawTxHex(txHex);
|
|
1818
1951
|
|
|
1819
|
-
const sparkClient = await this.connectionManager.createSparkClient(
|
|
1820
|
-
this.config.getCoordinatorAddress(),
|
|
1821
|
-
);
|
|
1822
|
-
|
|
1823
1952
|
const unusedDepositAddresses: Map<string, DepositAddressQueryResult> =
|
|
1824
1953
|
new Map(
|
|
1825
1954
|
(
|
|
1826
|
-
await
|
|
1955
|
+
await this.queryAllUnusedDepositAddresses({
|
|
1827
1956
|
identityPublicKey:
|
|
1828
1957
|
await this.config.signer.getIdentityPublicKey(),
|
|
1829
1958
|
network: NetworkToProto[this.config.getNetwork()],
|
|
1830
1959
|
})
|
|
1831
|
-
).
|
|
1960
|
+
).map((addr) => [addr.depositAddress, addr]),
|
|
1832
1961
|
);
|
|
1833
1962
|
let depositAddress: DepositAddressQueryResult | undefined;
|
|
1834
1963
|
let vout = 0;
|
|
@@ -1888,17 +2017,15 @@ export class SparkWallet extends EventEmitter {
|
|
|
1888
2017
|
*/
|
|
1889
2018
|
public async advancedDeposit(txHex: string) {
|
|
1890
2019
|
const depositTx = getTxFromRawTxHex(txHex);
|
|
1891
|
-
|
|
1892
|
-
this.config.getCoordinatorAddress(),
|
|
1893
|
-
);
|
|
2020
|
+
|
|
1894
2021
|
const unusedDepositAddresses: Map<string, DepositAddressQueryResult> =
|
|
1895
2022
|
new Map(
|
|
1896
2023
|
(
|
|
1897
|
-
await
|
|
2024
|
+
await this.queryAllUnusedDepositAddresses({
|
|
1898
2025
|
identityPublicKey: await this.config.signer.getIdentityPublicKey(),
|
|
1899
2026
|
network: NetworkToProto[this.config.getNetwork()],
|
|
1900
2027
|
})
|
|
1901
|
-
).
|
|
2028
|
+
).map((addr) => [addr.depositAddress, addr]),
|
|
1902
2029
|
);
|
|
1903
2030
|
|
|
1904
2031
|
let vout = 0;
|
|
@@ -2023,7 +2150,9 @@ export class SparkWallet extends EventEmitter {
|
|
|
2023
2150
|
);
|
|
2024
2151
|
|
|
2025
2152
|
return await this.withLeaves(async () => {
|
|
2026
|
-
let leavesToSend = await this.selectLeaves(amountSats)
|
|
2153
|
+
let leavesToSend = (await this.selectLeaves([amountSats])).get(
|
|
2154
|
+
amountSats,
|
|
2155
|
+
)!;
|
|
2027
2156
|
|
|
2028
2157
|
leavesToSend = await this.checkRefreshTimelockNodes(leavesToSend);
|
|
2029
2158
|
leavesToSend = await this.checkExtendTimeLockNodes(leavesToSend);
|
|
@@ -2325,7 +2454,7 @@ export class SparkWallet extends EventEmitter {
|
|
|
2325
2454
|
transfer.status !==
|
|
2326
2455
|
TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAKED &&
|
|
2327
2456
|
transfer.status !==
|
|
2328
|
-
TransferStatus.
|
|
2457
|
+
TransferStatus.TRANSFER_STATUS_RECEIVER_REFUND_SIGNED &&
|
|
2329
2458
|
transfer.status !==
|
|
2330
2459
|
TransferStatus.TRANSFER_STATUS_RECEIVER_KEY_TWEAK_APPLIED
|
|
2331
2460
|
) {
|
|
@@ -2648,7 +2777,7 @@ export class SparkWallet extends EventEmitter {
|
|
|
2648
2777
|
});
|
|
2649
2778
|
}
|
|
2650
2779
|
|
|
2651
|
-
let leaves = await this.selectLeaves(totalAmount)
|
|
2780
|
+
let leaves = (await this.selectLeaves([totalAmount])).get(totalAmount)!;
|
|
2652
2781
|
|
|
2653
2782
|
leaves = await this.checkRefreshTimelockNodes(leaves);
|
|
2654
2783
|
leaves = await this.checkExtendTimeLockNodes(leaves);
|
|
@@ -2786,17 +2915,24 @@ export class SparkWallet extends EventEmitter {
|
|
|
2786
2915
|
*
|
|
2787
2916
|
* @param {Object} params - Parameters for the withdrawal
|
|
2788
2917
|
* @param {string} params.onchainAddress - The Bitcoin address where the funds should be sent
|
|
2789
|
-
* @param {
|
|
2918
|
+
* @param {CoopExitFeeQuote} params.feeQuote - The fee quote for the withdrawal
|
|
2919
|
+
* @param {ExitSpeed} params.exitSpeed - The exit speed chosen for the withdrawal
|
|
2920
|
+
* @param {number} [params.amountSats] - The amount in satoshis to withdraw. If not specified, attempts to withdraw all available funds and deductFeeFromWithdrawalAmount is set to true.
|
|
2921
|
+
* @param {boolean} [params.deductFeeFromWithdrawalAmount] - Controls how the withdrawal fee is handled. If true, the fee is deducted from the withdrawal amount (amountSats), meaning the recipient will receive amountSats minus the fee. If false, the fee is paid separately from the wallet balance, meaning the recipient will receive the full amountSats.
|
|
2790
2922
|
* @returns {Promise<CoopExitRequest | null | undefined>} The withdrawal request details, or null/undefined if the request cannot be completed
|
|
2791
2923
|
*/
|
|
2792
2924
|
public async withdraw({
|
|
2793
2925
|
onchainAddress,
|
|
2794
2926
|
exitSpeed,
|
|
2927
|
+
feeQuote,
|
|
2795
2928
|
amountSats,
|
|
2929
|
+
deductFeeFromWithdrawalAmount = true,
|
|
2796
2930
|
}: {
|
|
2797
2931
|
onchainAddress: string;
|
|
2798
2932
|
exitSpeed: ExitSpeed;
|
|
2933
|
+
feeQuote: CoopExitFeeQuote;
|
|
2799
2934
|
amountSats?: number;
|
|
2935
|
+
deductFeeFromWithdrawalAmount?: boolean;
|
|
2800
2936
|
}) {
|
|
2801
2937
|
if (!Number.isSafeInteger(amountSats)) {
|
|
2802
2938
|
throw new ValidationError("Sats amount must be less than 2^53", {
|
|
@@ -2806,7 +2942,13 @@ export class SparkWallet extends EventEmitter {
|
|
|
2806
2942
|
});
|
|
2807
2943
|
}
|
|
2808
2944
|
return await this.withLeaves(async () => {
|
|
2809
|
-
return await this.coopExit(
|
|
2945
|
+
return await this.coopExit(
|
|
2946
|
+
onchainAddress,
|
|
2947
|
+
feeQuote,
|
|
2948
|
+
exitSpeed,
|
|
2949
|
+
deductFeeFromWithdrawalAmount,
|
|
2950
|
+
amountSats,
|
|
2951
|
+
);
|
|
2810
2952
|
});
|
|
2811
2953
|
}
|
|
2812
2954
|
|
|
@@ -2820,7 +2962,9 @@ export class SparkWallet extends EventEmitter {
|
|
|
2820
2962
|
*/
|
|
2821
2963
|
private async coopExit(
|
|
2822
2964
|
onchainAddress: string,
|
|
2965
|
+
feeEstimate: CoopExitFeeQuote,
|
|
2823
2966
|
exitSpeed: ExitSpeed,
|
|
2967
|
+
deductFeeFromWithdrawalAmount: boolean,
|
|
2824
2968
|
targetAmountSats?: number,
|
|
2825
2969
|
) {
|
|
2826
2970
|
if (!Number.isSafeInteger(targetAmountSats)) {
|
|
@@ -2831,53 +2975,46 @@ export class SparkWallet extends EventEmitter {
|
|
|
2831
2975
|
});
|
|
2832
2976
|
}
|
|
2833
2977
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
leavesToSend = await this.selectLeaves(targetAmountSats);
|
|
2837
|
-
} else {
|
|
2838
|
-
leavesToSend = this.leaves.map((leaf) => ({
|
|
2839
|
-
...leaf,
|
|
2840
|
-
}));
|
|
2978
|
+
if (!targetAmountSats) {
|
|
2979
|
+
deductFeeFromWithdrawalAmount = true;
|
|
2841
2980
|
}
|
|
2842
2981
|
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2982
|
+
let fee: number | undefined;
|
|
2983
|
+
switch (exitSpeed) {
|
|
2984
|
+
case ExitSpeed.FAST:
|
|
2985
|
+
fee =
|
|
2986
|
+
(feeEstimate.l1BroadcastFeeFast?.originalValue || 0) +
|
|
2987
|
+
(feeEstimate.userFeeFast?.originalValue || 0);
|
|
2988
|
+
break;
|
|
2989
|
+
case ExitSpeed.MEDIUM:
|
|
2990
|
+
fee =
|
|
2991
|
+
(feeEstimate.l1BroadcastFeeMedium?.originalValue || 0) +
|
|
2992
|
+
(feeEstimate.userFeeMedium?.originalValue || 0);
|
|
2993
|
+
break;
|
|
2994
|
+
case ExitSpeed.SLOW:
|
|
2995
|
+
fee =
|
|
2996
|
+
(feeEstimate.l1BroadcastFeeSlow?.originalValue || 0) +
|
|
2997
|
+
(feeEstimate.userFeeSlow?.originalValue || 0);
|
|
2998
|
+
break;
|
|
2999
|
+
default:
|
|
3000
|
+
throw new ValidationError("Invalid exit speed", {
|
|
3001
|
+
field: "exitSpeed",
|
|
3002
|
+
value: exitSpeed,
|
|
3003
|
+
expected: "FAST, MEDIUM, or SLOW",
|
|
3004
|
+
});
|
|
3005
|
+
}
|
|
2848
3006
|
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
switch (exitSpeed) {
|
|
2852
|
-
case ExitSpeed.FAST:
|
|
2853
|
-
fee =
|
|
2854
|
-
(feeEstimate.speedFast?.l1BroadcastFee.originalValue || 0) +
|
|
2855
|
-
(feeEstimate.speedFast?.userFee.originalValue || 0);
|
|
2856
|
-
break;
|
|
2857
|
-
case ExitSpeed.MEDIUM:
|
|
2858
|
-
fee =
|
|
2859
|
-
(feeEstimate.speedMedium?.l1BroadcastFee.originalValue || 0) +
|
|
2860
|
-
(feeEstimate.speedMedium?.userFee.originalValue || 0);
|
|
2861
|
-
break;
|
|
2862
|
-
case ExitSpeed.SLOW:
|
|
2863
|
-
fee =
|
|
2864
|
-
(feeEstimate.speedSlow?.l1BroadcastFee.originalValue || 0) +
|
|
2865
|
-
(feeEstimate.speedSlow?.userFee.originalValue || 0);
|
|
2866
|
-
break;
|
|
2867
|
-
default:
|
|
2868
|
-
throw new ValidationError("Invalid exit speed", {
|
|
2869
|
-
field: "exitSpeed",
|
|
2870
|
-
value: exitSpeed,
|
|
2871
|
-
expected: "FAST, MEDIUM, or SLOW",
|
|
2872
|
-
});
|
|
2873
|
-
}
|
|
3007
|
+
let leavesToSendToSsp: TreeNode[] = [];
|
|
3008
|
+
let leavesToSendToSE: TreeNode[] = [];
|
|
2874
3009
|
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
3010
|
+
if (deductFeeFromWithdrawalAmount) {
|
|
3011
|
+
leavesToSendToSsp = targetAmountSats
|
|
3012
|
+
? (await this.selectLeaves([targetAmountSats])).get(targetAmountSats)!
|
|
3013
|
+
: this.leaves;
|
|
3014
|
+
|
|
3015
|
+
if (fee > leavesToSendToSsp.reduce((acc, leaf) => acc + leaf.value, 0)) {
|
|
2879
3016
|
throw new ValidationError(
|
|
2880
|
-
"The fee for the withdrawal is greater than the target amount",
|
|
3017
|
+
"The fee for the withdrawal is greater than the target withdrawal amount",
|
|
2881
3018
|
{
|
|
2882
3019
|
field: "fee",
|
|
2883
3020
|
value: fee,
|
|
@@ -2885,12 +3022,38 @@ export class SparkWallet extends EventEmitter {
|
|
|
2885
3022
|
},
|
|
2886
3023
|
);
|
|
2887
3024
|
}
|
|
3025
|
+
} else {
|
|
3026
|
+
if (!targetAmountSats) {
|
|
3027
|
+
throw new ValidationError(
|
|
3028
|
+
"targetAmountSats is required when deductFeeFromWithdrawalAmount is false",
|
|
3029
|
+
{
|
|
3030
|
+
field: "targetAmountSats",
|
|
3031
|
+
value: targetAmountSats,
|
|
3032
|
+
expected: "defined when deductFeeFromWithdrawalAmount is false",
|
|
3033
|
+
},
|
|
3034
|
+
);
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
const leaves = await this.selectLeaves([targetAmountSats, fee]);
|
|
3038
|
+
|
|
3039
|
+
const leavesForTargetAmount = leaves.get(targetAmountSats);
|
|
3040
|
+
const leavesForFee = leaves.get(fee);
|
|
3041
|
+
|
|
3042
|
+
if (!leavesForTargetAmount || !leavesForFee) {
|
|
3043
|
+
throw new Error("Failed to select leaves for target amount and fee");
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
leavesToSendToSsp = leavesForTargetAmount;
|
|
3047
|
+
leavesToSendToSE = leavesForFee;
|
|
2888
3048
|
}
|
|
2889
|
-
|
|
2890
|
-
|
|
3049
|
+
|
|
3050
|
+
leavesToSendToSsp = await this.checkRefreshTimelockNodes(leavesToSendToSsp);
|
|
3051
|
+
leavesToSendToSsp = await this.checkExtendTimeLockNodes(leavesToSendToSsp);
|
|
3052
|
+
leavesToSendToSE = await this.checkRefreshTimelockNodes(leavesToSendToSE);
|
|
3053
|
+
leavesToSendToSE = await this.checkExtendTimeLockNodes(leavesToSendToSE);
|
|
2891
3054
|
|
|
2892
3055
|
const leafKeyTweaks = await Promise.all(
|
|
2893
|
-
|
|
3056
|
+
[...leavesToSendToSE, ...leavesToSendToSsp].map(async (leaf) => ({
|
|
2894
3057
|
leaf,
|
|
2895
3058
|
signingPubKey: await this.config.signer.generatePublicKey(
|
|
2896
3059
|
sha256(leaf.id),
|
|
@@ -2899,13 +3062,26 @@ export class SparkWallet extends EventEmitter {
|
|
|
2899
3062
|
})),
|
|
2900
3063
|
);
|
|
2901
3064
|
|
|
2902
|
-
const
|
|
2903
|
-
leafExternalIds:
|
|
3065
|
+
const requestCoopExitParams: RequestCoopExitInput = {
|
|
3066
|
+
leafExternalIds: leavesToSendToSsp.map((leaf) => leaf.id),
|
|
2904
3067
|
withdrawalAddress: onchainAddress,
|
|
2905
3068
|
idempotencyKey: uuidv7(),
|
|
2906
3069
|
exitSpeed,
|
|
2907
|
-
withdrawAll:
|
|
2908
|
-
}
|
|
3070
|
+
withdrawAll: deductFeeFromWithdrawalAmount,
|
|
3071
|
+
};
|
|
3072
|
+
|
|
3073
|
+
if (!deductFeeFromWithdrawalAmount) {
|
|
3074
|
+
requestCoopExitParams.feeQuoteId = feeEstimate.id;
|
|
3075
|
+
requestCoopExitParams.feeLeafExternalIds = leavesToSendToSE.map(
|
|
3076
|
+
(leaf) => leaf.id,
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
|
|
3080
|
+
const sspClient = this.getSspClient();
|
|
3081
|
+
|
|
3082
|
+
const coopExitRequest = await sspClient.requestCoopExit(
|
|
3083
|
+
requestCoopExitParams,
|
|
3084
|
+
);
|
|
2909
3085
|
|
|
2910
3086
|
if (!coopExitRequest?.rawConnectorTransaction) {
|
|
2911
3087
|
throw new Error("Failed to request coop exit");
|
|
@@ -2952,15 +3128,15 @@ export class SparkWallet extends EventEmitter {
|
|
|
2952
3128
|
* @param {Object} params - Input parameters for fee estimation
|
|
2953
3129
|
* @param {number} params.amountSats - The amount in satoshis to withdraw
|
|
2954
3130
|
* @param {string} params.withdrawalAddress - The Bitcoin address where the funds should be sent
|
|
2955
|
-
* @returns {Promise<
|
|
3131
|
+
* @returns {Promise<CoopExitFeeQuote | null>} Fee estimate for the withdrawal
|
|
2956
3132
|
*/
|
|
2957
|
-
public async
|
|
3133
|
+
public async getWithdrawalFeeQuote({
|
|
2958
3134
|
amountSats,
|
|
2959
3135
|
withdrawalAddress,
|
|
2960
3136
|
}: {
|
|
2961
3137
|
amountSats: number;
|
|
2962
3138
|
withdrawalAddress: string;
|
|
2963
|
-
}): Promise<
|
|
3139
|
+
}): Promise<CoopExitFeeQuote | null> {
|
|
2964
3140
|
const sspClient = this.getSspClient();
|
|
2965
3141
|
|
|
2966
3142
|
if (!Number.isSafeInteger(amountSats)) {
|
|
@@ -2971,12 +3147,12 @@ export class SparkWallet extends EventEmitter {
|
|
|
2971
3147
|
});
|
|
2972
3148
|
}
|
|
2973
3149
|
|
|
2974
|
-
let leaves = await this.selectLeaves(amountSats)
|
|
3150
|
+
let leaves = (await this.selectLeaves([amountSats])).get(amountSats)!;
|
|
2975
3151
|
|
|
2976
3152
|
leaves = await this.checkRefreshTimelockNodes(leaves);
|
|
2977
3153
|
leaves = await this.checkExtendTimeLockNodes(leaves);
|
|
2978
3154
|
|
|
2979
|
-
const feeEstimate = await sspClient.
|
|
3155
|
+
const feeEstimate = await sspClient.getCoopExitFeeQuote({
|
|
2980
3156
|
leafExternalIds: leaves.map((leaf) => leaf.id),
|
|
2981
3157
|
withdrawalAddress,
|
|
2982
3158
|
});
|