@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/browser/dist/browser.js +1 -1
- package/dist/index.js +178 -42
- package/dist/index.mjs +178 -42
- package/dist/modules/diamond-hands-sdk.d.ts +28 -4
- package/dist/utils/server-session.d.ts +31 -2
- package/package.json +1 -1
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
|
-
/**
|
|
6621
|
-
|
|
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
|
-
|
|
6664
|
-
|
|
6665
|
-
|
|
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
|
|
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
|
|
6702
|
-
if (
|
|
6703
|
-
|
|
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
|
-
|
|
6731
|
-
|
|
6784
|
+
if (notCleared()) {
|
|
6785
|
+
this.persist(payload, session);
|
|
6786
|
+
this.cached = session;
|
|
6787
|
+
}
|
|
6732
6788
|
return session;
|
|
6733
6789
|
} finally {
|
|
6734
|
-
|
|
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
|
-
*
|
|
16108
|
-
*
|
|
16109
|
-
*
|
|
16110
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
6545
|
-
|
|
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
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
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
|
|
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
|
|
6626
|
-
if (
|
|
6627
|
-
|
|
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
|
-
|
|
6655
|
-
|
|
6708
|
+
if (notCleared()) {
|
|
6709
|
+
this.persist(payload, session);
|
|
6710
|
+
this.cached = session;
|
|
6711
|
+
}
|
|
6656
6712
|
return session;
|
|
6657
6713
|
} finally {
|
|
6658
|
-
|
|
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
|
-
*
|
|
16036
|
-
*
|
|
16037
|
-
*
|
|
16038
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
*
|