@gvnrdao/dh-sdk 0.0.307 → 0.0.309

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.mjs CHANGED
@@ -1514,6 +1514,19 @@ var require_pkg_src = __commonJS({
1514
1514
  publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
1515
1515
  ethAddress: "0xc7e1588Db94Bbe92Ab209B833235144Fc5b97607"
1516
1516
  }
1517
+ },
1518
+ btcUtxoInvalidator: {
1519
+ cid: "QmRue1EAjuJwCDTtTH4czdkkmDEfNpT7iudvwtjaRs6AdA",
1520
+ authorizedCidHex: cidToHex(
1521
+ "QmRue1EAjuJwCDTtTH4czdkkmDEfNpT7iudvwtjaRs6AdA"
1522
+ ),
1523
+ name: "Btc Utxo Invalidator",
1524
+ description: "Production Btc Utxo Invalidator",
1525
+ version: "1.0.0",
1526
+ deployed: true,
1527
+ deployedAt: 1784801043540,
1528
+ size: 60717,
1529
+ hash: "2ca9736edf3d54bba7445491920cba28429029c32a2f2830cad831a37a3cb310"
1517
1530
  }
1518
1531
  };
1519
1532
  function getDeploymentsForNetwork(network) {
@@ -2259,7 +2272,7 @@ ${errorReport}`);
2259
2272
  }
2260
2273
  init_debug_logger();
2261
2274
  init_session_signature_cache();
2262
- var import_ethers16 = __require("ethers");
2275
+ var import_ethers17 = __require("ethers");
2263
2276
  var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
2264
2277
  var GRACE_PERIOD_DAYS = 30;
2265
2278
  var SATOSHIS_PER_BITCOIN = 100000000n;
@@ -5089,12 +5102,12 @@ ${auth.clientId}`;
5089
5102
  }
5090
5103
  async function executeVaultSnapshot(params) {
5091
5104
  global.ethers = {
5092
- ...import_ethers16.ethers,
5105
+ ...import_ethers17.ethers,
5093
5106
  providers: {
5094
- StaticJsonRpcProvider: import_ethers16.ethers.JsonRpcProvider
5107
+ StaticJsonRpcProvider: import_ethers17.ethers.JsonRpcProvider
5095
5108
  },
5096
- Contract: import_ethers16.ethers.Contract,
5097
- utils: import_ethers16.ethers
5109
+ Contract: import_ethers17.ethers.Contract,
5110
+ utils: import_ethers17.ethers
5098
5111
  // v6 moved utils to top level
5099
5112
  };
5100
5113
  global.Lit = {
@@ -5552,7 +5565,7 @@ var init_network_configs = __esm({
5552
5565
  });
5553
5566
 
5554
5567
  // src/modules/diamond-hands-sdk.ts
5555
- import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2565, solidityPackedKeccak256 as solidityPackedKeccak2564, toUtf8Bytes as toUtf8Bytes5, getBytes as getBytes4, zeroPadValue as zeroPadValue4, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
5568
+ import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2566, solidityPackedKeccak256 as solidityPackedKeccak2565, toUtf8Bytes as toUtf8Bytes6, getBytes as getBytes5, zeroPadValue as zeroPadValue5, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
5556
5569
 
5557
5570
  // src/types/result.ts
5558
5571
  function success(value) {
@@ -6413,7 +6426,7 @@ async function generateExtendAuthorization(positionId, newTerm, chainId, signerO
6413
6426
  import { hexlify, randomBytes } from "ethers";
6414
6427
  function buildLoginDomain(chainId) {
6415
6428
  return {
6416
- name: "Diamond Hands lit-ops-server",
6429
+ name: "Diamond Hands",
6417
6430
  version: "1",
6418
6431
  chainId
6419
6432
  };
@@ -6528,6 +6541,8 @@ function createDefaultSessionStore() {
6528
6541
 
6529
6542
  // src/utils/server-session.ts
6530
6543
  var REFRESH_LEEWAY_SECONDS = 30;
6544
+ var LOGIN_RETRY_BASE_MS = 3e4;
6545
+ var LOGIN_RETRY_MAX_MS = 5 * 6e4;
6531
6546
  var ServerLoginError = class extends Error {
6532
6547
  constructor(message, status) {
6533
6548
  super(message);
@@ -6548,6 +6563,11 @@ var ServerSession = class {
6548
6563
  fetchImpl;
6549
6564
  cached = null;
6550
6565
  inFlight = null;
6566
+ /**
6567
+ * Set when a prompted login failed; suppresses further prompts until `until`.
6568
+ * `error` is replayed to callers so they still see the real failure reason.
6569
+ */
6570
+ promptBackoff = null;
6551
6571
  /**
6552
6572
  * Bumped by `clearPersisted()` (logout / disconnect). An in-flight
6553
6573
  * `getOrRefresh()` captures this at start and refuses to write its freshly
@@ -6622,6 +6642,7 @@ var ServerSession = class {
6622
6642
  this.generation += 1;
6623
6643
  this.cached = null;
6624
6644
  this.inFlight = null;
6645
+ this.promptBackoff = null;
6625
6646
  if (!this.store)
6626
6647
  return;
6627
6648
  try {
@@ -6718,19 +6739,39 @@ var ServerSession = class {
6718
6739
  this.inFlight = (async () => {
6719
6740
  try {
6720
6741
  const existing = await this.tryResolveSilently(gen);
6721
- if (existing)
6742
+ if (existing) {
6743
+ this.promptBackoff = null;
6722
6744
  return existing;
6745
+ }
6746
+ if (this.promptBackoff && this.now() * 1e3 < this.promptBackoff.until) {
6747
+ throw this.promptBackoff.error;
6748
+ }
6723
6749
  this.onSignaturePrompt?.();
6724
- const payload = await buildSignedLoginPayload(
6725
- this.signer,
6726
- this.chainId,
6727
- this.serviceEndpoint
6728
- );
6729
- const session = await this.login(payload);
6730
- if (notCleared()) {
6731
- this.persist(payload, session);
6732
- this.cached = session;
6750
+ let session;
6751
+ try {
6752
+ const payload = await buildSignedLoginPayload(
6753
+ this.signer,
6754
+ this.chainId,
6755
+ this.serviceEndpoint
6756
+ );
6757
+ session = await this.login(payload);
6758
+ if (notCleared()) {
6759
+ this.persist(payload, session);
6760
+ this.cached = session;
6761
+ }
6762
+ } catch (error) {
6763
+ const failures = (this.promptBackoff?.failures ?? 0) + 1;
6764
+ this.promptBackoff = {
6765
+ failures,
6766
+ error,
6767
+ until: this.now() * 1e3 + Math.min(
6768
+ LOGIN_RETRY_BASE_MS * 2 ** (failures - 1),
6769
+ LOGIN_RETRY_MAX_MS
6770
+ )
6771
+ };
6772
+ throw error;
6733
6773
  }
6774
+ this.promptBackoff = null;
6734
6775
  return session;
6735
6776
  } finally {
6736
6777
  if (notCleared())
@@ -7068,10 +7109,87 @@ async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = def
7068
7109
  };
7069
7110
  }
7070
7111
 
7071
- // src/utils/btc-withdrawal-message.ts
7072
- import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
7112
+ // src/utils/stale-spend-invalidate-message.ts
7113
+ import {
7114
+ keccak256 as keccak2563,
7115
+ toUtf8Bytes as toUtf8Bytes3,
7116
+ solidityPackedKeccak256 as solidityPackedKeccak2563,
7117
+ getBytes as getBytes3,
7118
+ zeroPadValue
7119
+ } from "ethers";
7120
+ var RECOVERY_INVALIDATE_ACTION = "btc-utxo-invalidator";
7073
7121
  var QUANTUM_SECONDS = 60;
7074
7122
  var QUANTUM_SAFE_THRESHOLD = 40;
7123
+ function buildInvalidateMessageHash(params) {
7124
+ const positionIdBytes32 = zeroPadValue(
7125
+ params.positionId.startsWith("0x") ? params.positionId : `0x${params.positionId}`,
7126
+ 32
7127
+ );
7128
+ const actionHash = keccak2563(toUtf8Bytes3(RECOVERY_INVALIDATE_ACTION));
7129
+ return solidityPackedKeccak2563(
7130
+ [
7131
+ "bytes32",
7132
+ // positionId
7133
+ "uint256",
7134
+ // timestamp
7135
+ "uint256",
7136
+ // chainId
7137
+ "string",
7138
+ // utxoTxid (display order)
7139
+ "uint32",
7140
+ // utxoVout
7141
+ "bytes32"
7142
+ // actionHash
7143
+ ],
7144
+ [
7145
+ positionIdBytes32,
7146
+ params.timestamp,
7147
+ params.chainId,
7148
+ params.utxoTxid,
7149
+ params.utxoVout,
7150
+ actionHash
7151
+ ]
7152
+ );
7153
+ }
7154
+ async function buildInvalidateStaleSpendEnvelope(params) {
7155
+ const { positionId, utxoTxid, utxoVout, chainId, signer } = params;
7156
+ const nowSec = Date.now() / 1e3;
7157
+ const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
7158
+ const secsIntoQuantum = nowSec - currentQuantumStart;
7159
+ let timestamp;
7160
+ if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
7161
+ const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
7162
+ const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
7163
+ await new Promise((r) => setTimeout(r, waitMs));
7164
+ timestamp = nextQuantumStart;
7165
+ } else {
7166
+ timestamp = currentQuantumStart;
7167
+ }
7168
+ const messageHash = buildInvalidateMessageHash({
7169
+ positionId,
7170
+ timestamp,
7171
+ chainId,
7172
+ utxoTxid,
7173
+ utxoVout
7174
+ });
7175
+ const signature = await signer.signMessage(getBytes3(messageHash));
7176
+ const borrowerAddress = await signer.getAddress();
7177
+ return {
7178
+ positionId,
7179
+ utxoTxid,
7180
+ utxoVout,
7181
+ timestamp,
7182
+ chainId,
7183
+ action: RECOVERY_INVALIDATE_ACTION,
7184
+ signature,
7185
+ borrowerAddress
7186
+ };
7187
+ }
7188
+
7189
+ // src/utils/btc-withdrawal-message.ts
7190
+ import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, solidityPackedKeccak256 as solidityPackedKeccak2564, getBytes as getBytes4, zeroPadValue as zeroPadValue2 } from "ethers";
7191
+ var QUANTUM_SECONDS2 = 60;
7192
+ var QUANTUM_SAFE_THRESHOLD2 = 40;
7075
7193
  function hashBtcInputSet(utxos) {
7076
7194
  if (!Array.isArray(utxos) || utxos.length === 0) {
7077
7195
  throw new Error("hashBtcInputSet: at least one input is required");
@@ -7100,7 +7218,7 @@ function hashBtcInputSet(utxos) {
7100
7218
  seen.add(key);
7101
7219
  }
7102
7220
  const canonical = normalized.sort((a, b) => a.txid < b.txid ? -1 : a.txid > b.txid ? 1 : a.vout - b.vout).map((u) => `${u.txid}:${u.vout}:${u.value}`).join("|");
7103
- return keccak2563(toUtf8Bytes3(canonical));
7221
+ return keccak2564(toUtf8Bytes4(canonical));
7104
7222
  }
7105
7223
  async function buildBtcExecuteEnvelope(params) {
7106
7224
  const {
@@ -7134,26 +7252,26 @@ async function buildBtcExecuteEnvelope(params) {
7134
7252
  }
7135
7253
  const utxosHash = hashBtcInputSet(utxos);
7136
7254
  const nowSec = Date.now() / 1e3;
7137
- const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
7255
+ const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS2) * QUANTUM_SECONDS2;
7138
7256
  const secsIntoQuantum = nowSec - currentQuantumStart;
7139
7257
  let timestamp;
7140
- if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
7141
- const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
7258
+ if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD2) {
7259
+ const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS2;
7142
7260
  const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
7143
7261
  await new Promise((r) => setTimeout(r, waitMs));
7144
7262
  timestamp = nextQuantumStart;
7145
7263
  } else {
7146
7264
  timestamp = currentQuantumStart;
7147
7265
  }
7148
- const actionHash = keccak2563(
7149
- toUtf8Bytes3("execute-btc-withdrawal")
7266
+ const actionHash = keccak2564(
7267
+ toUtf8Bytes4("execute-btc-withdrawal")
7150
7268
  );
7151
7269
  const utxoIdentifier = `${txid}:${vout}`;
7152
- const positionIdBytes32 = zeroPadValue(
7270
+ const positionIdBytes32 = zeroPadValue2(
7153
7271
  positionId.startsWith("0x") ? positionId : `0x${positionId}`,
7154
7272
  32
7155
7273
  );
7156
- const messageHash = solidityPackedKeccak2563(
7274
+ const messageHash = solidityPackedKeccak2564(
7157
7275
  [
7158
7276
  "bytes32",
7159
7277
  // positionId
@@ -7186,7 +7304,7 @@ async function buildBtcExecuteEnvelope(params) {
7186
7304
  actionHash
7187
7305
  ]
7188
7306
  );
7189
- const userSignature = await signer.signMessage(getBytes3(messageHash));
7307
+ const userSignature = await signer.signMessage(getBytes4(messageHash));
7190
7308
  const borrowerAddress = await signer.getAddress();
7191
7309
  return {
7192
7310
  positionId,
@@ -7250,7 +7368,7 @@ function maybeTemperSignature(signature, shouldTemper, operationName) {
7250
7368
  }
7251
7369
 
7252
7370
  // src/utils/address-conversion.utils.ts
7253
- import { zeroPadValue as zeroPadValue2 } from "ethers";
7371
+ import { zeroPadValue as zeroPadValue3 } from "ethers";
7254
7372
 
7255
7373
  // src/utils/chunks/bitcoin-utils.ts
7256
7374
  import { sha256 as nobleSha256 } from "@noble/hashes/sha256";
@@ -8834,7 +8952,7 @@ function createPKPManager(config) {
8834
8952
  }
8835
8953
 
8836
8954
  // src/modules/loan/loan-creator.module.ts
8837
- import { Interface as Interface2, zeroPadValue as zeroPadValue3, toBeHex, SigningKey } from "ethers";
8955
+ import { Interface as Interface2, zeroPadValue as zeroPadValue4, toBeHex, SigningKey } from "ethers";
8838
8956
  var import_dh_lit_actions = __toESM(require_pkg_src());
8839
8957
  var LoanCreator = class {
8840
8958
  config;
@@ -9164,7 +9282,7 @@ var LoanCreator = class {
9164
9282
  try {
9165
9283
  const mainnetVaultAddress = bitcoinAddresses.mainnet;
9166
9284
  const regtestVaultAddress = bitcoinAddresses.regtest;
9167
- const pkpIdBytes32 = zeroPadValue3(
9285
+ const pkpIdBytes32 = zeroPadValue4(
9168
9286
  toBeHex(BigInt(pkpData.tokenId)),
9169
9287
  32
9170
9288
  );
@@ -9345,7 +9463,7 @@ var LoanCreator = class {
9345
9463
  }
9346
9464
  }
9347
9465
  if (this.config.debug) {
9348
- const pkpIdBytes32Check = zeroPadValue3(
9466
+ const pkpIdBytes32Check = zeroPadValue4(
9349
9467
  toBeHex(BigInt(pkpData.tokenId)),
9350
9468
  32
9351
9469
  );
@@ -10108,7 +10226,7 @@ function createLoanQuery(config) {
10108
10226
  }
10109
10227
 
10110
10228
  // src/modules/withdrawal-address/withdrawal-address.module.ts
10111
- import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, ZeroAddress as ZeroAddress2 } from "ethers";
10229
+ import { keccak256 as keccak2565, toUtf8Bytes as toUtf8Bytes5, ZeroAddress as ZeroAddress2 } from "ethers";
10112
10230
  var WithdrawalAddressModule = class {
10113
10231
  config;
10114
10232
  graphClient;
@@ -10143,7 +10261,7 @@ var WithdrawalAddressModule = class {
10143
10261
  }
10144
10262
  /** keccak256 of the canonicalized address — matches the on-chain key. */
10145
10263
  static addressHash(btcAddress) {
10146
- return keccak2564(toUtf8Bytes4(normalizeBitcoinAddress(btcAddress)));
10264
+ return keccak2565(toUtf8Bytes5(normalizeBitcoinAddress(btcAddress)));
10147
10265
  }
10148
10266
  /**
10149
10267
  * Add a Bitcoin address to the caller's allowlist. It becomes usable only
@@ -17592,8 +17710,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17592
17710
  throw new Error(`Position not found: ${request.positionId}`);
17593
17711
  }
17594
17712
  const currentDebt = position.ucdDebt.toString();
17595
- const ucdDebtHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
17596
- const contractHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(
17713
+ const ucdDebtHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
17714
+ const contractHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(
17597
17715
  ["address", "address", "address", "address"],
17598
17716
  [
17599
17717
  positionManagerAddress,
@@ -17670,7 +17788,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17670
17788
  const currentPosition = await positionCore["getPositionDetails"](
17671
17789
  positionIdBytes32
17672
17790
  );
17673
- const currentDebtHash = keccak2565(
17791
+ const currentDebtHash = keccak2566(
17674
17792
  AbiCoder2.defaultAbiCoder().encode(
17675
17793
  ["uint256"],
17676
17794
  [currentPosition.ucdDebt]
@@ -21406,7 +21524,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21406
21524
  * been broadcast to the Bitcoin network. An empty array means no pending withdrawals.
21407
21525
  *
21408
21526
  * @param positionId - Position identifier
21409
- * @returns Array of pending withdrawals, each including a pre-computed utxoKey for use with cancelPendingWithdrawal
21527
+ * @returns Array of pending withdrawals, each including a pre-computed utxoKey (the key recoverStaleSpend and the admin clearing path operate on)
21410
21528
  */
21411
21529
  async getPendingWithdrawals(positionId) {
21412
21530
  this.ensureInitialized();
@@ -21438,7 +21556,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21438
21556
  targetAddress: spend.targetAddress,
21439
21557
  targetAmount: Number(spend.targetAmount),
21440
21558
  authorizedAt: Number(spend.authorizedAt),
21441
- utxoKey: solidityPackedKeccak2564(
21559
+ utxoKey: solidityPackedKeccak2565(
21442
21560
  ["string", "uint32"],
21443
21561
  [spend.txid, Number(spend.vout)]
21444
21562
  )
@@ -21451,11 +21569,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21451
21569
  * broadcast happened. Callers MUST use the returned `status` to decide what
21452
21570
  * to offer:
21453
21571
  * - EXECUTABLE → offer Execute (the only status that may).
21454
- * - EXECUTED → auto-clear; `spendingTxid` is the completion proof.
21455
- * - SPENT_MISMATCH → unexecutable; offer cancelPendingWithdrawal.
21572
+ * - EXECUTED → auto-clear; `spendingTxid` is the completion proof
21573
+ * feed it to recoverStaleSpend to clear the reservation.
21574
+ * - SPENT_MISMATCH → unexecutable; recoverStaleSpend with the spending txid
21575
+ * as invalidator clears the reservation.
21456
21576
  * - CORRUPT → authorization contradicts the chain (e.g. declared
21457
- * satoshis ≠ real output value); offer Cancel &
21458
- * re-request Execute can only die at the signer guard.
21577
+ * satoshis ≠ real output value); Execute can only die at
21578
+ * the signer guard, and no borrower-side cancel exists
21579
+ * (audit #62210) — clearing needs the AdminModule
21580
+ * cancelStaleSpendByAdmin operator override.
21459
21581
  * - UNFUNDED → funding tx unknown/unconfirmed; wait.
21460
21582
  *
21461
21583
  * @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
@@ -21479,18 +21601,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21479
21601
  spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
21480
21602
  );
21481
21603
  }
21482
- /**
21483
- * Cancel a pending BTC withdrawal
21484
- *
21485
- * Removes a stale authorized spend from BTCSpendAuthorizer, unlocking the UTXO
21486
- * and allowing the borrower to retry the withdrawal from scratch.
21487
- * Only the position's borrower may call this requires a connected signer.
21488
- *
21489
- * @param positionId - Position identifier
21490
- * @param txid - Bitcoin UTXO transaction ID (from getPendingWithdrawals)
21491
- * @param vout - Output index (from getPendingWithdrawals)
21492
- * @returns Transaction hash of the cancellation EVM transaction
21493
- */
21604
+ // NOTE: there is deliberately no cancelPendingWithdrawal. Borrower-callable
21605
+ // cleanup was permanently disabled by audit #62210 (markSpentByOwner /
21606
+ // cancelStaleSpendByOwner revert with BorrowerBTCSpendMutationDisabled — a
21607
+ // borrower cancel could be replayed to mint UCD against the same UTXO).
21608
+ // Reservations clear via recoverStaleSpend below (trustless attestation once
21609
+ // the UTXO is provably consumed) or the AdminModule-timelocked
21610
+ // cancelStaleSpendByAdmin operator override.
21494
21611
  /**
21495
21612
  * Recover (clear) a stale BTC reservation via LIT-attested proof.
21496
21613
  *
@@ -21642,6 +21759,256 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21642
21759
  };
21643
21760
  }
21644
21761
  }
21762
+ /**
21763
+ * Sign + broadcast the PKP vault→vault self-spend that invalidates one
21764
+ * stuck authorized outpoint (btc-utxo-invalidator 1.0.0). Once the returned
21765
+ * `invalidatorTxid` reaches >=6 Bitcoin confirmations, `recoverStaleSpend`
21766
+ * clears the on-chain reservation — the trustless replacement for the
21767
+ * AdminModule cancelStaleSpendByAdmin path on unspent stuck/corrupt
21768
+ * reservations (incident 2026-07-22). Requires the borrower (or position
21769
+ * delegate) signer; the signature is scoped to exactly this outpoint.
21770
+ */
21771
+ async invalidateStaleSpend(params) {
21772
+ this.ensureInitialized();
21773
+ const chainId = this.config.chainId ?? this.config.networkOverride?.chainId;
21774
+ if (!chainId) {
21775
+ return {
21776
+ success: false,
21777
+ failedStep: "config",
21778
+ error: "chainId required for invalidateStaleSpend"
21779
+ };
21780
+ }
21781
+ const signer = this.getSignerOrThrow();
21782
+ const envelope = await buildInvalidateStaleSpendEnvelope({
21783
+ positionId: params.positionId,
21784
+ utxoTxid: params.utxoTxid,
21785
+ utxoVout: params.utxoVout,
21786
+ chainId,
21787
+ signer
21788
+ });
21789
+ if (this.config.mode === "service" && this.config.serviceEndpoint) {
21790
+ const headers = {
21791
+ "Content-Type": "application/json",
21792
+ ...await this.getAuthHeader()
21793
+ };
21794
+ const resp = await fetch(
21795
+ `${this.config.serviceEndpoint}/api/lit/stale-spend/invalidate`,
21796
+ {
21797
+ method: "POST",
21798
+ headers,
21799
+ body: JSON.stringify({
21800
+ positionId: this.toBytes32(params.positionId),
21801
+ utxoTxid: params.utxoTxid,
21802
+ utxoVout: params.utxoVout,
21803
+ chainId,
21804
+ auth: {
21805
+ timestamp: envelope.timestamp,
21806
+ action: envelope.action,
21807
+ signature: envelope.signature
21808
+ }
21809
+ })
21810
+ }
21811
+ );
21812
+ const json = await resp.json();
21813
+ return json.data ?? {
21814
+ success: false,
21815
+ error: json.error ?? "stale-spend invalidate request failed"
21816
+ };
21817
+ }
21818
+ const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
21819
+ if (!rpcUrl) {
21820
+ return {
21821
+ success: false,
21822
+ failedStep: "config",
21823
+ error: "ethRpcUrl required for invalidateStaleSpend \u2014 set DiamondHandsSDKConfig.ethRpcUrl or pass rpcUrl"
21824
+ };
21825
+ }
21826
+ const positionDetails = await this.getPositionDetailsView(
21827
+ params.positionId
21828
+ );
21829
+ if (!positionDetails?.pkpId || positionDetails.pkpId === ZeroHash) {
21830
+ return { success: false, error: "Position has no PKP" };
21831
+ }
21832
+ const pkpCache = this.cacheManager.getCache("pkp-data", {
21833
+ maxSize: 500,
21834
+ ttlMs: 24 * 60 * 60 * 1e3
21835
+ });
21836
+ let publicKey = pkpCache.get(params.positionId)?.publicKey;
21837
+ if (!publicKey) {
21838
+ const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
21839
+ pkpId: positionDetails.pkpId
21840
+ });
21841
+ publicKey = derived.publicKey;
21842
+ }
21843
+ if (!publicKey) {
21844
+ return { success: false, error: "Failed to resolve PKP public key" };
21845
+ }
21846
+ const chipotlePkpAddress = computeAddress(publicKey);
21847
+ const dc = this.config.contractAddresses || {};
21848
+ const chainName = chainId === 1 ? "ethereum" : chainId === 11155111 ? "sepolia" : chainId === 1337 || chainId === 31337 ? "hardhat" : void 0;
21849
+ if (!chainName) {
21850
+ return {
21851
+ success: false,
21852
+ error: `Unsupported chainId ${chainId} for invalidateStaleSpend (supported: 1, 11155111, 1337, 31337)`
21853
+ };
21854
+ }
21855
+ const signResult = await this.litOps.signBtcUtxoInvalidator({
21856
+ auth: {
21857
+ positionId: this.toBytes32(params.positionId),
21858
+ utxoTxid: params.utxoTxid,
21859
+ utxoVout: params.utxoVout,
21860
+ timestamp: envelope.timestamp,
21861
+ chainId,
21862
+ action: envelope.action,
21863
+ signature: envelope.signature
21864
+ },
21865
+ publicKey,
21866
+ pkpId: chipotlePkpAddress,
21867
+ chain: chainName,
21868
+ rpcUrl,
21869
+ ...this.config.bitcoinProviders?.[0]?.url && (chainId === 1337 || chainId === 31337) ? { devBitcoinProviderUrl: this.config.bitcoinProviders[0].url } : {},
21870
+ contractAddresses: {
21871
+ PositionManager: dc.positionManager ?? "",
21872
+ BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
21873
+ ...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {},
21874
+ ...dc.positionDelegateRegistry ? { PositionDelegateRegistry: dc.positionDelegateRegistry } : {}
21875
+ }
21876
+ });
21877
+ if (!signResult.success) {
21878
+ return {
21879
+ success: false,
21880
+ error: signResult.error ?? "btc-utxo-invalidator signing failed",
21881
+ failedStep: signResult.failedStep
21882
+ };
21883
+ }
21884
+ if (signResult.alreadySpent && typeof signResult.alreadySpent === "object") {
21885
+ return {
21886
+ success: true,
21887
+ alreadySpent: { spendingTxid: signResult.alreadySpent.spendingTxid }
21888
+ };
21889
+ }
21890
+ if (!signResult.signatures?.length || !signResult.utxo || !signResult.pkpBtcAddress || typeof signResult.invalidatorAmount !== "number") {
21891
+ return {
21892
+ success: false,
21893
+ error: "btc-utxo-invalidator returned incomplete data (signatures/utxo/pkpBtcAddress/invalidatorAmount)"
21894
+ };
21895
+ }
21896
+ const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
21897
+ if (!broadcastUrl) {
21898
+ return {
21899
+ success: false,
21900
+ error: "invalidateStaleSpend broadcast requires config.bitcoinProviders[0].url (esplora base)"
21901
+ };
21902
+ }
21903
+ const { txid: invalidatorTxid } = await broadcastSignedBitcoinTransaction({
21904
+ signatures: signResult.signatures,
21905
+ utxos: [signResult.utxo],
21906
+ pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
21907
+ destination: signResult.pkpBtcAddress,
21908
+ userReceivesAmount: signResult.invalidatorAmount,
21909
+ changeAmount: 0,
21910
+ pkpBtcAddress: signResult.pkpBtcAddress,
21911
+ bitcoinNetwork: chainId === 1 ? "mainnet" : "regtest",
21912
+ bitcoinRpcUrl: broadcastUrl
21913
+ });
21914
+ return {
21915
+ success: true,
21916
+ invalidatorTxid,
21917
+ invalidatorAmount: signResult.invalidatorAmount,
21918
+ networkFee: signResult.networkFee,
21919
+ valueMismatch: signResult.valueMismatch
21920
+ };
21921
+ }
21922
+ /**
21923
+ * Resumable, stateless clearing state machine for one stuck reservation —
21924
+ * Bitcoin itself is the state store, so every call either advances the flow
21925
+ * one step or reports where it stands (survives reloads and the multi-day
21926
+ * 6-confirmation wait; no internal polling).
21927
+ *
21928
+ * (a) reservation gone → phase "cleared"
21929
+ * (b) outpoint spent, >=6 confs → recoverStaleSpend → "cleared"/"failed"
21930
+ * outpoint spent, <6 confs → "awaiting-confirmations" (x/6)
21931
+ * (c) outpoint unspent → invalidateStaleSpend → "invalidator-broadcast"
21932
+ */
21933
+ async clearStuckReservation(params) {
21934
+ this.ensureInitialized();
21935
+ const REQUIRED_CONFS = 6;
21936
+ const esploraBaseUrl = params.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
21937
+ const reconciled = await this.reconcilePendingWithdrawals(
21938
+ params.positionId,
21939
+ esploraBaseUrl ? { esploraBaseUrl } : void 0
21940
+ );
21941
+ const row = reconciled.find(
21942
+ (r) => r.spend.txid.toLowerCase() === params.utxoTxid.toLowerCase() && r.spend.vout === params.utxoVout
21943
+ );
21944
+ if (!row) {
21945
+ return { phase: "cleared" };
21946
+ }
21947
+ if (row.spendingTxid && esploraBaseUrl) {
21948
+ const base = esploraBaseUrl.replace(/\/+$/, "");
21949
+ const txRes = await fetch(`${base}/tx/${row.spendingTxid}`);
21950
+ const tipRes = await fetch(`${base}/blocks/tip/height`);
21951
+ let confirmations = 0;
21952
+ if (txRes.ok && tipRes.ok) {
21953
+ const tx = await txRes.json();
21954
+ const tip = Number(await tipRes.text());
21955
+ if (tx.status?.confirmed && typeof tx.status.block_height === "number" && Number.isFinite(tip)) {
21956
+ confirmations = Math.max(0, tip - tx.status.block_height + 1);
21957
+ }
21958
+ }
21959
+ if (confirmations < REQUIRED_CONFS) {
21960
+ return {
21961
+ phase: "awaiting-confirmations",
21962
+ invalidatorTxid: row.spendingTxid,
21963
+ confirmations,
21964
+ required: REQUIRED_CONFS
21965
+ };
21966
+ }
21967
+ const recovery = await this.recoverStaleSpend({
21968
+ positionId: params.positionId,
21969
+ utxoTxid: params.utxoTxid,
21970
+ utxoVout: params.utxoVout,
21971
+ invalidatorTxid: row.spendingTxid
21972
+ });
21973
+ if (recovery.success) {
21974
+ return {
21975
+ phase: "cleared",
21976
+ invalidatorTxid: row.spendingTxid,
21977
+ transactionHash: recovery.transactionHash,
21978
+ classification: recovery.classification
21979
+ };
21980
+ }
21981
+ return {
21982
+ phase: "failed",
21983
+ invalidatorTxid: row.spendingTxid,
21984
+ error: recovery.error ?? "stale-spend recovery failed"
21985
+ };
21986
+ }
21987
+ const invalidation = await this.invalidateStaleSpend({
21988
+ positionId: params.positionId,
21989
+ utxoTxid: params.utxoTxid,
21990
+ utxoVout: params.utxoVout
21991
+ });
21992
+ if (!invalidation.success) {
21993
+ return {
21994
+ phase: "failed",
21995
+ error: invalidation.error ?? "invalidator signing/broadcast failed"
21996
+ };
21997
+ }
21998
+ if (invalidation.alreadySpent) {
21999
+ return {
22000
+ phase: "awaiting-confirmations",
22001
+ invalidatorTxid: invalidation.alreadySpent.spendingTxid,
22002
+ confirmations: 0,
22003
+ required: REQUIRED_CONFS
22004
+ };
22005
+ }
22006
+ return {
22007
+ phase: "invalidator-broadcast",
22008
+ invalidatorTxid: invalidation.invalidatorTxid,
22009
+ valueMismatch: invalidation.valueMismatch
22010
+ };
22011
+ }
21645
22012
  /**
21646
22013
  * Withdraw BTC and execute transfer (Complete Flow)
21647
22014
  *
@@ -22732,7 +23099,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22732
23099
  * Convert decimal position ID to bytes32 format
22733
23100
  */
22734
23101
  toBytes32(value) {
22735
- return zeroPadValue4(toBeHex2(BigInt(value)), 32);
23102
+ return zeroPadValue5(toBeHex2(BigInt(value)), 32);
22736
23103
  }
22737
23104
  /**
22738
23105
  * Check if an error indicates a technical failure vs business logic rejection
@@ -22950,8 +23317,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22950
23317
  const MIN_REVEAL_DELAY = 60;
22951
23318
  const MAX_RANDOM_DELAY = 240;
22952
23319
  const entropyInput = positionId + quantumTimestamp.toString();
22953
- const hash = keccak2565(
22954
- toUtf8Bytes5(entropyInput)
23320
+ const hash = keccak2566(
23321
+ toUtf8Bytes6(entropyInput)
22955
23322
  );
22956
23323
  const randomValue = BigInt(hash);
22957
23324
  const randomDelay = Number(randomValue % BigInt(MAX_RANDOM_DELAY));
@@ -22976,12 +23343,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22976
23343
  const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22977
23344
  const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22978
23345
  const intentTimestamp = Math.floor(Date.now() / 1e3);
22979
- const intentActionHash = keccak2565(toUtf8Bytes5("liquidate-position"));
22980
- const intentHash = solidityPackedKeccak2564(
23346
+ const intentActionHash = keccak2566(toUtf8Bytes6("liquidate-position"));
23347
+ const intentHash = solidityPackedKeccak2565(
22981
23348
  ["bytes32", "uint256", "uint256", "address", "bytes32"],
22982
23349
  [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22983
23350
  );
22984
- const intentSignature = await signer.signMessage(getBytes4(intentHash));
23351
+ const intentSignature = await signer.signMessage(getBytes5(intentHash));
22985
23352
  const response = await fetch(endpoint, {
22986
23353
  method: "POST",
22987
23354
  headers: { "Content-Type": "application/json" },
@@ -23033,7 +23400,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
23033
23400
  const vrfSeedRaw = await lmResult.value["vrfSeeds"](positionId);
23034
23401
  const vrfSeed = BigInt(vrfSeedRaw.toString());
23035
23402
  if (vrfSeed !== 0n) {
23036
- const finalEntropy = solidityPackedKeccak2564(
23403
+ const finalEntropy = solidityPackedKeccak2565(
23037
23404
  ["uint256", "uint256", "bytes32"],
23038
23405
  [vrfSeed, BigInt(quantumTimestamp.toString()), positionId]
23039
23406
  );
@@ -23354,6 +23721,7 @@ export {
23354
23721
  PKPManager,
23355
23722
  PaymentType,
23356
23723
  PositionStatus,
23724
+ RECOVERY_INVALIDATE_ACTION,
23357
23725
  SDKError,
23358
23726
  SDK_DEFAULTS,
23359
23727
  SEPOLIA_CONTRACTS,
@@ -23366,6 +23734,8 @@ export {
23366
23734
  assertProtocolNotPaused,
23367
23735
  assertSafeServiceEndpoint,
23368
23736
  buildBtcExecuteEnvelope,
23737
+ buildInvalidateMessageHash,
23738
+ buildInvalidateStaleSpendEnvelope,
23369
23739
  collectFailures,
23370
23740
  collectSuccesses,
23371
23741
  combine,