@breeztech/breez-sdk-spark 0.18.0-dev4 → 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.
@@ -18,6 +18,8 @@ export const bitcoinchainservicehandle_recommendedFees: (a: number) => any;
18
18
  export const breezsdk_addContact: (a: number, b: any) => any;
19
19
  export const breezsdk_addEventListener: (a: number, b: any) => any;
20
20
  export const breezsdk_authorizeLightningAddressTransfer: (a: number, b: any) => any;
21
+ export const breezsdk_buildUnsignedLnurlPayPackage: (a: number, b: any) => any;
22
+ export const breezsdk_buildUnsignedTransferPackage: (a: number, b: any) => any;
21
23
  export const breezsdk_buyBitcoin: (a: number, b: any) => any;
22
24
  export const breezsdk_checkLightningAddressAvailable: (a: number, b: any) => any;
23
25
  export const breezsdk_checkMessage: (a: number, b: any) => any;
@@ -48,6 +50,8 @@ export const breezsdk_optimizeLeaves: (a: number, b: any) => any;
48
50
  export const breezsdk_parse: (a: number, b: number, c: number) => any;
49
51
  export const breezsdk_prepareLnurlPay: (a: number, b: any) => any;
50
52
  export const breezsdk_prepareSendPayment: (a: number, b: any) => any;
53
+ export const breezsdk_publishSignedLnurlPayPackage: (a: number, b: any) => any;
54
+ export const breezsdk_publishSignedTransferPackage: (a: number, b: any) => any;
51
55
  export const breezsdk_receivePayment: (a: number, b: any) => any;
52
56
  export const breezsdk_recommendedFees: (a: number) => any;
53
57
  export const breezsdk_refundDeposit: (a: number, b: any) => any;
@@ -63,13 +67,18 @@ export const breezsdk_updateContact: (a: number, b: any) => any;
63
67
  export const breezsdk_updateUserSettings: (a: number, b: any) => any;
64
68
  export const connect: (a: any) => any;
65
69
  export const connectWithSigner: (a: any, b: any, c: any, d: number, e: number) => any;
70
+ export const connectWithSigningOnlySigner: (a: any, b: any, c: any, d: number, e: number) => any;
66
71
  export const createTurnkeySigner: (a: any) => any;
72
+ export const createTurnkeySigningOnlySigner: (a: any) => any;
67
73
  export const defaultConfig: (a: any) => any;
68
74
  export const defaultExternalSigners: (a: number, b: number, c: number, d: number, e: any, f: number) => [number, number, number];
69
75
  export const defaultMysqlStorageConfig: (a: number, b: number) => any;
70
76
  export const defaultPostgresStorageConfig: (a: number, b: number) => any;
71
77
  export const defaultServerConfig: (a: any) => any;
78
+ export const defaultSessionStore: (a: number, b: any, c: number, d: number) => any;
72
79
  export const defaultStorage: (a: number, b: number) => number;
80
+ export const defaultsessionstore_getSession: (a: number, b: number, c: number) => any;
81
+ export const defaultsessionstore_setSession: (a: number, b: number, c: number, d: any) => any;
73
82
  export const externalbreezsignerhandle_decryptEcies: (a: number, b: number, c: number, d: number, e: number) => any;
74
83
  export const externalbreezsignerhandle_derivePublicKey: (a: number, b: number, c: number) => any;
75
84
  export const externalbreezsignerhandle_encryptEcies: (a: number, b: number, c: number, d: number, e: number) => any;
@@ -79,6 +88,10 @@ export const externalbreezsignerhandle_signEcdsaRecoverable: (a: number, b: any,
79
88
  export const externalbreezsignerhandle_signHashSchnorr: (a: number, b: number, c: number, d: number, e: number) => any;
80
89
  export const externalsigners_breezSigner: (a: number) => number;
81
90
  export const externalsigners_sparkSigner: (a: number) => number;
91
+ export const externalsigningsignerhandle_derivePublicKey: (a: number, b: number, c: number) => any;
92
+ export const externalsigningsignerhandle_signEcdsa: (a: number, b: any, c: number, d: number) => any;
93
+ export const externalsigningsignerhandle_signEcdsaRecoverable: (a: number, b: any, c: number, d: number) => any;
94
+ export const externalsigningsignerhandle_signHashSchnorr: (a: number, b: number, c: number, d: number, e: number) => any;
82
95
  export const externalsparksignerhandle_getIdentityPublicKey: (a: number) => any;
83
96
  export const externalsparksignerhandle_getPublicKeyForLeaf: (a: number, b: any) => any;
84
97
  export const externalsparksignerhandle_getStaticDepositPublicKey: (a: number, b: number) => any;
@@ -110,6 +123,7 @@ export const postgresStorage: (a: any) => number;
110
123
  export const sdkbuilder_build: (a: number) => any;
111
124
  export const sdkbuilder_new: (a: any, b: any) => number;
112
125
  export const sdkbuilder_newWithSigner: (a: any, b: any, c: any) => number;
126
+ export const sdkbuilder_newWithSigningOnlySigner: (a: any, b: any, c: any) => number;
113
127
  export const sdkbuilder_withAccountNumber: (a: number, b: number) => number;
114
128
  export const sdkbuilder_withChainService: (a: number, b: any) => number;
115
129
  export const sdkbuilder_withDefaultStorage: (a: number, b: number, c: number) => any;
@@ -119,6 +133,7 @@ export const sdkbuilder_withMysqlBackend: (a: number, b: any) => [number, number
119
133
  export const sdkbuilder_withPaymentObserver: (a: number, b: any) => number;
120
134
  export const sdkbuilder_withPostgresBackend: (a: number, b: any) => [number, number, number];
121
135
  export const sdkbuilder_withRestChainService: (a: number, b: number, c: number, d: any, e: number) => number;
136
+ export const sdkbuilder_withSessionStore: (a: number, b: any) => number;
122
137
  export const sdkbuilder_withSharedContext: (a: number, b: number) => number;
123
138
  export const sdkbuilder_withStorage: (a: number, b: any) => number;
124
139
  export const sdkbuilder_withStorageBackend: (a: number, b: number) => number;
@@ -149,12 +164,17 @@ export const intounderlyingsink_write: (a: number, b: any) => any;
149
164
  export const intounderlyingsource_cancel: (a: number) => void;
150
165
  export const intounderlyingsource_pull: (a: number, b: any) => any;
151
166
  export const __wbg_externalbreezsignerhandle_free: (a: number, b: number) => void;
167
+ export const __wbg_defaultsessionstore_free: (a: number, b: number) => void;
168
+ export const __wbg_externalsigningsignerhandle_free: (a: number, b: number) => void;
152
169
  export const __wbg_externalsparksignerhandle_free: (a: number, b: number) => void;
153
- export const wasm_bindgen__convert__closures_____invoke__h039d35996dc01d12: (a: number, b: number, c: any) => [number, number];
154
- export const wasm_bindgen__convert__closures_____invoke__h039d35996dc01d12_6: (a: number, b: number, c: any) => [number, number];
155
- export const wasm_bindgen__convert__closures_____invoke__h039d35996dc01d12_7: (a: number, b: number, c: any) => [number, number];
156
- export const wasm_bindgen__convert__closures_____invoke__h039d35996dc01d12_8: (a: number, b: number, c: any) => [number, number];
157
- export const wasm_bindgen__convert__closures_____invoke__h039d35996dc01d12_9: (a: number, b: number, c: any) => [number, number];
170
+ export const __wbg_signingonlyexternalsigners_free: (a: number, b: number) => void;
171
+ export const signingonlyexternalsigners_breezSigner: (a: number) => number;
172
+ export const signingonlyexternalsigners_sparkSigner: (a: number) => number;
173
+ export const wasm_bindgen__convert__closures_____invoke__h21f6855640435575: (a: number, b: number, c: any) => [number, number];
174
+ export const wasm_bindgen__convert__closures_____invoke__h21f6855640435575_6: (a: number, b: number, c: any) => [number, number];
175
+ export const wasm_bindgen__convert__closures_____invoke__h21f6855640435575_7: (a: number, b: number, c: any) => [number, number];
176
+ export const wasm_bindgen__convert__closures_____invoke__h21f6855640435575_8: (a: number, b: number, c: any) => [number, number];
177
+ export const wasm_bindgen__convert__closures_____invoke__h21f6855640435575_9: (a: number, b: number, c: any) => [number, number];
158
178
  export const wasm_bindgen__convert__closures_____invoke__h41057d61edf43a32: (a: number, b: number, c: any, d: any) => void;
159
179
  export const wasm_bindgen__convert__closures_____invoke__h3120db8c4a8a92b2: (a: number, b: number, c: any) => void;
160
180
  export const wasm_bindgen__convert__closures_____invoke__h3120db8c4a8a92b2_2: (a: number, b: number, c: any) => void;
package/nodejs/index.mjs CHANGED
@@ -6,12 +6,15 @@ import pkg from './index.js';
6
6
  export const {
7
7
  connect,
8
8
  connectWithSigner,
9
+ connectWithSigningOnlySigner,
9
10
  createTurnkeySigner,
11
+ createTurnkeySigningOnlySigner,
10
12
  defaultConfig,
11
13
  defaultExternalSigners,
12
14
  defaultMysqlStorageConfig,
13
15
  defaultPostgresStorageConfig,
14
16
  defaultServerConfig,
17
+ defaultSessionStore,
15
18
  defaultStorage,
16
19
  getSparkStatus,
17
20
  initLogging,
@@ -23,8 +26,10 @@ export const {
23
26
  task_worker_entry_point,
24
27
  BitcoinChainServiceHandle,
25
28
  BreezSdk,
29
+ DefaultSessionStore,
26
30
  ExternalBreezSignerHandle,
27
31
  ExternalSigners,
32
+ ExternalSigningSignerHandle,
28
33
  ExternalSparkSignerHandle,
29
34
  IntoUnderlyingByteSource,
30
35
  IntoUnderlyingSink,
@@ -32,6 +37,7 @@ export const {
32
37
  PasskeyClient,
33
38
  PasskeyLabels,
34
39
  SdkBuilder,
40
+ SigningOnlyExternalSigners,
35
41
  TokenIssuer,
36
42
  WasmSdkContext,
37
43
  WasmStorageConfig,
@@ -658,7 +658,7 @@ class MysqlTokenStore {
658
658
 
659
659
  let outputs = outputRows.map((row) => this._outputFromRow(row));
660
660
 
661
- if (preferredOutputs && preferredOutputs.length > 0) {
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
- let selectedOutputs;
671
-
672
- if (target.type === "minTotalValue") {
673
- const amount = BigInt(target.value);
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
- `SELECT id, value
467
- FROM brz_tree_leaves
468
- WHERE user_id = ?
469
- AND status = 'Available'
470
- AND is_missing_from_operators = 0
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
- return rows.map((r) => parseJson(r.data));
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) {