@gvnrdao/dh-sdk 0.0.300 → 0.0.303

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.
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { Wallet, Signer, Provider } from "ethers";
5
5
  import { LitNetwork } from "@gvnrdao/dh-lit-ops";
6
+ import type { ServerSessionStore } from "../../utils/server-session-store";
6
7
  export type SDKMode = "standalone" | "service";
7
8
  /**
8
9
  * Contract Addresses Configuration
@@ -55,6 +56,27 @@ interface BaseSDKConfig {
55
56
  * is a SafeModuleSignerAdapter that routes transactions through the module.
56
57
  */
57
58
  authSigner?: Wallet | Signer;
59
+ /**
60
+ * Persistence for the lit-ops-server session (service mode). The signed
61
+ * EIP-712 login envelope silently re-mints 15-minute JWTs for up to 24h, so
62
+ * persisting it means at most one wallet signature per day per device
63
+ * instead of one per 15 minutes / page reload. Defaults to enabled with
64
+ * `localStorage` in browsers; degrades to per-instance memory elsewhere.
65
+ * Note the persisted envelope is a re-mint credential for its window —
66
+ * call `clearServerSession()` on wallet disconnect.
67
+ */
68
+ sessionPersistence?: {
69
+ /** Default true. Set false for a fresh signature per instance + expiry. */
70
+ enabled?: boolean;
71
+ /** Custom store (e.g. a file-backed store for CLI daemons). */
72
+ store?: ServerSessionStore;
73
+ };
74
+ /**
75
+ * Fires immediately before the server-session login requests a wallet
76
+ * signature — never on the silent re-mint paths. Lets UIs show a
77
+ * "check your wallet" prompt.
78
+ */
79
+ onSessionSignaturePrompt?: () => void;
58
80
  ethRpcUrl?: string;
59
81
  chainId?: number;
60
82
  networkOverride?: {
@@ -111,6 +111,13 @@ export declare class DiamondHandsSDK {
111
111
  * reject the call with 401, which is the correct outcome.
112
112
  */
113
113
  private getAuthHeader;
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.
119
+ */
120
+ clearServerSession(): Promise<void>;
114
121
  /**
115
122
  * Audit H-9: invalidate the LoanQuery cache so subsequent reads return
116
123
  * the post-write state. We clear the entire loan-query cache (not just
@@ -456,6 +463,17 @@ export declare class DiamondHandsSDK {
456
463
  * Action can cross-check against the on-chain authorizer record.
457
464
  */
458
465
  targetAmount: number;
466
+ /**
467
+ * FULL input set the phase-2 signer may spend (P6/#8 — bound into the
468
+ * signature via a canonical set hash). Omit for the common single-UTXO
469
+ * withdrawal; multi-UTXO consolidation withdrawals MUST list every input
470
+ * (each with its full sat value), including the authorized UTXO.
471
+ */
472
+ utxos?: Array<{
473
+ txid: string;
474
+ vout: number;
475
+ value: number;
476
+ }>;
459
477
  }): Promise<{
460
478
  success: boolean;
461
479
  txid?: string;
@@ -658,6 +676,13 @@ export declare class DiamondHandsSDK {
658
676
  * Get all events for a loan position from the subgraph
659
677
  */
660
678
  getLoanEvents(positionId: string, filter?: import("../types/event-types").LoanEventsFilter): Promise<Result<import("../types/event-types").LoanEvents, SDKError>>;
679
+ /**
680
+ * Get loan-event history for every position owned by a wallet.
681
+ * Composes getUserPositions + getLoanEvents; returns one LoanEvents per
682
+ * position (each carrying its own positionId). Bitcoin deposits are not on the
683
+ * subgraph — merge those in from an Esplora source on the consumer side.
684
+ */
685
+ getWalletLoanEvents(userAddress: string, filter?: import("../types/event-types").LoanEventsFilter): Promise<Result<import("../types/event-types").LoanEvents[], SDKError>>;
661
686
  /**
662
687
  * Get protocol-wide events across all indexed entity types.
663
688
  * Returns a merged, sorted discriminated-union timeline.
@@ -839,6 +864,22 @@ export declare class DiamondHandsSDK {
839
864
  signature: string;
840
865
  validatorPkp: string;
841
866
  }, SDKError>>;
867
+ /**
868
+ * List a vault address's PENDING (not-yet-confirmed) incoming BTC deposits.
869
+ *
870
+ * A deposit is "pending" until it reaches the protocol's minimum confirmation
871
+ * depth (6) — the confirmed balance endpoints (`getVaultBalance` /
872
+ * `getAddressBalance`) omit anything shallower, so this surfaces in-flight
873
+ * deposits (0–5 confirmations). Server-delegated: lit-ops-server queries its
874
+ * configured premium Esplora provider (`/api/lit/pending-deposits`). Requires
875
+ * service mode.
876
+ */
877
+ getPendingDeposits(vaultAddress: string): Promise<Array<{
878
+ txid: string;
879
+ sats: number;
880
+ confirmations: number;
881
+ minConfirmations: number;
882
+ }>>;
842
883
  /**
843
884
  * Mint mock BTC tokens (test networks only)
844
885
  *
@@ -187,6 +187,29 @@ export declare class LoanQuery {
187
187
  * ```
188
188
  */
189
189
  getLoanEvents(positionId: string, filter?: LoanEventsFilter): Promise<Result<LoanEvents, SDKError>>;
190
+ /**
191
+ * Get loan-event history for every position owned by a wallet.
192
+ *
193
+ * Composes getUserPositions + getLoanEvents (the subgraph has no wallet-scoped
194
+ * event collection). Each returned LoanEvents carries its own positionId, so
195
+ * callers can flatten and merge into a single per-wallet timeline.
196
+ *
197
+ * Bitcoin deposits never appear in the subgraph — merge those in from an
198
+ * Esplora source on the consumer side.
199
+ *
200
+ * @param userAddress - Loan-owner wallet address.
201
+ * @param filter - Optional per-position event filter (same semantics as getLoanEvents).
202
+ * @returns One LoanEvents object per position (empty array if the wallet has none).
203
+ *
204
+ * @example
205
+ * ```typescript
206
+ * const result = await loanQuery.getWalletLoanEvents('0x830b...');
207
+ * if (result.success) {
208
+ * const allEvents = result.value; // LoanEvents[] — one per position
209
+ * }
210
+ * ```
211
+ */
212
+ getWalletLoanEvents(userAddress: string, filter?: LoanEventsFilter): Promise<Result<LoanEvents[], SDKError>>;
190
213
  /**
191
214
  * Clear loan cache
192
215
  *
@@ -40,8 +40,6 @@ export interface MintUCDAuthParams extends BaseAuthParams {
40
40
  amount: UCD;
41
41
  /** PKP public key (optional - validator PKP is passed separately to authorizeMintUCD) */
42
42
  publicKey?: string;
43
- /** Bitcoin provider URL for balance verification */
44
- bitcoinProviderUrl?: string;
45
43
  /** Selected term in months (override when contract state is zero) */
46
44
  selectedTerm?: number;
47
45
  }
@@ -14,6 +14,24 @@ export interface BtcExecuteSignParams {
14
14
  chainId: number;
15
15
  signer: Signer;
16
16
  }
17
+ /** One Bitcoin input the borrower authorizes the phase-2 signer to spend. */
18
+ export interface BtcExecuteInput {
19
+ txid: string;
20
+ vout: number;
21
+ /** Full output value in sats (feeds the BIP143 sighash). */
22
+ value: number;
23
+ }
24
+ /**
25
+ * Canonical hash of a Bitcoin input set (remediation P6/#8).
26
+ *
27
+ * CANONICALIZATION MUST MATCH
28
+ * `lit-actions/src/modules/bitcoin/utxo-set-hash.ts` byte-for-byte: entries
29
+ * normalized to `${txidLowercase}:${vout}:${value}`, sorted by (txid asc,
30
+ * vout asc), joined with "|", then keccak256 over the UTF-8 bytes. The Lit
31
+ * Action recomputes this over the `utxos[]` it actually signs, so an operator
32
+ * cannot spend inputs beyond the set hashed here.
33
+ */
34
+ export declare function hashBtcInputSet(utxos: BtcExecuteInput[]): string;
17
35
  export interface BtcExecuteSignedEnvelope {
18
36
  positionId: string;
19
37
  txid: string;
@@ -31,6 +49,12 @@ export interface BtcExecuteSignedEnvelope {
31
49
  userSignature: string;
32
50
  /** Address recovered from the signer (borrower). */
33
51
  borrowerAddress: string;
52
+ /**
53
+ * The FULL input set bound into the signature (P6/#8). The server must
54
+ * forward exactly this set to the phase-2 signer — any other set fails
55
+ * signature verification in the Lit Action.
56
+ */
57
+ utxos: BtcExecuteInput[];
34
58
  }
35
59
  /**
36
60
  * Build the quantum-aligned timestamp + EIP-191 signature the
@@ -57,4 +81,11 @@ export declare function buildBtcExecuteEnvelope(params: BtcExecuteSignParams & {
57
81
  satoshis: number;
58
82
  targetAddress: string;
59
83
  targetAmount: number;
84
+ /**
85
+ * FULL input set the phase-2 signer may spend (P6/#8). Omit for the
86
+ * common single-UTXO withdrawal — it defaults to exactly the authorized
87
+ * UTXO, the most restrictive set. Multi-UTXO consolidation withdrawals
88
+ * MUST list every input here; the authorized UTXO must be among them.
89
+ */
90
+ utxos?: BtcExecuteInput[];
60
91
  }): Promise<BtcExecuteSignedEnvelope>;
@@ -4,27 +4,24 @@
4
4
  */
5
5
  import { type Provider, type Signer, type TransactionResponse } from "ethers";
6
6
  /**
7
- * Cap on mintUCD's gasLimit. mintUCD dynamically sizes gasLimit via
8
- * estimateContractCallGasWithMargin() (real eth_estimateGas + 25% margin) on
9
- * mainnet, so this value is NOT normally what gets submitted -- it only
10
- * becomes the literal gasLimit if (a) the estimate comes back anomalously
11
- * high, or (b) eth_estimateGas itself fails (some RPCs return bare 0x even
12
- * for valid txs), in which case — after a static call proves the tx valid —
13
- * this raw value is used as the gasLimit on any chain.
14
- * Previously 10,000,000 with a comment claiming "mintUCD + LIT validation
15
- * can exceed 7M gas" -- no evidence of that was found across 16 real mint
16
- * transactions checked (1 mainnet, 15 recent Sepolia, 2026-07-02): the
17
- * dynamically-computed gasLimit actually submitted ranged 388,620-886,988.
18
- * Lowered to 2,000,000 (~2.25x headroom over the highest real value seen)
19
- * for consistency with MAKE_PAYMENT_GAS_CEILING/EXTEND_POSITION_GAS_CEILING's
20
- * ~1.5-2x-headroom convention -- a smaller margin than those because this
21
- * ceiling is a rarely-hit fallback/cap, not the everyday operating value, so
22
- * a little extra buffer for cold-storage variance is cheap insurance.
7
+ * Fixed-gas ceiling for mintUCD's broadcast. Since the gold-standard refactor
8
+ * (2026-07-08) this IS the everyday submitted gasLimit: mintUCD no longer runs
9
+ * eth_estimateGas (its round-trip latency re-introduced the quantum-boundary
10
+ * race the submission gate exists to prevent), matching makePayment /
11
+ * withdrawBTC / extendPosition. Grounded in 16 real mint transactions
12
+ * (1 mainnet, 15 recent Sepolia, 2026-07-02): the estimate-derived gasLimit
13
+ * actually submitted ranged 388,620-886,988, and those limits all succeeded —
14
+ * so 886,988 is a KNOWN-SUFFICIENT limit, not a bare gasUsed figure.
15
+ * 1,300,000 gives ~1.47x headroom over it, matching MAKE_PAYMENT_GAS_CEILING's
16
+ * ~1.5x convention. Deliberately NOT higher: wallets (MetaMask) reject a tx
17
+ * when balance < gasLimit × maxFeePerGas, so an oversized fixed ceiling
18
+ * (previously 2M as a rarely-hit cap) would block low-ETH users from an
19
+ * affordable mint.
23
20
  */
24
21
  export declare const MINT_UCD_GAS_CEILING: bigint;
25
22
  /**
26
- * Fixed-gas ceiling for the withdrawBTC raw-tx fallback (used when estimateGas
27
- * is unavailable on the RPC). The on-chain flow now spans PositionManager →
23
+ * Fixed-gas ceiling for withdrawBTC's broadcast (the everyday submitted
24
+ * gasLimit — no estimateGas runs on this path). The on-chain flow now spans PositionManager →
28
25
  * CollateralManager → LoanOperationsManager → BTCSpendAuthorizer plus the audit
29
26
  * #3 Chainlink feed-staleness gate and the BitcoinWithdrawalAddressRegistry
30
27
  * allowlist STATICCALL; observed end-to-end cost is ~1.0–1.3M gas, so the prior
@@ -89,12 +86,6 @@ export declare const LIQUIDATION_COMMIT_GAS_CEILING: bigint;
89
86
  * provisional until re-measured against a real liquidation.
90
87
  */
91
88
  export declare const LIQUIDATION_REVEAL_GAS_CEILING: bigint;
92
- /** Headroom on eth_estimateGas (25%). */
93
- export declare const MINT_UCD_ESTIMATE_MARGIN_BPS = 2500;
94
- /**
95
- * Returns gas limit with margin, capped, or null if estimateGas reverts/fails.
96
- */
97
- export declare function estimateContractCallGasWithMargin(provider: Provider, from: string, to: string, data: string, marginBps: number, ceiling: bigint): Promise<bigint | null>;
98
89
  export declare function resolveEip1559FeeFields(provider: Provider): Promise<{
99
90
  maxFeePerGas: bigint;
100
91
  maxPriorityFeePerGas: bigint;
@@ -10,6 +10,10 @@
10
10
  */
11
11
  /** `DeadZoneViolation()` selector — the stale-latest-block artifact we tolerate. */
12
12
  export declare const DEAD_ZONE_VIOLATION_SELECTOR = "0xbe4b82c1";
13
+ /** Solidity `Error(string)` (require/revert with a message). */
14
+ export declare const ERROR_STRING_SELECTOR = "0x08c379a0";
15
+ /** Solidity `Panic(uint256)` (assert failures, overflow, div-by-zero, …). */
16
+ export declare const PANIC_SELECTOR = "0x4e487b71";
13
17
  /**
14
18
  * Union of the custom-error selectors the mint/payment/quantum paths decode. Keep in
15
19
  * sync with OperationAuthorizationRegistry / PositionManager / LoanOperationsManager.
@@ -26,6 +30,11 @@ export interface DecodedRevert {
26
30
  /**
27
31
  * Extract the revert data / custom-error selector / known name from an ethers error,
28
32
  * probing every provider nesting (JsonRpc, Browser/MetaMask, wrapped `error.error`).
33
+ *
34
+ * Besides the custom-error selector map, decodes the two standard Solidity revert
35
+ * shapes so require-message reverts stay actionable when call sites rely solely on
36
+ * this decoder: `Error(string)` yields `name = 'Error("<message>")'` and
37
+ * `Panic(uint256)` yields `name = "Panic(0x<code>)"`.
29
38
  */
30
39
  export declare function decodeQuantumRevert(e: any): DecodedRevert;
31
40
  /** True iff the error decodes to `DeadZoneViolation()` from any provider nesting. */
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Shared submission-time safety for quantum-signed broadcasts
3
+ * (makePayment / mintUCD / withdrawBTC / extendPosition).
4
+ *
5
+ * One helper owns the dead-zone gate AND the pre-send simulate/re-simulate loop
6
+ * that used to exist as three subtly different inline copies in
7
+ * diamond-hands-sdk.ts. Callers encode calldata, call
8
+ * `assertSafeQuantumSubmission`, then broadcast ONCE with a fixed gas ceiling
9
+ * (`sendEip1559Transaction`) — never `estimateGas`, whose latency re-introduces
10
+ * the boundary race the gate exists to prevent, and never a re-broadcast.
11
+ *
12
+ * See docs/sdk-quantum-goldstandard-refactor-plan-2026-07-08.md.
13
+ */
14
+ /** Default cap on dead-zone-triggered re-simulations (mirrors old makePayment). */
15
+ export declare const MAX_DEADZONE_RESIMULATIONS = 3;
16
+ /**
17
+ * Thrown when the pre-send simulation proves the transaction would revert.
18
+ * Carries everything a caller needs to enrich the failure without re-probing
19
+ * the provider error itself:
20
+ *
21
+ * - `errorName` — decoded name, e.g. `"QuantumExpired()"` or `Error("msg")`
22
+ * (named to avoid shadowing `Error.name`).
23
+ * - `selector` — 4-byte selector, e.g. `"0x131d9a21"`.
24
+ * - `data` — FULL revert data (mintUCD decodes
25
+ * `DebtUpdateVerificationFailedDetailed` args from it).
26
+ * - `cause` — the original provider error (`reason` / `shortMessage`
27
+ * fallbacks, empty-data detection → `diagnoseMintUCD`).
28
+ *
29
+ * Only thrown for DECODABLE reverts (revert data present). Transport-level
30
+ * `eth_call` failures never raise this — see `assertSafeQuantumSubmission`.
31
+ */
32
+ export declare class QuantumRevertError extends Error {
33
+ readonly errorName: string | null;
34
+ readonly selector: string | null;
35
+ readonly data: string | null;
36
+ readonly cause: unknown;
37
+ constructor(errorName: string | null, selector: string | null, data: string | null, cause: unknown);
38
+ }
39
+ export interface SafeQuantumSubmissionOpts {
40
+ /** Provider used for the `eth_call` simulation (only `.call` is needed). */
41
+ provider: {
42
+ call(tx: {
43
+ to: string;
44
+ from: string;
45
+ data: string;
46
+ }): Promise<string>;
47
+ };
48
+ /** Target contract address. */
49
+ to: string;
50
+ /** Sender address — `eth_call` must run as the real sender (auth checks). */
51
+ from: string;
52
+ /** Encoded calldata; broadcast MUST send these exact bytes. */
53
+ data: string;
54
+ /** The quantum timestamp embedded in the LIT-signed payload. */
55
+ quantumTimestamp: number;
56
+ /** Cap on dead-zone re-simulations (default {@link MAX_DEADZONE_RESIMULATIONS}). */
57
+ maxResimulations?: number;
58
+ /** Clock seam (whole seconds). Drives BOTH the gate and the artifact test. */
59
+ now?: () => number;
60
+ /** Sleep seam, forwarded to the gate. */
61
+ sleep?: (ms: number) => Promise<void>;
62
+ /** Debug sink; called with human-readable progress lines. */
63
+ onDebug?: (msg: string) => void;
64
+ }
65
+ /**
66
+ * Gate + pre-send simulation loop. Resolves when it is safe to broadcast;
67
+ * throws {@link QuantumRevertError} on a real (non-artifact) decodable revert.
68
+ * Never broadcasts anything itself.
69
+ *
70
+ * Sequence:
71
+ * 1. **Gate** (`awaitSafeSubmissionWindow`): a NEXT-quantum signature mined in
72
+ * the last DEAD_ZONE_SECONDS of the current quantum reverts
73
+ * `DeadZoneViolation()` on-chain, so defer the send across the boundary
74
+ * when inclusion could land there.
75
+ * 2. **Simulate** (`eth_call`) and route the outcome:
76
+ * - success → safe to broadcast.
77
+ * - `DeadZoneViolation()` → `eth_call` runs against the LATEST block's
78
+ * (slightly stale) timestamp while the on-chain rule uses MINE time. If
79
+ * the signature is already the CURRENT quantum in real time it can never
80
+ * be dead-zoned on-chain — stale-block artifact, proceed. Otherwise the
81
+ * signature is still NEXT near the boundary: re-gate across it and
82
+ * re-simulate (bounded by `maxResimulations`).
83
+ * - any other DECODABLE revert → throw `QuantumRevertError` (fail fast: no
84
+ * broadcast, no gas burned on a doomed tx).
85
+ * - UNDECODABLE failure (no revert data: RPC timeout, rate limit, provider
86
+ * quirk) → log and proceed. The fixed-ceiling broadcast degrades to the
87
+ * pre-refactor blind-broadcast behavior instead of letting a flaky RPC
88
+ * block a valid money-path operation.
89
+ * 3. **Final re-gate**: the sim loop adds RPC round-trips after step 1; this
90
+ * closes the drift with pure clock math (no RPC). If it waits, the
91
+ * signature is CURRENT afterward and needs no further simulation.
92
+ */
93
+ export declare function assertSafeQuantumSubmission(opts: SafeQuantumSubmissionOpts): Promise<void>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Client-side persistence for the lit-ops-server session.
3
+ *
4
+ * The server accepts re-mints of the SAME signed login envelope until
5
+ * `issuedAt + REAUTH_WINDOW_SECONDS` (see lit-ops-server `login-handler.ts`),
6
+ * so caching the signed payload means an expired 15-minute JWT — or a full
7
+ * page reload — costs one silent API call instead of another wallet popup.
8
+ * Mirrors the frontend's `wallet-login-cache.ts` pattern for the Firebase
9
+ * `DhWebLogin` flow.
10
+ *
11
+ * Browser-safe: the default store auto-detects `localStorage` and degrades to
12
+ * a per-instance in-memory map elsewhere (Node, blocked storage), which keeps
13
+ * exactly the pre-persistence behavior.
14
+ */
15
+ import type { DhServerLoginPayload } from "./eip712-login";
16
+ /** Must match lit-ops-server's REAUTH_WINDOW_SECONDS (`eip712-digest.ts`). */
17
+ export declare const REAUTH_WINDOW_SECONDS: number;
18
+ /** Stop using a cached payload this long before the server would reject it. */
19
+ export declare const SAFETY_MARGIN_SECONDS: number;
20
+ /** One persisted session: the 24h re-mint credential plus the current JWT. */
21
+ export interface PersistedServerSession {
22
+ version: 1;
23
+ /** Lowercased signer address the payload authenticates. */
24
+ address: string;
25
+ chainId: number;
26
+ /** Normalized service endpoint (trailing slashes stripped) — the signed audience. */
27
+ endpoint: string;
28
+ payload: DhServerLoginPayload;
29
+ /** Current JWT, if still live — lets a reload skip even the silent re-mint call. */
30
+ token?: string;
31
+ /** JWT expiry, unix seconds. */
32
+ tokenExpiresAt?: number;
33
+ }
34
+ /**
35
+ * Pluggable persistence. Implementations must be best-effort and never throw
36
+ * out of `save`/`clear`; `load` returns null for absent or malformed entries.
37
+ */
38
+ export interface ServerSessionStore {
39
+ load(key: string): PersistedServerSession | null;
40
+ save(key: string, session: PersistedServerSession): void;
41
+ clear(key: string): void;
42
+ }
43
+ export declare function sessionStoreKey(address: string, chainId: number, endpoint: string): string;
44
+ export declare class LocalStorageSessionStore implements ServerSessionStore {
45
+ load(key: string): PersistedServerSession | null;
46
+ save(key: string, session: PersistedServerSession): void;
47
+ clear(key: string): void;
48
+ }
49
+ export declare class MemorySessionStore implements ServerSessionStore {
50
+ private readonly entries;
51
+ load(key: string): PersistedServerSession | null;
52
+ save(key: string, session: PersistedServerSession): void;
53
+ clear(key: string): void;
54
+ }
55
+ /** `localStorage` when usable (browser, not blocked), else per-instance memory. */
56
+ export declare function createDefaultSessionStore(): ServerSessionStore;
@@ -1,50 +1,103 @@
1
1
  /**
2
- * In-memory session JWT manager for lit-ops-server.
2
+ * Session JWT manager for lit-ops-server.
3
3
  *
4
4
  * The server's `POST /api/auth/login` route accepts an EIP-712-signed
5
- * `DhServerLogin` envelope and returns an HS256 JWT with a 15-minute TTL.
6
- * This helper:
5
+ * `DiamondHands` envelope and returns an HS256 JWT with a 15-minute TTL. The
6
+ * same signed envelope may silently re-mint fresh JWTs until
7
+ * `issuedAt + REAUTH_WINDOW_SECONDS` (24h), so a wallet signature is only
8
+ * needed once per window. This helper:
7
9
  *
8
10
  * - caches the JWT until ~30s before expiry,
11
+ * - persists the signed envelope (+ current JWT) via a pluggable store —
12
+ * `localStorage` in browsers by default — so page reloads and expired
13
+ * JWTs recover WITHOUT another wallet popup,
9
14
  * - coalesces concurrent callers behind a single in-flight Promise so we
10
15
  * don't spam `/api/auth/login` on parallel SDK calls,
11
16
  * - exposes `getValidToken()` returning the live token and
12
17
  * `getAuthHeader()` returning a ready-to-spread `{ Authorization: ... }`
13
18
  * object.
14
19
  *
15
- * Browser-safe — no filesystem touches. The CLI mirrors this shape but
16
- * persists to `~/.diamond-hands/session.json` for cross-invocation reuse.
20
+ * Browser-safe — no filesystem touches; outside browsers the store degrades
21
+ * to per-instance memory. The CLI mirrors this shape but persists to
22
+ * `~/.diamond-hands/session.json` for cross-invocation reuse.
17
23
  */
18
24
  import type { Signer } from "ethers";
25
+ import { type ServerSessionStore } from "./server-session-store";
19
26
  export interface ServerSessionOptions {
20
27
  signer: Signer;
21
28
  serviceEndpoint: string;
22
29
  chainId: number;
30
+ /**
31
+ * Persistence for the signed login envelope + JWT. Defaults to
32
+ * `localStorage` in browsers and per-instance memory elsewhere.
33
+ */
34
+ sessionStore?: ServerSessionStore;
35
+ /** Set false to disable persistence entirely (fresh signature per instance + expiry). */
36
+ persistSession?: boolean;
37
+ /**
38
+ * Fires immediately before a wallet signature is requested — i.e. only when
39
+ * neither the cached JWT nor the persisted envelope could renew the session,
40
+ * never on the silent paths. Lets UIs show a "check your wallet" prompt.
41
+ */
42
+ onSignaturePrompt?: () => void;
23
43
  /** Override for tests. */
24
44
  now?: () => number;
25
45
  /** Override for tests. */
26
46
  fetchImpl?: typeof fetch;
27
47
  }
48
+ /** Login failure carrying the HTTP status so callers can tell rejection from outage. */
49
+ export declare class ServerLoginError extends Error {
50
+ readonly status: number;
51
+ constructor(message: string, status: number);
52
+ }
28
53
  export declare class ServerSession {
29
54
  private readonly signer;
30
55
  private readonly serviceEndpoint;
31
56
  private readonly chainId;
57
+ private readonly store;
58
+ private readonly onSignaturePrompt?;
32
59
  private readonly now;
33
60
  private readonly fetchImpl;
34
61
  private cached;
35
62
  private inFlight;
63
+ /**
64
+ * Store key for the current signer, memoized on first use so the sync
65
+ * `clear()` can drop the persisted token without an async address lookup.
66
+ */
67
+ private lastStoreKey;
36
68
  constructor(opts: ServerSessionOptions);
37
69
  /** Returns a JWT good for at least `REFRESH_LEEWAY_SECONDS` more seconds. */
38
70
  getValidToken(): Promise<string>;
39
71
  /** Convenience for fetch: spread directly into `headers`. */
40
72
  getAuthHeader(): Promise<Record<string, string>>;
41
- /** Drop the cached token (e.g. on 401 from server). Next call re-logs in. */
73
+ /**
74
+ * Drop the cached token (e.g. on 401 from server). The persisted envelope is
75
+ * kept — the next call re-mints silently — but its stored token copy is
76
+ * dropped too so a dead JWT can't be re-adopted from the store.
77
+ */
42
78
  clear(): void;
43
79
  /**
44
- * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`).
45
- * Clears the local cache regardless of server response so the client stops
46
- * presenting the token.
80
+ * Drop the session AND the persisted re-mint envelope (wallet disconnect /
81
+ * explicit sign-out paths — anything that must remove the 24h credential
82
+ * from storage).
83
+ */
84
+ clearPersisted(): Promise<void>;
85
+ /**
86
+ * Revoke the current session JWT on lit-ops-server (`POST /api/auth/logout`)
87
+ * — which also tombstones the envelope's re-mint window server-side — and
88
+ * remove the persisted credential locally. Clears the local cache regardless
89
+ * of server response so the client stops presenting the token.
47
90
  */
48
91
  logout(): Promise<void>;
49
92
  private getOrRefresh;
93
+ private login;
94
+ /**
95
+ * Load the persisted session for the current signer, validating that it
96
+ * belongs to this address/chain/endpoint and that the envelope is still
97
+ * inside the re-auth window (with a safety margin). Invalid or expired
98
+ * entries are evicted.
99
+ */
100
+ private loadPersisted;
101
+ /** Best-effort save of the envelope + live JWT for silent recovery. */
102
+ private persist;
50
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.300",
3
+ "version": "0.0.303",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol - Bitcoin-backed lending with LIT Protocol PKPs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -82,8 +82,8 @@
82
82
  },
83
83
  "sideEffects": false,
84
84
  "dependencies": {
85
- "@gvnrdao/dh-lit-actions": "^0.0.312",
86
- "@gvnrdao/dh-lit-ops": "^0.0.302",
85
+ "@gvnrdao/dh-lit-actions": "^0.0.314",
86
+ "@gvnrdao/dh-lit-ops": "^0.0.304",
87
87
  "@noble/hashes": "^1.5.0",
88
88
  "axios": "^1.17.0",
89
89
  "bech32": "^2.0.0",