@gvnrdao/dh-sdk 0.0.306 → 0.0.308

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,7 @@ import { SDKError } from "../utils/error-handler";
18
18
  import type { CreateLoanRequest, CreateLoanResult, LoanDataDetail, UCDMintRequest, UCDMintResult, PartialPaymentRequest, PartialPaymentResult, BTCWithdrawalResult, RenewPositionRequest, RenewPositionResult, LiquidationRequest, LiquidationResult, ConfirmBalanceRequest, ConfirmBalanceResult, TermsWithFeesResult } from "../interfaces/chunks/loan-operations.i";
19
19
  import type { DiamondHandsSDKConfig } from "../interfaces/chunks/config.i";
20
20
  import type { PKPData } from "../interfaces/chunks/pkp-integration.i";
21
+ import { type ReconciledWithdrawal } from "../utils/withdrawal-reconciliation.utils";
21
22
  import { ContractManager } from "./contract/contract-manager.module";
22
23
  import { WithdrawalAddressModule } from "./withdrawal-address/withdrawal-address.module";
23
24
  import { BitcoinOperations } from "./bitcoin/bitcoin-operations.module";
@@ -545,7 +546,7 @@ export declare class DiamondHandsSDK {
545
546
  * been broadcast to the Bitcoin network. An empty array means no pending withdrawals.
546
547
  *
547
548
  * @param positionId - Position identifier
548
- * @returns Array of pending withdrawals, each including a pre-computed utxoKey for use with cancelPendingWithdrawal
549
+ * @returns Array of pending withdrawals, each including a pre-computed utxoKey (the key recoverStaleSpend and the admin clearing path operate on)
549
550
  */
550
551
  getPendingWithdrawals(positionId: string): Promise<Array<{
551
552
  txid: string;
@@ -557,17 +558,30 @@ export declare class DiamondHandsSDK {
557
558
  utxoKey: string;
558
559
  }>>;
559
560
  /**
560
- * Cancel a pending BTC withdrawal
561
- *
562
- * Removes a stale authorized spend from BTCSpendAuthorizer, unlocking the UTXO
563
- * and allowing the borrower to retry the withdrawal from scratch.
564
- * Only the position's borrower may call this — requires a connected signer.
565
- *
566
- * @param positionId - Position identifier
567
- * @param txid - Bitcoin UTXO transaction ID (from getPendingWithdrawals)
568
- * @param vout - Output index (from getPendingWithdrawals)
569
- * @returns Transaction hash of the cancellation EVM transaction
570
- */
561
+ * Reconcile every pending withdrawal against BITCOIN truth (incident
562
+ * 2026-07-22): `getPendingWithdrawals` only reflects on-chain
563
+ * authorizations, and the contract can never know whether the Phase-2 BTC
564
+ * broadcast happened. Callers MUST use the returned `status` to decide what
565
+ * to offer:
566
+ * - EXECUTABLE → offer Execute (the only status that may).
567
+ * - EXECUTED → auto-clear; `spendingTxid` is the completion proof —
568
+ * feed it to recoverStaleSpend to clear the reservation.
569
+ * - SPENT_MISMATCH → unexecutable; recoverStaleSpend with the spending txid
570
+ * as invalidator clears the reservation.
571
+ * - CORRUPT → authorization contradicts the chain (e.g. declared
572
+ * satoshis ≠ real output value); Execute can only die at
573
+ * the signer guard, and no borrower-side cancel exists
574
+ * (audit #62210) — clearing needs the AdminModule
575
+ * cancelStaleSpendByAdmin operator override.
576
+ * - UNFUNDED → funding tx unknown/unconfirmed; wait.
577
+ *
578
+ * @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
579
+ * `/v1/proxy/esplora/<network>`). Falls back to
580
+ * `config.bitcoinProviders[0].url`; throws when neither is configured.
581
+ */
582
+ reconcilePendingWithdrawals(positionId: string, opts?: {
583
+ esploraBaseUrl?: string;
584
+ }): Promise<Array<ReconciledWithdrawal<Awaited<ReturnType<DiamondHandsSDK["getPendingWithdrawals"]>>[number]>>>;
571
585
  /**
572
586
  * Recover (clear) a stale BTC reservation via LIT-attested proof.
573
587
  *
@@ -617,6 +631,59 @@ export declare class DiamondHandsSDK {
617
631
  error?: string;
618
632
  failedStep?: "attestation" | "submit" | "config";
619
633
  }>;
634
+ /**
635
+ * Sign + broadcast the PKP vault→vault self-spend that invalidates one
636
+ * stuck authorized outpoint (btc-utxo-invalidator 1.0.0). Once the returned
637
+ * `invalidatorTxid` reaches >=6 Bitcoin confirmations, `recoverStaleSpend`
638
+ * clears the on-chain reservation — the trustless replacement for the
639
+ * AdminModule cancelStaleSpendByAdmin path on unspent stuck/corrupt
640
+ * reservations (incident 2026-07-22). Requires the borrower (or position
641
+ * delegate) signer; the signature is scoped to exactly this outpoint.
642
+ */
643
+ invalidateStaleSpend(params: {
644
+ positionId: string;
645
+ utxoTxid: string;
646
+ utxoVout: number;
647
+ rpcUrl?: string;
648
+ }): Promise<{
649
+ success: boolean;
650
+ alreadySpent?: {
651
+ spendingTxid: string;
652
+ };
653
+ invalidatorTxid?: string;
654
+ invalidatorAmount?: number;
655
+ networkFee?: number;
656
+ /** True when the on-chain record's declared value ≠ the real output (corrupt-record recovery). */
657
+ valueMismatch?: boolean;
658
+ error?: string;
659
+ failedStep?: string;
660
+ }>;
661
+ /**
662
+ * Resumable, stateless clearing state machine for one stuck reservation —
663
+ * Bitcoin itself is the state store, so every call either advances the flow
664
+ * one step or reports where it stands (survives reloads and the multi-day
665
+ * 6-confirmation wait; no internal polling).
666
+ *
667
+ * (a) reservation gone → phase "cleared"
668
+ * (b) outpoint spent, >=6 confs → recoverStaleSpend → "cleared"/"failed"
669
+ * outpoint spent, <6 confs → "awaiting-confirmations" (x/6)
670
+ * (c) outpoint unspent → invalidateStaleSpend → "invalidator-broadcast"
671
+ */
672
+ clearStuckReservation(params: {
673
+ positionId: string;
674
+ utxoTxid: string;
675
+ utxoVout: number;
676
+ esploraBaseUrl?: string;
677
+ }): Promise<{
678
+ phase: "cleared" | "awaiting-confirmations" | "invalidator-broadcast" | "failed";
679
+ invalidatorTxid?: string;
680
+ confirmations?: number;
681
+ required?: number;
682
+ transactionHash?: string;
683
+ classification?: string;
684
+ valueMismatch?: boolean;
685
+ error?: string;
686
+ }>;
620
687
  /**
621
688
  * Withdraw BTC and execute transfer (Complete Flow)
622
689
  *
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Borrower authorization envelope for the btc-utxo-invalidator Lit Action —
3
+ * the SDK-side mirror of `AuthorizationModule.verifyRecoveryAuthorization`
4
+ * (lit-actions/src/modules/authorization.module.ts:539). Field order and
5
+ * types MUST match byte-for-byte; the action recomputes this hash and
6
+ * EIP-191-recovers the borrower/delegate from it.
7
+ *
8
+ * NOTE: the lit-actions side hashes with ethers v5 `solidityKeccak256`, this
9
+ * side with v6 `solidityPackedKeccak256` — identical packed encoding; the
10
+ * unit test asserts byte-equality of the preimages so drift cannot sneak in.
11
+ */
12
+ import { type Signer } from "ethers";
13
+ /** Must equal RECOVERY_INVALIDATE_ACTION_STRING in lit-actions constants. */
14
+ export declare const RECOVERY_INVALIDATE_ACTION = "btc-utxo-invalidator";
15
+ export interface InvalidateStaleSpendEnvelope {
16
+ positionId: string;
17
+ utxoTxid: string;
18
+ utxoVout: number;
19
+ /** Quantum-aligned unix seconds bound into the signature. */
20
+ timestamp: number;
21
+ chainId: number;
22
+ action: typeof RECOVERY_INVALIDATE_ACTION;
23
+ signature: string;
24
+ borrowerAddress: string;
25
+ }
26
+ /** The exact packed hash `verifyRecoveryAuthorization` recomputes in-TEE. */
27
+ export declare function buildInvalidateMessageHash(params: {
28
+ positionId: string;
29
+ timestamp: number;
30
+ chainId: number;
31
+ utxoTxid: string;
32
+ utxoVout: number;
33
+ }): string;
34
+ /**
35
+ * Build + sign the invalidate envelope with a quantum-aligned timestamp.
36
+ * The signature is scoped to exactly one (positionId, utxoTxid, utxoVout)
37
+ * recovery on one chain and cannot be replayed as any other authorization
38
+ * (the action string is pinned into the hash).
39
+ */
40
+ export declare function buildInvalidateStaleSpendEnvelope(params: {
41
+ positionId: string;
42
+ utxoTxid: string;
43
+ utxoVout: number;
44
+ chainId: number;
45
+ signer: Signer;
46
+ }): Promise<InvalidateStaleSpendEnvelope>;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pending-withdrawal reconciliation — classify each on-chain authorized spend
3
+ * (`BTCSpendAuthorizer.getAuthorizedSpends`) against BITCOIN truth before any
4
+ * UI offers "Execute" or any server invokes the TEE signer.
5
+ *
6
+ * Why this exists (incident 2026-07-22, position 0x992d5c…): the contract can
7
+ * never know whether a Phase-2 BTC broadcast happened, and a pre-P6/#8
8
+ * authorization could record a `(vout, satoshis)` pair that never matched the
9
+ * chain (declared 96,049 vs on-chain 33,000). Executing such an entry can only
10
+ * die at the signer's parent-fetch guard; and an entry whose outpoint was
11
+ * already spent paying the target is DONE and must auto-clear — while an
12
+ * entry whose funding tx merely confirmed must NOT be cleared as "complete".
13
+ *
14
+ * Statuses:
15
+ * - EXECUTABLE — outpoint confirmed, unspent, values coherent → offer Execute.
16
+ * - EXECUTED — outpoint spent by a tx that pays the authorized target →
17
+ * auto-clear, show `spendingTxid` as the completion proof.
18
+ * - SPENT_MISMATCH — outpoint spent but the spending tx pays the target
19
+ * nothing → unexecutable; recoverStaleSpend clears it.
20
+ * - CORRUPT — authorization contradicts the chain (declared value ≠
21
+ * real output value, targetAmount > real value, or vout
22
+ * out of range) → operator recovery (admin clearing path);
23
+ * never Execute.
24
+ * - UNFUNDED — funding tx unknown/unconfirmed → wait; no Execute yet.
25
+ *
26
+ * Esplora `spent: true` claims are only trusted when the spending tx can be
27
+ * fetched AND provably includes this outpoint among its inputs — the regtest
28
+ * faucet's esplora answers `spent: true` for arbitrary txids, and a false
29
+ * "executed" here would silently dismiss a withdrawal the user was never paid
30
+ * for. Unverifiable claims classify as EXECUTABLE (chain truth: no proven
31
+ * spend); a genuinely-spent outpoint then simply fails downstream, safely.
32
+ */
33
+ export type PendingWithdrawalStatus = "EXECUTABLE" | "EXECUTED" | "SPENT_MISMATCH" | "CORRUPT" | "UNFUNDED";
34
+ export interface AuthorizedSpendLike {
35
+ txid: string;
36
+ vout: number;
37
+ satoshis: number;
38
+ targetAddress: string;
39
+ targetAmount: number;
40
+ }
41
+ export interface ReconciledWithdrawal<T extends AuthorizedSpendLike = AuthorizedSpendLike> {
42
+ spend: T;
43
+ status: PendingWithdrawalStatus;
44
+ /** Human-readable, single-sentence explanation of the classification. */
45
+ reason: string;
46
+ /** Real value of the referenced outpoint, when the funding tx is known. */
47
+ onChainOutputValue?: number;
48
+ /** The verified spending tx, for EXECUTED / SPENT_MISMATCH. */
49
+ spendingTxid?: string;
50
+ /** Sats the verified spending tx pays to `targetAddress` (EXECUTED only). */
51
+ paidToTargetSats?: number;
52
+ }
53
+ /**
54
+ * HTTP seam: GET `url`, resolve `{ status, body }` (body null on non-JSON).
55
+ * Injectable for tests; the default uses global `fetch` (node ≥18 + browsers).
56
+ * Transport failures throw — reconciliation must fail LOUD, never classify on
57
+ * missing data.
58
+ */
59
+ export type HttpGetJson = (url: string) => Promise<{
60
+ status: number;
61
+ body: unknown;
62
+ }>;
63
+ export declare const defaultHttpGetJson: HttpGetJson;
64
+ /** Classify one authorized spend against the esplora at `esploraBaseUrl`. */
65
+ export declare function reconcileAuthorizedSpend<T extends AuthorizedSpendLike>(spend: T, esploraBaseUrl: string, httpGetJson?: HttpGetJson): Promise<ReconciledWithdrawal<T>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.306",
3
+ "version": "0.0.308",
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.315",
86
- "@gvnrdao/dh-lit-ops": "0.0.306",
85
+ "@gvnrdao/dh-lit-actions": "^0.0.316",
86
+ "@gvnrdao/dh-lit-ops": "^0.0.307",
87
87
  "@noble/hashes": "^1.5.0",
88
88
  "axios": "^1.17.0",
89
89
  "bech32": "^2.0.0",