@breeztech/breez-sdk-spark 0.18.0 → 0.19.0
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/breez-sdk-spark.tgz +0 -0
- package/bundler/breez_sdk_spark_wasm.d.ts +96 -0
- package/bundler/breez_sdk_spark_wasm.js +1 -1
- package/bundler/breez_sdk_spark_wasm_bg.js +189 -22
- package/bundler/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/bundler/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -6
- package/deno/breez_sdk_spark_wasm.d.ts +96 -0
- package/deno/breez_sdk_spark_wasm.js +189 -22
- package/deno/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/deno/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -6
- package/nodejs/breez_sdk_spark_wasm.d.ts +96 -0
- package/nodejs/breez_sdk_spark_wasm.js +191 -22
- package/nodejs/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/nodejs/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -6
- package/nodejs/index.mjs +2 -0
- package/nodejs/mysql-token-store/index.cjs +212 -70
- package/nodejs/mysql-tree-store/index.cjs +158 -26
- package/nodejs/postgres-token-store/index.cjs +207 -69
- package/nodejs/postgres-tree-store/index.cjs +157 -31
- package/package.json +1 -1
- package/ssr/index.js +12 -0
- package/web/breez_sdk_spark_wasm.d.ts +111 -6
- package/web/breez_sdk_spark_wasm.js +189 -22
- package/web/breez_sdk_spark_wasm_bg.wasm +0 -0
- package/web/breez_sdk_spark_wasm_bg.wasm.d.ts +15 -6
package/nodejs/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export const {
|
|
|
14
14
|
defaultMysqlStorageConfig,
|
|
15
15
|
defaultPostgresStorageConfig,
|
|
16
16
|
defaultServerConfig,
|
|
17
|
+
defaultSessionStore,
|
|
17
18
|
defaultStorage,
|
|
18
19
|
getSparkStatus,
|
|
19
20
|
initLogging,
|
|
@@ -25,6 +26,7 @@ export const {
|
|
|
25
26
|
task_worker_entry_point,
|
|
26
27
|
BitcoinChainServiceHandle,
|
|
27
28
|
BreezSdk,
|
|
29
|
+
DefaultSessionStore,
|
|
28
30
|
ExternalBreezSignerHandle,
|
|
29
31
|
ExternalSigners,
|
|
30
32
|
ExternalSigningSignerHandle,
|
|
@@ -658,7 +658,7 @@ class MysqlTokenStore {
|
|
|
658
658
|
|
|
659
659
|
let outputs = outputRows.map((row) => this._outputFromRow(row));
|
|
660
660
|
|
|
661
|
-
if (preferredOutputs
|
|
661
|
+
if (preferredOutputs) {
|
|
662
662
|
const preferredOutpoints = new Set(
|
|
663
663
|
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
664
664
|
);
|
|
@@ -667,75 +667,11 @@ class MysqlTokenStore {
|
|
|
667
667
|
);
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const totalAvailable = outputs.reduce(
|
|
676
|
-
(sum, o) => sum + BigInt(o.output.tokenAmount),
|
|
677
|
-
0n
|
|
678
|
-
);
|
|
679
|
-
if (totalAvailable < amount) {
|
|
680
|
-
throw new TokenStoreError("InsufficientFunds");
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const exactMatch = outputs.find(
|
|
684
|
-
(o) => BigInt(o.output.tokenAmount) === amount
|
|
685
|
-
);
|
|
686
|
-
if (exactMatch) {
|
|
687
|
-
selectedOutputs = [exactMatch];
|
|
688
|
-
} else {
|
|
689
|
-
if (selectionStrategy === "LargestFirst") {
|
|
690
|
-
outputs.sort(
|
|
691
|
-
(a, b) =>
|
|
692
|
-
Number(
|
|
693
|
-
BigInt(b.output.tokenAmount) - BigInt(a.output.tokenAmount)
|
|
694
|
-
)
|
|
695
|
-
);
|
|
696
|
-
} else {
|
|
697
|
-
outputs.sort(
|
|
698
|
-
(a, b) =>
|
|
699
|
-
Number(
|
|
700
|
-
BigInt(a.output.tokenAmount) - BigInt(b.output.tokenAmount)
|
|
701
|
-
)
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
selectedOutputs = [];
|
|
706
|
-
let remaining = amount;
|
|
707
|
-
for (const output of outputs) {
|
|
708
|
-
if (remaining <= 0n) break;
|
|
709
|
-
selectedOutputs.push(output);
|
|
710
|
-
remaining -= BigInt(output.output.tokenAmount);
|
|
711
|
-
}
|
|
712
|
-
if (remaining > 0n) {
|
|
713
|
-
throw new TokenStoreError("InsufficientFunds");
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
} else if (target.type === "maxOutputCount") {
|
|
717
|
-
const count = target.value;
|
|
718
|
-
|
|
719
|
-
if (selectionStrategy === "LargestFirst") {
|
|
720
|
-
outputs.sort(
|
|
721
|
-
(a, b) =>
|
|
722
|
-
Number(
|
|
723
|
-
BigInt(b.output.tokenAmount) - BigInt(a.output.tokenAmount)
|
|
724
|
-
)
|
|
725
|
-
);
|
|
726
|
-
} else {
|
|
727
|
-
outputs.sort(
|
|
728
|
-
(a, b) =>
|
|
729
|
-
Number(
|
|
730
|
-
BigInt(a.output.tokenAmount) - BigInt(b.output.tokenAmount)
|
|
731
|
-
)
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
selectedOutputs = outputs.slice(0, count);
|
|
736
|
-
} else {
|
|
737
|
-
throw new TokenStoreError(`Unknown target type: ${target.type}`);
|
|
738
|
-
}
|
|
670
|
+
const selectedOutputs = this._selectOutputs(
|
|
671
|
+
outputs,
|
|
672
|
+
target,
|
|
673
|
+
selectionStrategy
|
|
674
|
+
);
|
|
739
675
|
|
|
740
676
|
const reservationId = this._generateId();
|
|
741
677
|
|
|
@@ -773,6 +709,212 @@ class MysqlTokenStore {
|
|
|
773
709
|
}
|
|
774
710
|
}
|
|
775
711
|
|
|
712
|
+
_selectOutputs(outputs, target, selectionStrategy) {
|
|
713
|
+
if (target.type === "minTotalValue") {
|
|
714
|
+
const amount = BigInt(target.value);
|
|
715
|
+
const totalAvailable = outputs.reduce(
|
|
716
|
+
(sum, o) => sum + BigInt(o.output.tokenAmount),
|
|
717
|
+
0n
|
|
718
|
+
);
|
|
719
|
+
if (totalAvailable < amount) {
|
|
720
|
+
throw new TokenStoreError("InsufficientFunds");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const exactMatch = outputs.find(
|
|
724
|
+
(o) => BigInt(o.output.tokenAmount) === amount
|
|
725
|
+
);
|
|
726
|
+
if (exactMatch) {
|
|
727
|
+
return [exactMatch];
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (selectionStrategy === "LargestFirst") {
|
|
731
|
+
outputs.sort(
|
|
732
|
+
(a, b) =>
|
|
733
|
+
Number(BigInt(b.output.tokenAmount) - BigInt(a.output.tokenAmount))
|
|
734
|
+
);
|
|
735
|
+
} else {
|
|
736
|
+
outputs.sort(
|
|
737
|
+
(a, b) =>
|
|
738
|
+
Number(BigInt(a.output.tokenAmount) - BigInt(b.output.tokenAmount))
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const selected = [];
|
|
743
|
+
let remaining = amount;
|
|
744
|
+
for (const output of outputs) {
|
|
745
|
+
if (remaining <= 0n) break;
|
|
746
|
+
selected.push(output);
|
|
747
|
+
remaining -= BigInt(output.output.tokenAmount);
|
|
748
|
+
}
|
|
749
|
+
if (remaining > 0n) {
|
|
750
|
+
throw new TokenStoreError("InsufficientFunds");
|
|
751
|
+
}
|
|
752
|
+
return selected;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (target.type === "maxOutputCount") {
|
|
756
|
+
const count = target.value;
|
|
757
|
+
if (selectionStrategy === "LargestFirst") {
|
|
758
|
+
outputs.sort(
|
|
759
|
+
(a, b) =>
|
|
760
|
+
Number(BigInt(b.output.tokenAmount) - BigInt(a.output.tokenAmount))
|
|
761
|
+
);
|
|
762
|
+
} else {
|
|
763
|
+
outputs.sort(
|
|
764
|
+
(a, b) =>
|
|
765
|
+
Number(BigInt(a.output.tokenAmount) - BigInt(b.output.tokenAmount))
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
return outputs.slice(0, count);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
throw new TokenStoreError(`Unknown target type: ${target.type}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async selectTokenOutputs(
|
|
775
|
+
tokenIdentifier,
|
|
776
|
+
target,
|
|
777
|
+
preferredOutputs,
|
|
778
|
+
selectionStrategy
|
|
779
|
+
) {
|
|
780
|
+
try {
|
|
781
|
+
if (
|
|
782
|
+
target.type === "minTotalValue" &&
|
|
783
|
+
(!target.value || target.value === "0")
|
|
784
|
+
) {
|
|
785
|
+
throw new TokenStoreError("Amount to reserve must be greater than zero");
|
|
786
|
+
}
|
|
787
|
+
if (
|
|
788
|
+
target.type === "maxOutputCount" &&
|
|
789
|
+
(!target.value || target.value === 0)
|
|
790
|
+
) {
|
|
791
|
+
throw new TokenStoreError("Count to reserve must be greater than zero");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const [metadataRows] = await this.pool.query(
|
|
795
|
+
"SELECT * FROM brz_token_metadata WHERE user_id = ? AND identifier = ?",
|
|
796
|
+
[this.identity, tokenIdentifier]
|
|
797
|
+
);
|
|
798
|
+
if (metadataRows.length === 0) {
|
|
799
|
+
throw new TokenStoreError(
|
|
800
|
+
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
const metadata = this._metadataFromRow(metadataRows[0]);
|
|
804
|
+
|
|
805
|
+
const [outputRows] = await this.pool.query(
|
|
806
|
+
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
807
|
+
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
808
|
+
o.token_public_key, o.token_amount, o.token_identifier,
|
|
809
|
+
o.prev_tx_hash, o.prev_tx_vout
|
|
810
|
+
FROM brz_token_outputs o
|
|
811
|
+
WHERE o.user_id = ? AND o.token_identifier = ? AND o.reservation_id IS NULL`,
|
|
812
|
+
[this.identity, tokenIdentifier]
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
let outputs = outputRows.map((row) => this._outputFromRow(row));
|
|
816
|
+
|
|
817
|
+
if (preferredOutputs) {
|
|
818
|
+
const preferredOutpoints = new Set(
|
|
819
|
+
preferredOutputs.map((p) => `${p.prevTxHash}:${p.prevTxVout}`)
|
|
820
|
+
);
|
|
821
|
+
outputs = outputs.filter((o) =>
|
|
822
|
+
preferredOutpoints.has(`${o.prevTxHash}:${o.prevTxVout}`)
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const selectedOutputs = this._selectOutputs(
|
|
827
|
+
outputs,
|
|
828
|
+
target,
|
|
829
|
+
selectionStrategy
|
|
830
|
+
);
|
|
831
|
+
return { metadata, outputs: selectedOutputs };
|
|
832
|
+
} catch (error) {
|
|
833
|
+
if (error instanceof TokenStoreError) throw error;
|
|
834
|
+
throw new TokenStoreError(
|
|
835
|
+
`Failed to select token outputs: ${error.message}`,
|
|
836
|
+
error
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async reserveTokenOutputsByOutpoints(tokenIdentifier, outpoints, purpose) {
|
|
842
|
+
try {
|
|
843
|
+
if (!outpoints || outpoints.length === 0) {
|
|
844
|
+
throw new TokenStoreError("No outpoints provided");
|
|
845
|
+
}
|
|
846
|
+
return await this._withWriteTransaction(async (conn) => {
|
|
847
|
+
const [metadataRows] = await conn.query(
|
|
848
|
+
"SELECT * FROM brz_token_metadata WHERE user_id = ? AND identifier = ?",
|
|
849
|
+
[this.identity, tokenIdentifier]
|
|
850
|
+
);
|
|
851
|
+
if (metadataRows.length === 0) {
|
|
852
|
+
throw new TokenStoreError(
|
|
853
|
+
`Token outputs not found for identifier: ${tokenIdentifier}`
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
const metadata = this._metadataFromRow(metadataRows[0]);
|
|
857
|
+
|
|
858
|
+
const pairPlaceholders = outpoints.map(() => "(?, ?)").join(", ");
|
|
859
|
+
const selectParams = [this.identity, tokenIdentifier];
|
|
860
|
+
for (const o of outpoints) {
|
|
861
|
+
selectParams.push(o.prevTxHash, o.prevTxVout);
|
|
862
|
+
}
|
|
863
|
+
const [outputRows] = await conn.query(
|
|
864
|
+
`SELECT o.owner_public_key, o.revocation_commitment,
|
|
865
|
+
o.withdraw_bond_sats, o.withdraw_relative_block_locktime,
|
|
866
|
+
o.token_public_key, o.token_amount, o.token_identifier,
|
|
867
|
+
o.prev_tx_hash, o.prev_tx_vout
|
|
868
|
+
FROM brz_token_outputs o
|
|
869
|
+
WHERE o.user_id = ? AND o.token_identifier = ? AND o.reservation_id IS NULL
|
|
870
|
+
AND (o.prev_tx_hash, o.prev_tx_vout) IN (${pairPlaceholders})`,
|
|
871
|
+
selectParams
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
const selectedOutputs = outputRows.map((row) =>
|
|
875
|
+
this._outputFromRow(row)
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
const distinct = new Set(
|
|
879
|
+
outpoints.map((o) => `${o.prevTxHash}:${o.prevTxVout}`)
|
|
880
|
+
);
|
|
881
|
+
if (selectedOutputs.length !== distinct.size) {
|
|
882
|
+
throw new TokenStoreError("InsufficientFunds");
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const reservationId = this._generateId();
|
|
886
|
+
await conn.query(
|
|
887
|
+
"INSERT INTO brz_token_reservations (user_id, id, purpose, created_at) VALUES (?, ?, ?, UTC_TIMESTAMP(6))",
|
|
888
|
+
[this.identity, reservationId, purpose]
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
const updatePlaceholders = selectedOutputs
|
|
892
|
+
.map(() => "(?, ?)")
|
|
893
|
+
.join(", ");
|
|
894
|
+
const updateParams = [reservationId, this.identity];
|
|
895
|
+
for (const o of selectedOutputs) {
|
|
896
|
+
updateParams.push(o.prevTxHash, o.prevTxVout);
|
|
897
|
+
}
|
|
898
|
+
await conn.query(
|
|
899
|
+
`UPDATE brz_token_outputs SET reservation_id = ? WHERE user_id = ?
|
|
900
|
+
AND (prev_tx_hash, prev_tx_vout) IN (${updatePlaceholders})`,
|
|
901
|
+
updateParams
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
return {
|
|
905
|
+
id: reservationId,
|
|
906
|
+
tokenOutputs: { metadata, outputs: selectedOutputs },
|
|
907
|
+
};
|
|
908
|
+
});
|
|
909
|
+
} catch (error) {
|
|
910
|
+
if (error instanceof TokenStoreError) throw error;
|
|
911
|
+
throw new TokenStoreError(
|
|
912
|
+
`Failed to reserve token outputs by outpoints: ${error.message}`,
|
|
913
|
+
error
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
776
918
|
async cancelReservation(id) {
|
|
777
919
|
try {
|
|
778
920
|
await this._withTransaction(async (conn) => {
|
|
@@ -43,6 +43,36 @@ const WRITE_LOCK_TIMEOUT_SECS = 30;
|
|
|
43
43
|
const RESERVATION_TIMEOUT_SECS = 300;
|
|
44
44
|
const SPENT_MARKER_CLEANUP_THRESHOLD_MS = 5 * 60 * 1000;
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Slim projection: only (id, value) for leaves the selection might use.
|
|
48
|
+
* Includes all leaves with value <= the max target (covers exact-match + the
|
|
49
|
+
* small-leaf accumulators for the minimum-amount path) plus the single
|
|
50
|
+
* smallest leaf with a larger value (covers the minimum-amount fallback case
|
|
51
|
+
* where one larger leaf is sufficient).
|
|
52
|
+
* Params: user id, max target, user id, max target.
|
|
53
|
+
*/
|
|
54
|
+
const SLIM_LEAF_CANDIDATES_SQL = `SELECT id, value
|
|
55
|
+
FROM brz_tree_leaves
|
|
56
|
+
WHERE user_id = ?
|
|
57
|
+
AND status = 'Available'
|
|
58
|
+
AND is_missing_from_operators = 0
|
|
59
|
+
AND reservation_id IS NULL
|
|
60
|
+
AND (
|
|
61
|
+
value <= ?
|
|
62
|
+
OR id = (
|
|
63
|
+
SELECT id FROM (
|
|
64
|
+
SELECT id FROM brz_tree_leaves
|
|
65
|
+
WHERE user_id = ?
|
|
66
|
+
AND status = 'Available'
|
|
67
|
+
AND is_missing_from_operators = 0
|
|
68
|
+
AND reservation_id IS NULL
|
|
69
|
+
AND value > ?
|
|
70
|
+
ORDER BY value
|
|
71
|
+
LIMIT 1
|
|
72
|
+
) AS smallest_over
|
|
73
|
+
)
|
|
74
|
+
)`;
|
|
75
|
+
|
|
46
76
|
/**
|
|
47
77
|
* Derive a stable per-tenant lock name from a tenant identity pubkey. Hashes
|
|
48
78
|
* a domain prefix together with the identity (SHA-256, first 8 bytes hex).
|
|
@@ -245,6 +275,32 @@ class MysqlTreeStore {
|
|
|
245
275
|
}
|
|
246
276
|
}
|
|
247
277
|
|
|
278
|
+
async getVerifiedLeafKeys() {
|
|
279
|
+
try {
|
|
280
|
+
// Project just the two pubkeys out of the JSON, skipping each leaf's
|
|
281
|
+
// `data` blob (up to five transactions). The filter matches the verified
|
|
282
|
+
// categories the SDK expects: every reserved leaf plus every Available
|
|
283
|
+
// one, and nothing non-Available and unreserved.
|
|
284
|
+
const [rows] = await this.pool.query(
|
|
285
|
+
`SELECT l.id AS id,
|
|
286
|
+
l.data->>'$.verifying_public_key' AS verifying,
|
|
287
|
+
l.data->>'$.signing_keyshare.public_key' AS keyshare
|
|
288
|
+
FROM brz_tree_leaves l
|
|
289
|
+
LEFT JOIN brz_tree_reservations r
|
|
290
|
+
ON l.reservation_id = r.id AND l.user_id = r.user_id
|
|
291
|
+
WHERE l.user_id = ?
|
|
292
|
+
AND (r.purpose IS NOT NULL OR l.status = 'Available')`,
|
|
293
|
+
[this.identity]
|
|
294
|
+
);
|
|
295
|
+
return rows.map((row) => [row.id, row.verifying, row.keyshare]);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new TreeStoreError(
|
|
298
|
+
`Failed to get verified leaf keys: ${error.message}`,
|
|
299
|
+
error
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
248
304
|
async getLeaves() {
|
|
249
305
|
try {
|
|
250
306
|
const [rows] = await this.pool.query(
|
|
@@ -462,30 +518,12 @@ class MysqlTreeStore {
|
|
|
462
518
|
);
|
|
463
519
|
const available = Number(totalRows[0].total);
|
|
464
520
|
|
|
465
|
-
const [slimRows] = await conn.query(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
AND reservation_id IS NULL
|
|
472
|
-
AND (
|
|
473
|
-
value <= ?
|
|
474
|
-
OR id = (
|
|
475
|
-
SELECT id FROM (
|
|
476
|
-
SELECT id FROM brz_tree_leaves
|
|
477
|
-
WHERE user_id = ?
|
|
478
|
-
AND status = 'Available'
|
|
479
|
-
AND is_missing_from_operators = 0
|
|
480
|
-
AND reservation_id IS NULL
|
|
481
|
-
AND value > ?
|
|
482
|
-
ORDER BY value
|
|
483
|
-
LIMIT 1
|
|
484
|
-
) AS smallest_over
|
|
485
|
-
)
|
|
486
|
-
)`,
|
|
487
|
-
[this.identity, maxTarget, this.identity, maxTarget]
|
|
488
|
-
);
|
|
521
|
+
const [slimRows] = await conn.query(SLIM_LEAF_CANDIDATES_SQL, [
|
|
522
|
+
this.identity,
|
|
523
|
+
maxTarget,
|
|
524
|
+
this.identity,
|
|
525
|
+
maxTarget,
|
|
526
|
+
]);
|
|
489
527
|
|
|
490
528
|
const slimLeaves = slimRows.map((r) => ({
|
|
491
529
|
id: r.id,
|
|
@@ -579,6 +617,86 @@ class MysqlTreeStore {
|
|
|
579
617
|
}
|
|
580
618
|
}
|
|
581
619
|
|
|
620
|
+
async trySelectLeaves(targetAmounts) {
|
|
621
|
+
try {
|
|
622
|
+
const targetAmount = targetAmounts ? this._totalSats(targetAmounts) : 0;
|
|
623
|
+
const maxTarget = this._maxTargetForPrefilter(targetAmounts);
|
|
624
|
+
|
|
625
|
+
return await this._withTransaction(async (conn) => {
|
|
626
|
+
const [slimRows] = await conn.query(SLIM_LEAF_CANDIDATES_SQL, [
|
|
627
|
+
this.identity,
|
|
628
|
+
maxTarget,
|
|
629
|
+
this.identity,
|
|
630
|
+
maxTarget,
|
|
631
|
+
]);
|
|
632
|
+
|
|
633
|
+
const slimLeaves = slimRows.map((r) => ({
|
|
634
|
+
id: r.id,
|
|
635
|
+
value: Number(r.value),
|
|
636
|
+
}));
|
|
637
|
+
|
|
638
|
+
const selected = this._selectLeavesByTargetAmounts(slimLeaves, targetAmounts);
|
|
639
|
+
if (selected !== null && selected.length > 0) {
|
|
640
|
+
const fullLeaves = await this._fetchFullLeavesByIds(
|
|
641
|
+
conn,
|
|
642
|
+
selected.map((l) => l.id)
|
|
643
|
+
);
|
|
644
|
+
return { type: "exact", leaves: fullLeaves };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const minSelected = this._selectLeavesByMinimumAmount(slimLeaves, targetAmount);
|
|
648
|
+
if (minSelected !== null) {
|
|
649
|
+
const fullLeaves = await this._fetchFullLeavesByIds(
|
|
650
|
+
conn,
|
|
651
|
+
minSelected.map((l) => l.id)
|
|
652
|
+
);
|
|
653
|
+
return { type: "swapNeeded", leaves: fullLeaves };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return { type: "insufficientFunds" };
|
|
657
|
+
});
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof TreeStoreError) throw error;
|
|
660
|
+
throw new TreeStoreError(
|
|
661
|
+
`Failed to try select leaves: ${error.message}`,
|
|
662
|
+
error
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async tryReserveLeavesByIds(leafIds, purpose) {
|
|
668
|
+
try {
|
|
669
|
+
return await this._withWriteTransaction(async (conn) => {
|
|
670
|
+
if (!leafIds || leafIds.length === 0) {
|
|
671
|
+
throw new TreeStoreError("NonReservableLeaves");
|
|
672
|
+
}
|
|
673
|
+
// Every requested leaf must be available and unreserved; otherwise
|
|
674
|
+
// reserve nothing (the transaction rolls back).
|
|
675
|
+
const placeholders = leafIds.map(() => "?").join(", ");
|
|
676
|
+
const [availableRows] = await conn.query(
|
|
677
|
+
`SELECT id FROM brz_tree_leaves
|
|
678
|
+
WHERE user_id = ? AND id IN (${placeholders})
|
|
679
|
+
AND status = 'Available' AND is_missing_from_operators = 0
|
|
680
|
+
AND reservation_id IS NULL`,
|
|
681
|
+
[this.identity, ...leafIds]
|
|
682
|
+
);
|
|
683
|
+
if (availableRows.length !== leafIds.length) {
|
|
684
|
+
throw new TreeStoreError("NonReservableLeaves");
|
|
685
|
+
}
|
|
686
|
+
const fullLeaves = await this._fetchFullLeavesByIds(conn, leafIds);
|
|
687
|
+
const reservationId = this._generateId();
|
|
688
|
+
await this._createReservation(conn, reservationId, fullLeaves, purpose, 0);
|
|
689
|
+
return { id: reservationId, leaves: fullLeaves };
|
|
690
|
+
});
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (error instanceof TreeStoreError) throw error;
|
|
693
|
+
throw new TreeStoreError(
|
|
694
|
+
`Failed to try reserve leaves by ids: ${error.message}`,
|
|
695
|
+
error
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
582
700
|
async now() {
|
|
583
701
|
try {
|
|
584
702
|
const [rows] = await this.pool.query("SELECT UTC_TIMESTAMP(6) AS now");
|
|
@@ -682,10 +800,24 @@ class MysqlTreeStore {
|
|
|
682
800
|
if (!ids || ids.length === 0) return [];
|
|
683
801
|
const placeholders = ids.map(() => "?").join(", ");
|
|
684
802
|
const [rows] = await conn.query(
|
|
685
|
-
`SELECT data FROM brz_tree_leaves WHERE user_id = ? AND id IN (${placeholders})`,
|
|
803
|
+
`SELECT id, data FROM brz_tree_leaves WHERE user_id = ? AND id IN (${placeholders})`,
|
|
686
804
|
[this.identity, ...ids]
|
|
687
805
|
);
|
|
688
|
-
|
|
806
|
+
const byId = new Map(rows.map((r) => [r.id, r.data]));
|
|
807
|
+
const ordered = ids
|
|
808
|
+
.map((id) => {
|
|
809
|
+
const data = byId.get(id);
|
|
810
|
+
byId.delete(id);
|
|
811
|
+
return data;
|
|
812
|
+
})
|
|
813
|
+
.filter((data) => data !== undefined)
|
|
814
|
+
.map((data) => parseJson(data));
|
|
815
|
+
if (ordered.length !== ids.length) {
|
|
816
|
+
throw new TreeStoreError(
|
|
817
|
+
`Could not resolve full data for all selected leaves (wanted ${ids.length}, got ${ordered.length})`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return ordered;
|
|
689
821
|
}
|
|
690
822
|
|
|
691
823
|
_selectLeavesByTargetAmounts(leaves, targetAmounts) {
|