@gvnrdao/dh-sdk 0.0.303 → 0.0.304

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6602,6 +6602,14 @@ var ServerSession = class {
6602
6602
  fetchImpl;
6603
6603
  cached = null;
6604
6604
  inFlight = null;
6605
+ /**
6606
+ * Bumped by `clearPersisted()` (logout / disconnect). An in-flight
6607
+ * `getOrRefresh()` captures this at start and refuses to write its freshly
6608
+ * minted token back to `this.cached`/the store if the value changed
6609
+ * meanwhile — so a refresh racing a sign-out can't silently "un-clear" the
6610
+ * credential it was told to drop.
6611
+ */
6612
+ generation = 0;
6605
6613
  /**
6606
6614
  * Store key for the current signer, memoized on first use so the sync
6607
6615
  * `clear()` can drop the persisted token without an async address lookup.
@@ -6617,8 +6625,22 @@ var ServerSession = class {
6617
6625
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6618
6626
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6619
6627
  }
6620
- /** Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds. */
6621
- async getValidToken() {
6628
+ /**
6629
+ * Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds.
6630
+ * Pass `{ silent: true }` to forbid a fresh wallet signature: it resolves ONLY
6631
+ * from a live cached token or the persisted re-mint envelope, never prompting
6632
+ * and never joining a prompt-capable in-flight refresh (used by `logout()`,
6633
+ * which must never pop the wallet during sign-out). Rejects when only a fresh
6634
+ * signature could produce a token.
6635
+ */
6636
+ async getValidToken(opts) {
6637
+ if (opts?.silent) {
6638
+ const session2 = await this.tryResolveSilently(this.generation);
6639
+ if (!session2) {
6640
+ throw new Error("ServerSession: no existing session to refresh");
6641
+ }
6642
+ return session2.token;
6643
+ }
6622
6644
  const session = await this.getOrRefresh();
6623
6645
  return session.token;
6624
6646
  }
@@ -6651,20 +6673,24 @@ var ServerSession = class {
6651
6673
  * from storage).
6652
6674
  */
6653
6675
  async clearPersisted() {
6676
+ this.generation += 1;
6654
6677
  this.cached = null;
6655
6678
  this.inFlight = null;
6656
6679
  if (!this.store)
6657
6680
  return;
6658
- if (this.lastStoreKey) {
6659
- this.store.clear(this.lastStoreKey);
6660
- return;
6661
- }
6662
6681
  try {
6663
- const address = (await this.signer.getAddress()).toLowerCase();
6664
- this.store.clear(
6665
- sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6666
- );
6682
+ if (this.lastStoreKey) {
6683
+ this.store.clear(this.lastStoreKey);
6684
+ } else {
6685
+ const address = (await this.signer.getAddress()).toLowerCase();
6686
+ this.store.clear(
6687
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6688
+ );
6689
+ }
6667
6690
  } catch {
6691
+ } finally {
6692
+ this.generation += 1;
6693
+ this.cached = null;
6668
6694
  }
6669
6695
  }
6670
6696
  /**
@@ -6674,7 +6700,8 @@ var ServerSession = class {
6674
6700
  * of server response so the client stops presenting the token.
6675
6701
  */
6676
6702
  async logout() {
6677
- const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
6703
+ const cached = this.cached;
6704
+ const token = cached && cached.expiresAt > this.now() ? cached.token : await this.getValidToken({ silent: true }).catch(() => null);
6678
6705
  await this.clearPersisted();
6679
6706
  if (!token)
6680
6707
  return;
@@ -6689,6 +6716,50 @@ var ServerSession = class {
6689
6716
  );
6690
6717
  }
6691
6718
  }
6719
+ /**
6720
+ * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6721
+ * cached token, else the persisted JWT (adopted if still fresh, otherwise
6722
+ * silently re-minted from the envelope). Returns `null` only when a fresh
6723
+ * wallet signature would be required (dead/absent envelope). Never touches
6724
+ * `this.inFlight`, so a silent caller (logout) can resolve independently
6725
+ * instead of being dragged into a prompt-capable refresh already in flight.
6726
+ *
6727
+ * `gen` is the caller's clear-generation snapshot: writes to `this.cached`
6728
+ * and the store are skipped if a `clearPersisted()` landed meanwhile.
6729
+ */
6730
+ async tryResolveSilently(gen) {
6731
+ const nowSec = this.now();
6732
+ if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
6733
+ return this.cached;
6734
+ }
6735
+ const persisted = await this.loadPersisted();
6736
+ if (!persisted)
6737
+ return null;
6738
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6739
+ const adopted = {
6740
+ token: persisted.token,
6741
+ expiresAt: persisted.tokenExpiresAt
6742
+ };
6743
+ if (gen === this.generation)
6744
+ this.cached = adopted;
6745
+ return adopted;
6746
+ }
6747
+ try {
6748
+ const session = await this.login(persisted.payload);
6749
+ if (gen === this.generation) {
6750
+ this.persist(persisted.payload, session);
6751
+ this.cached = session;
6752
+ }
6753
+ return session;
6754
+ } catch (error) {
6755
+ if (!isLoginRejection(error))
6756
+ throw error;
6757
+ if (gen === this.generation && this.store && this.lastStoreKey) {
6758
+ this.store.clear(this.lastStoreKey);
6759
+ }
6760
+ return null;
6761
+ }
6762
+ }
6692
6763
  async getOrRefresh() {
6693
6764
  const nowSec = this.now();
6694
6765
  if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
@@ -6696,30 +6767,13 @@ var ServerSession = class {
6696
6767
  }
6697
6768
  if (this.inFlight)
6698
6769
  return this.inFlight;
6770
+ const gen = this.generation;
6771
+ const notCleared = () => gen === this.generation;
6699
6772
  this.inFlight = (async () => {
6700
6773
  try {
6701
- const persisted = await this.loadPersisted();
6702
- if (persisted) {
6703
- if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6704
- this.cached = {
6705
- token: persisted.token,
6706
- expiresAt: persisted.tokenExpiresAt
6707
- };
6708
- return this.cached;
6709
- }
6710
- try {
6711
- const session2 = await this.login(persisted.payload);
6712
- this.persist(persisted.payload, session2);
6713
- this.cached = session2;
6714
- return session2;
6715
- } catch (error) {
6716
- if (!isLoginRejection(error))
6717
- throw error;
6718
- if (this.store && this.lastStoreKey) {
6719
- this.store.clear(this.lastStoreKey);
6720
- }
6721
- }
6722
- }
6774
+ const existing = await this.tryResolveSilently(gen);
6775
+ if (existing)
6776
+ return existing;
6723
6777
  this.onSignaturePrompt?.();
6724
6778
  const payload = await buildSignedLoginPayload(
6725
6779
  this.signer,
@@ -6727,11 +6781,14 @@ var ServerSession = class {
6727
6781
  this.serviceEndpoint
6728
6782
  );
6729
6783
  const session = await this.login(payload);
6730
- this.persist(payload, session);
6731
- this.cached = session;
6784
+ if (notCleared()) {
6785
+ this.persist(payload, session);
6786
+ this.cached = session;
6787
+ }
6732
6788
  return session;
6733
6789
  } finally {
6734
- this.inFlight = null;
6790
+ if (notCleared())
6791
+ this.inFlight = null;
6735
6792
  }
6736
6793
  })();
6737
6794
  return this.inFlight;
@@ -16104,13 +16161,25 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16104
16161
  return this.serverSession.getAuthHeader();
16105
16162
  }
16106
16163
  /**
16107
- * Remove the persisted lit-ops-server session (the 24h re-mint envelope and
16108
- * its JWT) for the current auth signer. Call on wallet disconnect or
16109
- * explicit sign-out so the credential does not outlive the wallet
16110
- * connection. No-op in standalone mode.
16164
+ * Sign out of the lit-ops-server session for the current auth signer. Call on
16165
+ * wallet disconnect or explicit sign-out so the credential does not outlive
16166
+ * the wallet connection. No-op in standalone mode.
16167
+ *
16168
+ * Attempts a server-side revocation first (`ServerSession.logout()` — which
16169
+ * tombstones the envelope's 24h re-mint window and invalidates the JWT), then
16170
+ * always removes the local credential. If the revocation call fails (offline,
16171
+ * server down, wallet already disconnected) we still clear locally so the
16172
+ * session cannot silently re-mint after sign-out; `logout()` is best-effort
16173
+ * and never prompts the wallet.
16111
16174
  */
16112
16175
  async clearServerSession() {
16113
- await this.serverSession?.clearPersisted();
16176
+ if (!this.serverSession)
16177
+ return;
16178
+ try {
16179
+ await this.serverSession.logout();
16180
+ } catch {
16181
+ await this.serverSession.clearPersisted();
16182
+ }
16114
16183
  }
16115
16184
  /**
16116
16185
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
@@ -20715,6 +20784,42 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20715
20784
  };
20716
20785
  }
20717
20786
  }
20787
+ /**
20788
+ * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
20789
+ * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
20790
+ * assemble and sign the FULL input set for a multi-UTXO consolidation
20791
+ * withdrawal. Phase 1 only returns ONE representative UTXO, so a vault whose
20792
+ * balance is spread across several UTXOs otherwise fails the Phase-2 signer
20793
+ * with "Insufficient UTXO value ... across 1 input(s)".
20794
+ *
20795
+ * Returns `[]` on any failure (non-service mode, network error, bad shape) so
20796
+ * callers transparently fall back to the single authorized UTXO. Superseded
20797
+ * by the btc-withdrawal Lit Action returning its own confirmedUTXOs (Phase B).
20798
+ */
20799
+ async fetchConfirmedVaultUtxos(positionId) {
20800
+ if (this.config.mode !== "service" || !this.config.serviceEndpoint)
20801
+ return [];
20802
+ try {
20803
+ const pid = positionId.startsWith("0x") ? positionId : `0x${positionId}`;
20804
+ const resp = await fetch(
20805
+ `${this.config.serviceEndpoint}/api/lit/vault-utxos?positionId=${encodeURIComponent(pid)}`,
20806
+ { method: "GET", headers: { ...await this.getAuthHeader() } }
20807
+ );
20808
+ if (!resp.ok)
20809
+ return [];
20810
+ const json = await resp.json();
20811
+ if (!json.success || !Array.isArray(json.data?.utxos))
20812
+ return [];
20813
+ return json.data.utxos.map((u) => ({
20814
+ txid: String(u.txid),
20815
+ vout: Number(u.vout),
20816
+ value: Number(u.value),
20817
+ confirmations: Number(u.confirmations ?? 0)
20818
+ }));
20819
+ } catch {
20820
+ return [];
20821
+ }
20822
+ }
20718
20823
  /**
20719
20824
  * Execute Bitcoin withdrawal (Phase 2)
20720
20825
  *
@@ -21331,13 +21436,44 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21331
21436
  if (this.config.debug) {
21332
21437
  log.info("\u{1F50D} Found vault UTXO for execution", { utxoIdentifier });
21333
21438
  }
21439
+ let executionUtxos;
21440
+ const gatheredUtxos = await this.fetchConfirmedVaultUtxos(positionId);
21441
+ if (gatheredUtxos.length > 0) {
21442
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
21443
+ const byOutpoint = /* @__PURE__ */ new Map();
21444
+ for (const u of gatheredUtxos) {
21445
+ const txid = normTxid(u.txid);
21446
+ byOutpoint.set(`${txid}:${u.vout}`, {
21447
+ txid,
21448
+ vout: Number(u.vout),
21449
+ value: Number(u.value)
21450
+ });
21451
+ }
21452
+ const repKey = `${normTxid(authResult.utxoTxid)}:${authResult.utxoVout}`;
21453
+ if (!byOutpoint.has(repKey)) {
21454
+ byOutpoint.set(repKey, {
21455
+ txid: normTxid(authResult.utxoTxid),
21456
+ vout: Number(authResult.utxoVout),
21457
+ value: Number(authResult.utxoSatoshis)
21458
+ });
21459
+ }
21460
+ if (byOutpoint.size > 1) {
21461
+ executionUtxos = Array.from(byOutpoint.values());
21462
+ if (this.config.debug) {
21463
+ log.info("\u{1F9E9} Multi-UTXO withdrawal: signing full input set", {
21464
+ inputs: executionUtxos.length
21465
+ });
21466
+ }
21467
+ }
21468
+ }
21334
21469
  const executionResult = await this.executeBTCWithdrawal({
21335
21470
  positionId,
21336
21471
  utxoIdentifier,
21337
21472
  utxoSatoshis: Number(authResult.utxoSatoshis),
21338
21473
  networkFee,
21339
21474
  destination: withdrawalAddress,
21340
- targetAmount: withdrawalAmount
21475
+ targetAmount: withdrawalAmount,
21476
+ ...executionUtxos ? { utxos: executionUtxos } : {}
21341
21477
  });
21342
21478
  if (this.config.debug) {
21343
21479
  log.info("\u2705 Phase 2 complete: Bitcoin transaction executed", {
package/dist/index.mjs CHANGED
@@ -6526,6 +6526,14 @@ var ServerSession = class {
6526
6526
  fetchImpl;
6527
6527
  cached = null;
6528
6528
  inFlight = null;
6529
+ /**
6530
+ * Bumped by `clearPersisted()` (logout / disconnect). An in-flight
6531
+ * `getOrRefresh()` captures this at start and refuses to write its freshly
6532
+ * minted token back to `this.cached`/the store if the value changed
6533
+ * meanwhile — so a refresh racing a sign-out can't silently "un-clear" the
6534
+ * credential it was told to drop.
6535
+ */
6536
+ generation = 0;
6529
6537
  /**
6530
6538
  * Store key for the current signer, memoized on first use so the sync
6531
6539
  * `clear()` can drop the persisted token without an async address lookup.
@@ -6541,8 +6549,22 @@ var ServerSession = class {
6541
6549
  this.now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6542
6550
  this.fetchImpl = opts.fetchImpl ?? ((...a) => fetch(...a));
6543
6551
  }
6544
- /** Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds. */
6545
- async getValidToken() {
6552
+ /**
6553
+ * Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds.
6554
+ * Pass `{ silent: true }` to forbid a fresh wallet signature: it resolves ONLY
6555
+ * from a live cached token or the persisted re-mint envelope, never prompting
6556
+ * and never joining a prompt-capable in-flight refresh (used by `logout()`,
6557
+ * which must never pop the wallet during sign-out). Rejects when only a fresh
6558
+ * signature could produce a token.
6559
+ */
6560
+ async getValidToken(opts) {
6561
+ if (opts?.silent) {
6562
+ const session2 = await this.tryResolveSilently(this.generation);
6563
+ if (!session2) {
6564
+ throw new Error("ServerSession: no existing session to refresh");
6565
+ }
6566
+ return session2.token;
6567
+ }
6546
6568
  const session = await this.getOrRefresh();
6547
6569
  return session.token;
6548
6570
  }
@@ -6575,20 +6597,24 @@ var ServerSession = class {
6575
6597
  * from storage).
6576
6598
  */
6577
6599
  async clearPersisted() {
6600
+ this.generation += 1;
6578
6601
  this.cached = null;
6579
6602
  this.inFlight = null;
6580
6603
  if (!this.store)
6581
6604
  return;
6582
- if (this.lastStoreKey) {
6583
- this.store.clear(this.lastStoreKey);
6584
- return;
6585
- }
6586
6605
  try {
6587
- const address = (await this.signer.getAddress()).toLowerCase();
6588
- this.store.clear(
6589
- sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6590
- );
6606
+ if (this.lastStoreKey) {
6607
+ this.store.clear(this.lastStoreKey);
6608
+ } else {
6609
+ const address = (await this.signer.getAddress()).toLowerCase();
6610
+ this.store.clear(
6611
+ sessionStoreKey(address, this.chainId, this.serviceEndpoint)
6612
+ );
6613
+ }
6591
6614
  } catch {
6615
+ } finally {
6616
+ this.generation += 1;
6617
+ this.cached = null;
6592
6618
  }
6593
6619
  }
6594
6620
  /**
@@ -6598,7 +6624,8 @@ var ServerSession = class {
6598
6624
  * of server response so the client stops presenting the token.
6599
6625
  */
6600
6626
  async logout() {
6601
- const token = this.cached?.token ?? await this.getValidToken().catch(() => null);
6627
+ const cached = this.cached;
6628
+ const token = cached && cached.expiresAt > this.now() ? cached.token : await this.getValidToken({ silent: true }).catch(() => null);
6602
6629
  await this.clearPersisted();
6603
6630
  if (!token)
6604
6631
  return;
@@ -6613,6 +6640,50 @@ var ServerSession = class {
6613
6640
  );
6614
6641
  }
6615
6642
  }
6643
+ /**
6644
+ * Resolve a session WITHOUT ever prompting for a wallet signature: a live
6645
+ * cached token, else the persisted JWT (adopted if still fresh, otherwise
6646
+ * silently re-minted from the envelope). Returns `null` only when a fresh
6647
+ * wallet signature would be required (dead/absent envelope). Never touches
6648
+ * `this.inFlight`, so a silent caller (logout) can resolve independently
6649
+ * instead of being dragged into a prompt-capable refresh already in flight.
6650
+ *
6651
+ * `gen` is the caller's clear-generation snapshot: writes to `this.cached`
6652
+ * and the store are skipped if a `clearPersisted()` landed meanwhile.
6653
+ */
6654
+ async tryResolveSilently(gen) {
6655
+ const nowSec = this.now();
6656
+ if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
6657
+ return this.cached;
6658
+ }
6659
+ const persisted = await this.loadPersisted();
6660
+ if (!persisted)
6661
+ return null;
6662
+ if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6663
+ const adopted = {
6664
+ token: persisted.token,
6665
+ expiresAt: persisted.tokenExpiresAt
6666
+ };
6667
+ if (gen === this.generation)
6668
+ this.cached = adopted;
6669
+ return adopted;
6670
+ }
6671
+ try {
6672
+ const session = await this.login(persisted.payload);
6673
+ if (gen === this.generation) {
6674
+ this.persist(persisted.payload, session);
6675
+ this.cached = session;
6676
+ }
6677
+ return session;
6678
+ } catch (error) {
6679
+ if (!isLoginRejection(error))
6680
+ throw error;
6681
+ if (gen === this.generation && this.store && this.lastStoreKey) {
6682
+ this.store.clear(this.lastStoreKey);
6683
+ }
6684
+ return null;
6685
+ }
6686
+ }
6616
6687
  async getOrRefresh() {
6617
6688
  const nowSec = this.now();
6618
6689
  if (this.cached && this.cached.expiresAt - REFRESH_LEEWAY_SECONDS > nowSec) {
@@ -6620,30 +6691,13 @@ var ServerSession = class {
6620
6691
  }
6621
6692
  if (this.inFlight)
6622
6693
  return this.inFlight;
6694
+ const gen = this.generation;
6695
+ const notCleared = () => gen === this.generation;
6623
6696
  this.inFlight = (async () => {
6624
6697
  try {
6625
- const persisted = await this.loadPersisted();
6626
- if (persisted) {
6627
- if (persisted.token && typeof persisted.tokenExpiresAt === "number" && persisted.tokenExpiresAt - REFRESH_LEEWAY_SECONDS > this.now()) {
6628
- this.cached = {
6629
- token: persisted.token,
6630
- expiresAt: persisted.tokenExpiresAt
6631
- };
6632
- return this.cached;
6633
- }
6634
- try {
6635
- const session2 = await this.login(persisted.payload);
6636
- this.persist(persisted.payload, session2);
6637
- this.cached = session2;
6638
- return session2;
6639
- } catch (error) {
6640
- if (!isLoginRejection(error))
6641
- throw error;
6642
- if (this.store && this.lastStoreKey) {
6643
- this.store.clear(this.lastStoreKey);
6644
- }
6645
- }
6646
- }
6698
+ const existing = await this.tryResolveSilently(gen);
6699
+ if (existing)
6700
+ return existing;
6647
6701
  this.onSignaturePrompt?.();
6648
6702
  const payload = await buildSignedLoginPayload(
6649
6703
  this.signer,
@@ -6651,11 +6705,14 @@ var ServerSession = class {
6651
6705
  this.serviceEndpoint
6652
6706
  );
6653
6707
  const session = await this.login(payload);
6654
- this.persist(payload, session);
6655
- this.cached = session;
6708
+ if (notCleared()) {
6709
+ this.persist(payload, session);
6710
+ this.cached = session;
6711
+ }
6656
6712
  return session;
6657
6713
  } finally {
6658
- this.inFlight = null;
6714
+ if (notCleared())
6715
+ this.inFlight = null;
6659
6716
  }
6660
6717
  })();
6661
6718
  return this.inFlight;
@@ -16032,13 +16089,25 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16032
16089
  return this.serverSession.getAuthHeader();
16033
16090
  }
16034
16091
  /**
16035
- * Remove the persisted lit-ops-server session (the 24h re-mint envelope and
16036
- * its JWT) for the current auth signer. Call on wallet disconnect or
16037
- * explicit sign-out so the credential does not outlive the wallet
16038
- * connection. No-op in standalone mode.
16092
+ * Sign out of the lit-ops-server session for the current auth signer. Call on
16093
+ * wallet disconnect or explicit sign-out so the credential does not outlive
16094
+ * the wallet connection. No-op in standalone mode.
16095
+ *
16096
+ * Attempts a server-side revocation first (`ServerSession.logout()` — which
16097
+ * tombstones the envelope's 24h re-mint window and invalidates the JWT), then
16098
+ * always removes the local credential. If the revocation call fails (offline,
16099
+ * server down, wallet already disconnected) we still clear locally so the
16100
+ * session cannot silently re-mint after sign-out; `logout()` is best-effort
16101
+ * and never prompts the wallet.
16039
16102
  */
16040
16103
  async clearServerSession() {
16041
- await this.serverSession?.clearPersisted();
16104
+ if (!this.serverSession)
16105
+ return;
16106
+ try {
16107
+ await this.serverSession.logout();
16108
+ } catch {
16109
+ await this.serverSession.clearPersisted();
16110
+ }
16042
16111
  }
16043
16112
  /**
16044
16113
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
@@ -20643,6 +20712,42 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20643
20712
  };
20644
20713
  }
20645
20714
  }
20715
+ /**
20716
+ * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
20717
+ * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
20718
+ * assemble and sign the FULL input set for a multi-UTXO consolidation
20719
+ * withdrawal. Phase 1 only returns ONE representative UTXO, so a vault whose
20720
+ * balance is spread across several UTXOs otherwise fails the Phase-2 signer
20721
+ * with "Insufficient UTXO value ... across 1 input(s)".
20722
+ *
20723
+ * Returns `[]` on any failure (non-service mode, network error, bad shape) so
20724
+ * callers transparently fall back to the single authorized UTXO. Superseded
20725
+ * by the btc-withdrawal Lit Action returning its own confirmedUTXOs (Phase B).
20726
+ */
20727
+ async fetchConfirmedVaultUtxos(positionId) {
20728
+ if (this.config.mode !== "service" || !this.config.serviceEndpoint)
20729
+ return [];
20730
+ try {
20731
+ const pid = positionId.startsWith("0x") ? positionId : `0x${positionId}`;
20732
+ const resp = await fetch(
20733
+ `${this.config.serviceEndpoint}/api/lit/vault-utxos?positionId=${encodeURIComponent(pid)}`,
20734
+ { method: "GET", headers: { ...await this.getAuthHeader() } }
20735
+ );
20736
+ if (!resp.ok)
20737
+ return [];
20738
+ const json = await resp.json();
20739
+ if (!json.success || !Array.isArray(json.data?.utxos))
20740
+ return [];
20741
+ return json.data.utxos.map((u) => ({
20742
+ txid: String(u.txid),
20743
+ vout: Number(u.vout),
20744
+ value: Number(u.value),
20745
+ confirmations: Number(u.confirmations ?? 0)
20746
+ }));
20747
+ } catch {
20748
+ return [];
20749
+ }
20750
+ }
20646
20751
  /**
20647
20752
  * Execute Bitcoin withdrawal (Phase 2)
20648
20753
  *
@@ -21259,13 +21364,44 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21259
21364
  if (this.config.debug) {
21260
21365
  log.info("\u{1F50D} Found vault UTXO for execution", { utxoIdentifier });
21261
21366
  }
21367
+ let executionUtxos;
21368
+ const gatheredUtxos = await this.fetchConfirmedVaultUtxos(positionId);
21369
+ if (gatheredUtxos.length > 0) {
21370
+ const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
21371
+ const byOutpoint = /* @__PURE__ */ new Map();
21372
+ for (const u of gatheredUtxos) {
21373
+ const txid = normTxid(u.txid);
21374
+ byOutpoint.set(`${txid}:${u.vout}`, {
21375
+ txid,
21376
+ vout: Number(u.vout),
21377
+ value: Number(u.value)
21378
+ });
21379
+ }
21380
+ const repKey = `${normTxid(authResult.utxoTxid)}:${authResult.utxoVout}`;
21381
+ if (!byOutpoint.has(repKey)) {
21382
+ byOutpoint.set(repKey, {
21383
+ txid: normTxid(authResult.utxoTxid),
21384
+ vout: Number(authResult.utxoVout),
21385
+ value: Number(authResult.utxoSatoshis)
21386
+ });
21387
+ }
21388
+ if (byOutpoint.size > 1) {
21389
+ executionUtxos = Array.from(byOutpoint.values());
21390
+ if (this.config.debug) {
21391
+ log.info("\u{1F9E9} Multi-UTXO withdrawal: signing full input set", {
21392
+ inputs: executionUtxos.length
21393
+ });
21394
+ }
21395
+ }
21396
+ }
21262
21397
  const executionResult = await this.executeBTCWithdrawal({
21263
21398
  positionId,
21264
21399
  utxoIdentifier,
21265
21400
  utxoSatoshis: Number(authResult.utxoSatoshis),
21266
21401
  networkFee,
21267
21402
  destination: withdrawalAddress,
21268
- targetAmount: withdrawalAmount
21403
+ targetAmount: withdrawalAmount,
21404
+ ...executionUtxos ? { utxos: executionUtxos } : {}
21269
21405
  });
21270
21406
  if (this.config.debug) {
21271
21407
  log.info("\u2705 Phase 2 complete: Bitcoin transaction executed", {
@@ -112,10 +112,16 @@ export declare class DiamondHandsSDK {
112
112
  */
113
113
  private getAuthHeader;
114
114
  /**
115
- * Remove the persisted lit-ops-server session (the 24h re-mint envelope and
116
- * its JWT) for the current auth signer. Call on wallet disconnect or
117
- * explicit sign-out so the credential does not outlive the wallet
118
- * connection. No-op in standalone mode.
115
+ * Sign out of the lit-ops-server session for the current auth signer. Call on
116
+ * wallet disconnect or explicit sign-out so the credential does not outlive
117
+ * the wallet connection. No-op in standalone mode.
118
+ *
119
+ * Attempts a server-side revocation first (`ServerSession.logout()` — which
120
+ * tombstones the envelope's 24h re-mint window and invalidates the JWT), then
121
+ * always removes the local credential. If the revocation call fails (offline,
122
+ * server down, wallet already disconnected) we still clear locally so the
123
+ * session cannot silently re-mint after sign-out; `logout()` is best-effort
124
+ * and never prompts the wallet.
119
125
  */
120
126
  clearServerSession(): Promise<void>;
121
127
  /**
@@ -442,6 +448,24 @@ export declare class DiamondHandsSDK {
442
448
  * @returns Withdrawal result with transaction details
443
449
  */
444
450
  withdrawBTC(positionId: string, withdrawalAddress: string, withdrawalAmount: number): Promise<BTCWithdrawalResult>;
451
+ /**
452
+ * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
453
+ * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
454
+ * assemble and sign the FULL input set for a multi-UTXO consolidation
455
+ * withdrawal. Phase 1 only returns ONE representative UTXO, so a vault whose
456
+ * balance is spread across several UTXOs otherwise fails the Phase-2 signer
457
+ * with "Insufficient UTXO value ... across 1 input(s)".
458
+ *
459
+ * Returns `[]` on any failure (non-service mode, network error, bad shape) so
460
+ * callers transparently fall back to the single authorized UTXO. Superseded
461
+ * by the btc-withdrawal Lit Action returning its own confirmedUTXOs (Phase B).
462
+ */
463
+ fetchConfirmedVaultUtxos(positionId: string): Promise<Array<{
464
+ txid: string;
465
+ vout: number;
466
+ value: number;
467
+ confirmations: number;
468
+ }>>;
445
469
  /**
446
470
  * Execute Bitcoin withdrawal (Phase 2)
447
471
  *