@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.
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) {
@@ -6952,10 +6965,203 @@ async function isLaneGuardActiveOnProvider(provider) {
6952
6965
  }
6953
6966
  }
6954
6967
 
6955
- // src/utils/btc-withdrawal-message.ts
6956
- import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
6968
+ // src/utils/withdrawal-reconciliation.utils.ts
6969
+ var defaultHttpGetJson = async (url) => {
6970
+ const res = await fetch(url, { headers: { accept: "application/json" } });
6971
+ let body = null;
6972
+ try {
6973
+ body = await res.json();
6974
+ } catch {
6975
+ body = null;
6976
+ }
6977
+ return { status: res.status, body };
6978
+ };
6979
+ async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = defaultHttpGetJson) {
6980
+ const base = esploraBaseUrl.replace(/\/+$/, "");
6981
+ const fundingRes = await httpGetJson(`${base}/tx/${spend.txid}`);
6982
+ if (fundingRes.status === 404) {
6983
+ return {
6984
+ spend,
6985
+ status: "UNFUNDED",
6986
+ reason: `Funding tx ${spend.txid} not found on the network`
6987
+ };
6988
+ }
6989
+ if (fundingRes.status !== 200) {
6990
+ throw new Error(
6991
+ `Withdrawal reconciliation: esplora /tx/${spend.txid} returned HTTP ${fundingRes.status}`
6992
+ );
6993
+ }
6994
+ const funding = fundingRes.body;
6995
+ if (!funding?.status?.confirmed) {
6996
+ return {
6997
+ spend,
6998
+ status: "UNFUNDED",
6999
+ reason: `Funding tx ${spend.txid} is not confirmed yet`
7000
+ };
7001
+ }
7002
+ const output = funding.vout?.[spend.vout];
7003
+ if (!output || typeof output.value !== "number") {
7004
+ return {
7005
+ spend,
7006
+ status: "CORRUPT",
7007
+ reason: `Authorized vout ${spend.vout} does not exist on tx ${spend.txid} (${funding.vout?.length ?? 0} outputs)`
7008
+ };
7009
+ }
7010
+ const onChainOutputValue = output.value;
7011
+ if (onChainOutputValue !== spend.satoshis) {
7012
+ return {
7013
+ spend,
7014
+ status: "CORRUPT",
7015
+ onChainOutputValue,
7016
+ reason: `Authorization declares ${spend.satoshis} sats for ${spend.txid}:${spend.vout} but the on-chain output is ${onChainOutputValue} sats`
7017
+ };
7018
+ }
7019
+ if (spend.targetAmount > onChainOutputValue) {
7020
+ return {
7021
+ spend,
7022
+ status: "CORRUPT",
7023
+ onChainOutputValue,
7024
+ reason: `Authorized targetAmount ${spend.targetAmount} sats exceeds the ${onChainOutputValue}-sat output it is authorized against`
7025
+ };
7026
+ }
7027
+ const outspendRes = await httpGetJson(
7028
+ `${base}/tx/${spend.txid}/outspend/${spend.vout}`
7029
+ );
7030
+ if (outspendRes.status !== 200) {
7031
+ throw new Error(
7032
+ `Withdrawal reconciliation: esplora outspend for ${spend.txid}:${spend.vout} returned HTTP ${outspendRes.status}`
7033
+ );
7034
+ }
7035
+ const outspend = outspendRes.body;
7036
+ if (outspend?.spent) {
7037
+ const spendingTxid = typeof outspend.txid === "string" && outspend.txid ? outspend.txid : null;
7038
+ if (spendingTxid) {
7039
+ const spendingRes = await httpGetJson(`${base}/tx/${spendingTxid}`);
7040
+ if (spendingRes.status === 200) {
7041
+ const spendingTx = spendingRes.body;
7042
+ const includesOutpoint = (spendingTx.vin ?? []).some(
7043
+ (v) => v.txid === spend.txid && Number(v.vout) === spend.vout
7044
+ );
7045
+ if (includesOutpoint) {
7046
+ const paidToTargetSats = (spendingTx.vout ?? []).filter(
7047
+ (o) => (o.scriptpubkey_address ?? "").toLowerCase() === spend.targetAddress.toLowerCase()
7048
+ ).reduce((sum, o) => sum + (o.value ?? 0), 0);
7049
+ if (paidToTargetSats > 0) {
7050
+ return {
7051
+ spend,
7052
+ status: "EXECUTED",
7053
+ spendingTxid,
7054
+ paidToTargetSats,
7055
+ onChainOutputValue,
7056
+ reason: `Outpoint spent by ${spendingTxid}, paying ${paidToTargetSats} sats to the authorized target \u2014 withdrawal complete`
7057
+ };
7058
+ }
7059
+ return {
7060
+ spend,
7061
+ status: "SPENT_MISMATCH",
7062
+ spendingTxid,
7063
+ onChainOutputValue,
7064
+ reason: `Outpoint spent by ${spendingTxid} which pays the authorized target nothing \u2014 authorization is unexecutable`
7065
+ };
7066
+ }
7067
+ }
7068
+ }
7069
+ return {
7070
+ spend,
7071
+ status: "EXECUTABLE",
7072
+ onChainOutputValue,
7073
+ reason: "Esplora claims the outpoint is spent but no spending tx provably includes it (known regtest-faucet artifact) \u2014 treated as unspent"
7074
+ };
7075
+ }
7076
+ return {
7077
+ spend,
7078
+ status: "EXECUTABLE",
7079
+ onChainOutputValue,
7080
+ reason: "Outpoint confirmed, unspent, and coherent with the authorization"
7081
+ };
7082
+ }
7083
+
7084
+ // src/utils/stale-spend-invalidate-message.ts
7085
+ import {
7086
+ keccak256 as keccak2563,
7087
+ toUtf8Bytes as toUtf8Bytes3,
7088
+ solidityPackedKeccak256 as solidityPackedKeccak2563,
7089
+ getBytes as getBytes3,
7090
+ zeroPadValue
7091
+ } from "ethers";
7092
+ var RECOVERY_INVALIDATE_ACTION = "btc-utxo-invalidator";
6957
7093
  var QUANTUM_SECONDS = 60;
6958
7094
  var QUANTUM_SAFE_THRESHOLD = 40;
7095
+ function buildInvalidateMessageHash(params) {
7096
+ const positionIdBytes32 = zeroPadValue(
7097
+ params.positionId.startsWith("0x") ? params.positionId : `0x${params.positionId}`,
7098
+ 32
7099
+ );
7100
+ const actionHash = keccak2563(toUtf8Bytes3(RECOVERY_INVALIDATE_ACTION));
7101
+ return solidityPackedKeccak2563(
7102
+ [
7103
+ "bytes32",
7104
+ // positionId
7105
+ "uint256",
7106
+ // timestamp
7107
+ "uint256",
7108
+ // chainId
7109
+ "string",
7110
+ // utxoTxid (display order)
7111
+ "uint32",
7112
+ // utxoVout
7113
+ "bytes32"
7114
+ // actionHash
7115
+ ],
7116
+ [
7117
+ positionIdBytes32,
7118
+ params.timestamp,
7119
+ params.chainId,
7120
+ params.utxoTxid,
7121
+ params.utxoVout,
7122
+ actionHash
7123
+ ]
7124
+ );
7125
+ }
7126
+ async function buildInvalidateStaleSpendEnvelope(params) {
7127
+ const { positionId, utxoTxid, utxoVout, chainId, signer } = params;
7128
+ const nowSec = Date.now() / 1e3;
7129
+ const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
7130
+ const secsIntoQuantum = nowSec - currentQuantumStart;
7131
+ let timestamp;
7132
+ if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
7133
+ const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
7134
+ const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
7135
+ await new Promise((r) => setTimeout(r, waitMs));
7136
+ timestamp = nextQuantumStart;
7137
+ } else {
7138
+ timestamp = currentQuantumStart;
7139
+ }
7140
+ const messageHash = buildInvalidateMessageHash({
7141
+ positionId,
7142
+ timestamp,
7143
+ chainId,
7144
+ utxoTxid,
7145
+ utxoVout
7146
+ });
7147
+ const signature = await signer.signMessage(getBytes3(messageHash));
7148
+ const borrowerAddress = await signer.getAddress();
7149
+ return {
7150
+ positionId,
7151
+ utxoTxid,
7152
+ utxoVout,
7153
+ timestamp,
7154
+ chainId,
7155
+ action: RECOVERY_INVALIDATE_ACTION,
7156
+ signature,
7157
+ borrowerAddress
7158
+ };
7159
+ }
7160
+
7161
+ // src/utils/btc-withdrawal-message.ts
7162
+ import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, solidityPackedKeccak256 as solidityPackedKeccak2564, getBytes as getBytes4, zeroPadValue as zeroPadValue2 } from "ethers";
7163
+ var QUANTUM_SECONDS2 = 60;
7164
+ var QUANTUM_SAFE_THRESHOLD2 = 40;
6959
7165
  function hashBtcInputSet(utxos) {
6960
7166
  if (!Array.isArray(utxos) || utxos.length === 0) {
6961
7167
  throw new Error("hashBtcInputSet: at least one input is required");
@@ -6984,7 +7190,7 @@ function hashBtcInputSet(utxos) {
6984
7190
  seen.add(key);
6985
7191
  }
6986
7192
  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("|");
6987
- return keccak2563(toUtf8Bytes3(canonical));
7193
+ return keccak2564(toUtf8Bytes4(canonical));
6988
7194
  }
6989
7195
  async function buildBtcExecuteEnvelope(params) {
6990
7196
  const {
@@ -7018,26 +7224,26 @@ async function buildBtcExecuteEnvelope(params) {
7018
7224
  }
7019
7225
  const utxosHash = hashBtcInputSet(utxos);
7020
7226
  const nowSec = Date.now() / 1e3;
7021
- const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
7227
+ const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS2) * QUANTUM_SECONDS2;
7022
7228
  const secsIntoQuantum = nowSec - currentQuantumStart;
7023
7229
  let timestamp;
7024
- if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
7025
- const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
7230
+ if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD2) {
7231
+ const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS2;
7026
7232
  const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
7027
7233
  await new Promise((r) => setTimeout(r, waitMs));
7028
7234
  timestamp = nextQuantumStart;
7029
7235
  } else {
7030
7236
  timestamp = currentQuantumStart;
7031
7237
  }
7032
- const actionHash = keccak2563(
7033
- toUtf8Bytes3("execute-btc-withdrawal")
7238
+ const actionHash = keccak2564(
7239
+ toUtf8Bytes4("execute-btc-withdrawal")
7034
7240
  );
7035
7241
  const utxoIdentifier = `${txid}:${vout}`;
7036
- const positionIdBytes32 = zeroPadValue(
7242
+ const positionIdBytes32 = zeroPadValue2(
7037
7243
  positionId.startsWith("0x") ? positionId : `0x${positionId}`,
7038
7244
  32
7039
7245
  );
7040
- const messageHash = solidityPackedKeccak2563(
7246
+ const messageHash = solidityPackedKeccak2564(
7041
7247
  [
7042
7248
  "bytes32",
7043
7249
  // positionId
@@ -7070,7 +7276,7 @@ async function buildBtcExecuteEnvelope(params) {
7070
7276
  actionHash
7071
7277
  ]
7072
7278
  );
7073
- const userSignature = await signer.signMessage(getBytes3(messageHash));
7279
+ const userSignature = await signer.signMessage(getBytes4(messageHash));
7074
7280
  const borrowerAddress = await signer.getAddress();
7075
7281
  return {
7076
7282
  positionId,
@@ -7134,7 +7340,7 @@ function maybeTemperSignature(signature, shouldTemper, operationName) {
7134
7340
  }
7135
7341
 
7136
7342
  // src/utils/address-conversion.utils.ts
7137
- import { zeroPadValue as zeroPadValue2 } from "ethers";
7343
+ import { zeroPadValue as zeroPadValue3 } from "ethers";
7138
7344
 
7139
7345
  // src/utils/chunks/bitcoin-utils.ts
7140
7346
  import { sha256 as nobleSha256 } from "@noble/hashes/sha256";
@@ -8718,7 +8924,7 @@ function createPKPManager(config) {
8718
8924
  }
8719
8925
 
8720
8926
  // src/modules/loan/loan-creator.module.ts
8721
- import { Interface as Interface2, zeroPadValue as zeroPadValue3, toBeHex, SigningKey } from "ethers";
8927
+ import { Interface as Interface2, zeroPadValue as zeroPadValue4, toBeHex, SigningKey } from "ethers";
8722
8928
  var import_dh_lit_actions = __toESM(require_pkg_src());
8723
8929
  var LoanCreator = class {
8724
8930
  config;
@@ -9048,7 +9254,7 @@ var LoanCreator = class {
9048
9254
  try {
9049
9255
  const mainnetVaultAddress = bitcoinAddresses.mainnet;
9050
9256
  const regtestVaultAddress = bitcoinAddresses.regtest;
9051
- const pkpIdBytes32 = zeroPadValue3(
9257
+ const pkpIdBytes32 = zeroPadValue4(
9052
9258
  toBeHex(BigInt(pkpData.tokenId)),
9053
9259
  32
9054
9260
  );
@@ -9229,7 +9435,7 @@ var LoanCreator = class {
9229
9435
  }
9230
9436
  }
9231
9437
  if (this.config.debug) {
9232
- const pkpIdBytes32Check = zeroPadValue3(
9438
+ const pkpIdBytes32Check = zeroPadValue4(
9233
9439
  toBeHex(BigInt(pkpData.tokenId)),
9234
9440
  32
9235
9441
  );
@@ -9992,7 +10198,7 @@ function createLoanQuery(config) {
9992
10198
  }
9993
10199
 
9994
10200
  // src/modules/withdrawal-address/withdrawal-address.module.ts
9995
- import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, ZeroAddress as ZeroAddress2 } from "ethers";
10201
+ import { keccak256 as keccak2565, toUtf8Bytes as toUtf8Bytes5, ZeroAddress as ZeroAddress2 } from "ethers";
9996
10202
  var WithdrawalAddressModule = class {
9997
10203
  config;
9998
10204
  graphClient;
@@ -10027,7 +10233,7 @@ var WithdrawalAddressModule = class {
10027
10233
  }
10028
10234
  /** keccak256 of the canonicalized address — matches the on-chain key. */
10029
10235
  static addressHash(btcAddress) {
10030
- return keccak2564(toUtf8Bytes4(normalizeBitcoinAddress(btcAddress)));
10236
+ return keccak2565(toUtf8Bytes5(normalizeBitcoinAddress(btcAddress)));
10031
10237
  }
10032
10238
  /**
10033
10239
  * Add a Bitcoin address to the caller's allowlist. It becomes usable only
@@ -17476,8 +17682,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17476
17682
  throw new Error(`Position not found: ${request.positionId}`);
17477
17683
  }
17478
17684
  const currentDebt = position.ucdDebt.toString();
17479
- const ucdDebtHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
17480
- const contractHash = keccak2565(AbiCoder2.defaultAbiCoder().encode(
17685
+ const ucdDebtHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
17686
+ const contractHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(
17481
17687
  ["address", "address", "address", "address"],
17482
17688
  [
17483
17689
  positionManagerAddress,
@@ -17554,7 +17760,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17554
17760
  const currentPosition = await positionCore["getPositionDetails"](
17555
17761
  positionIdBytes32
17556
17762
  );
17557
- const currentDebtHash = keccak2565(
17763
+ const currentDebtHash = keccak2566(
17558
17764
  AbiCoder2.defaultAbiCoder().encode(
17559
17765
  ["uint256"],
17560
17766
  [currentPosition.ucdDebt]
@@ -21290,7 +21496,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21290
21496
  * been broadcast to the Bitcoin network. An empty array means no pending withdrawals.
21291
21497
  *
21292
21498
  * @param positionId - Position identifier
21293
- * @returns Array of pending withdrawals, each including a pre-computed utxoKey for use with cancelPendingWithdrawal
21499
+ * @returns Array of pending withdrawals, each including a pre-computed utxoKey (the key recoverStaleSpend and the admin clearing path operate on)
21294
21500
  */
21295
21501
  async getPendingWithdrawals(positionId) {
21296
21502
  this.ensureInitialized();
@@ -21322,24 +21528,58 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21322
21528
  targetAddress: spend.targetAddress,
21323
21529
  targetAmount: Number(spend.targetAmount),
21324
21530
  authorizedAt: Number(spend.authorizedAt),
21325
- utxoKey: solidityPackedKeccak2564(
21531
+ utxoKey: solidityPackedKeccak2565(
21326
21532
  ["string", "uint32"],
21327
21533
  [spend.txid, Number(spend.vout)]
21328
21534
  )
21329
21535
  }));
21330
21536
  }
21331
21537
  /**
21332
- * Cancel a pending BTC withdrawal
21333
- *
21334
- * Removes a stale authorized spend from BTCSpendAuthorizer, unlocking the UTXO
21335
- * and allowing the borrower to retry the withdrawal from scratch.
21336
- * Only the position's borrower may call this — requires a connected signer.
21538
+ * Reconcile every pending withdrawal against BITCOIN truth (incident
21539
+ * 2026-07-22): `getPendingWithdrawals` only reflects on-chain
21540
+ * authorizations, and the contract can never know whether the Phase-2 BTC
21541
+ * broadcast happened. Callers MUST use the returned `status` to decide what
21542
+ * to offer:
21543
+ * - EXECUTABLE → offer Execute (the only status that may).
21544
+ * - EXECUTED → auto-clear; `spendingTxid` is the completion proof —
21545
+ * feed it to recoverStaleSpend to clear the reservation.
21546
+ * - SPENT_MISMATCH → unexecutable; recoverStaleSpend with the spending txid
21547
+ * as invalidator clears the reservation.
21548
+ * - CORRUPT → authorization contradicts the chain (e.g. declared
21549
+ * satoshis ≠ real output value); Execute can only die at
21550
+ * the signer guard, and no borrower-side cancel exists
21551
+ * (audit #62210) — clearing needs the AdminModule
21552
+ * cancelStaleSpendByAdmin operator override.
21553
+ * - UNFUNDED → funding tx unknown/unconfirmed; wait.
21337
21554
  *
21338
- * @param positionId - Position identifier
21339
- * @param txid - Bitcoin UTXO transaction ID (from getPendingWithdrawals)
21340
- * @param vout - Output index (from getPendingWithdrawals)
21341
- * @returns Transaction hash of the cancellation EVM transaction
21555
+ * @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
21556
+ * `/v1/proxy/esplora/<network>`). Falls back to
21557
+ * `config.bitcoinProviders[0].url`; throws when neither is configured.
21342
21558
  */
21559
+ async reconcilePendingWithdrawals(positionId, opts) {
21560
+ this.ensureInitialized();
21561
+ const esploraBaseUrl = opts?.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
21562
+ if (!esploraBaseUrl) {
21563
+ throw new SDKError({
21564
+ code: "SDK_ESPLORA_NOT_CONFIGURED",
21565
+ message: "reconcilePendingWithdrawals requires opts.esploraBaseUrl or config.bitcoinProviders[0].url",
21566
+ category: "CONFIGURATION" /* CONFIGURATION */,
21567
+ severity: "HIGH" /* HIGH */,
21568
+ originalError: new Error("esplora base URL not configured")
21569
+ });
21570
+ }
21571
+ const spends = await this.getPendingWithdrawals(positionId);
21572
+ return Promise.all(
21573
+ spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
21574
+ );
21575
+ }
21576
+ // NOTE: there is deliberately no cancelPendingWithdrawal. Borrower-callable
21577
+ // cleanup was permanently disabled by audit #62210 (markSpentByOwner /
21578
+ // cancelStaleSpendByOwner revert with BorrowerBTCSpendMutationDisabled — a
21579
+ // borrower cancel could be replayed to mint UCD against the same UTXO).
21580
+ // Reservations clear via recoverStaleSpend below (trustless attestation once
21581
+ // the UTXO is provably consumed) or the AdminModule-timelocked
21582
+ // cancelStaleSpendByAdmin operator override.
21343
21583
  /**
21344
21584
  * Recover (clear) a stale BTC reservation via LIT-attested proof.
21345
21585
  *
@@ -21491,6 +21731,256 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21491
21731
  };
21492
21732
  }
21493
21733
  }
21734
+ /**
21735
+ * Sign + broadcast the PKP vault→vault self-spend that invalidates one
21736
+ * stuck authorized outpoint (btc-utxo-invalidator 1.0.0). Once the returned
21737
+ * `invalidatorTxid` reaches >=6 Bitcoin confirmations, `recoverStaleSpend`
21738
+ * clears the on-chain reservation — the trustless replacement for the
21739
+ * AdminModule cancelStaleSpendByAdmin path on unspent stuck/corrupt
21740
+ * reservations (incident 2026-07-22). Requires the borrower (or position
21741
+ * delegate) signer; the signature is scoped to exactly this outpoint.
21742
+ */
21743
+ async invalidateStaleSpend(params) {
21744
+ this.ensureInitialized();
21745
+ const chainId = this.config.chainId ?? this.config.networkOverride?.chainId;
21746
+ if (!chainId) {
21747
+ return {
21748
+ success: false,
21749
+ failedStep: "config",
21750
+ error: "chainId required for invalidateStaleSpend"
21751
+ };
21752
+ }
21753
+ const signer = this.getSignerOrThrow();
21754
+ const envelope = await buildInvalidateStaleSpendEnvelope({
21755
+ positionId: params.positionId,
21756
+ utxoTxid: params.utxoTxid,
21757
+ utxoVout: params.utxoVout,
21758
+ chainId,
21759
+ signer
21760
+ });
21761
+ if (this.config.mode === "service" && this.config.serviceEndpoint) {
21762
+ const headers = {
21763
+ "Content-Type": "application/json",
21764
+ ...await this.getAuthHeader()
21765
+ };
21766
+ const resp = await fetch(
21767
+ `${this.config.serviceEndpoint}/api/lit/stale-spend/invalidate`,
21768
+ {
21769
+ method: "POST",
21770
+ headers,
21771
+ body: JSON.stringify({
21772
+ positionId: this.toBytes32(params.positionId),
21773
+ utxoTxid: params.utxoTxid,
21774
+ utxoVout: params.utxoVout,
21775
+ chainId,
21776
+ auth: {
21777
+ timestamp: envelope.timestamp,
21778
+ action: envelope.action,
21779
+ signature: envelope.signature
21780
+ }
21781
+ })
21782
+ }
21783
+ );
21784
+ const json = await resp.json();
21785
+ return json.data ?? {
21786
+ success: false,
21787
+ error: json.error ?? "stale-spend invalidate request failed"
21788
+ };
21789
+ }
21790
+ const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
21791
+ if (!rpcUrl) {
21792
+ return {
21793
+ success: false,
21794
+ failedStep: "config",
21795
+ error: "ethRpcUrl required for invalidateStaleSpend \u2014 set DiamondHandsSDKConfig.ethRpcUrl or pass rpcUrl"
21796
+ };
21797
+ }
21798
+ const positionDetails = await this.getPositionDetailsView(
21799
+ params.positionId
21800
+ );
21801
+ if (!positionDetails?.pkpId || positionDetails.pkpId === ZeroHash) {
21802
+ return { success: false, error: "Position has no PKP" };
21803
+ }
21804
+ const pkpCache = this.cacheManager.getCache("pkp-data", {
21805
+ maxSize: 500,
21806
+ ttlMs: 24 * 60 * 60 * 1e3
21807
+ });
21808
+ let publicKey = pkpCache.get(params.positionId)?.publicKey;
21809
+ if (!publicKey) {
21810
+ const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
21811
+ pkpId: positionDetails.pkpId
21812
+ });
21813
+ publicKey = derived.publicKey;
21814
+ }
21815
+ if (!publicKey) {
21816
+ return { success: false, error: "Failed to resolve PKP public key" };
21817
+ }
21818
+ const chipotlePkpAddress = computeAddress(publicKey);
21819
+ const dc = this.config.contractAddresses || {};
21820
+ const chainName = chainId === 1 ? "ethereum" : chainId === 11155111 ? "sepolia" : chainId === 1337 || chainId === 31337 ? "hardhat" : void 0;
21821
+ if (!chainName) {
21822
+ return {
21823
+ success: false,
21824
+ error: `Unsupported chainId ${chainId} for invalidateStaleSpend (supported: 1, 11155111, 1337, 31337)`
21825
+ };
21826
+ }
21827
+ const signResult = await this.litOps.signBtcUtxoInvalidator({
21828
+ auth: {
21829
+ positionId: this.toBytes32(params.positionId),
21830
+ utxoTxid: params.utxoTxid,
21831
+ utxoVout: params.utxoVout,
21832
+ timestamp: envelope.timestamp,
21833
+ chainId,
21834
+ action: envelope.action,
21835
+ signature: envelope.signature
21836
+ },
21837
+ publicKey,
21838
+ pkpId: chipotlePkpAddress,
21839
+ chain: chainName,
21840
+ rpcUrl,
21841
+ ...this.config.bitcoinProviders?.[0]?.url && (chainId === 1337 || chainId === 31337) ? { devBitcoinProviderUrl: this.config.bitcoinProviders[0].url } : {},
21842
+ contractAddresses: {
21843
+ PositionManager: dc.positionManager ?? "",
21844
+ BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
21845
+ ...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {},
21846
+ ...dc.positionDelegateRegistry ? { PositionDelegateRegistry: dc.positionDelegateRegistry } : {}
21847
+ }
21848
+ });
21849
+ if (!signResult.success) {
21850
+ return {
21851
+ success: false,
21852
+ error: signResult.error ?? "btc-utxo-invalidator signing failed",
21853
+ failedStep: signResult.failedStep
21854
+ };
21855
+ }
21856
+ if (signResult.alreadySpent && typeof signResult.alreadySpent === "object") {
21857
+ return {
21858
+ success: true,
21859
+ alreadySpent: { spendingTxid: signResult.alreadySpent.spendingTxid }
21860
+ };
21861
+ }
21862
+ if (!signResult.signatures?.length || !signResult.utxo || !signResult.pkpBtcAddress || typeof signResult.invalidatorAmount !== "number") {
21863
+ return {
21864
+ success: false,
21865
+ error: "btc-utxo-invalidator returned incomplete data (signatures/utxo/pkpBtcAddress/invalidatorAmount)"
21866
+ };
21867
+ }
21868
+ const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
21869
+ if (!broadcastUrl) {
21870
+ return {
21871
+ success: false,
21872
+ error: "invalidateStaleSpend broadcast requires config.bitcoinProviders[0].url (esplora base)"
21873
+ };
21874
+ }
21875
+ const { txid: invalidatorTxid } = await broadcastSignedBitcoinTransaction({
21876
+ signatures: signResult.signatures,
21877
+ utxos: [signResult.utxo],
21878
+ pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
21879
+ destination: signResult.pkpBtcAddress,
21880
+ userReceivesAmount: signResult.invalidatorAmount,
21881
+ changeAmount: 0,
21882
+ pkpBtcAddress: signResult.pkpBtcAddress,
21883
+ bitcoinNetwork: chainId === 1 ? "mainnet" : "regtest",
21884
+ bitcoinRpcUrl: broadcastUrl
21885
+ });
21886
+ return {
21887
+ success: true,
21888
+ invalidatorTxid,
21889
+ invalidatorAmount: signResult.invalidatorAmount,
21890
+ networkFee: signResult.networkFee,
21891
+ valueMismatch: signResult.valueMismatch
21892
+ };
21893
+ }
21894
+ /**
21895
+ * Resumable, stateless clearing state machine for one stuck reservation —
21896
+ * Bitcoin itself is the state store, so every call either advances the flow
21897
+ * one step or reports where it stands (survives reloads and the multi-day
21898
+ * 6-confirmation wait; no internal polling).
21899
+ *
21900
+ * (a) reservation gone → phase "cleared"
21901
+ * (b) outpoint spent, >=6 confs → recoverStaleSpend → "cleared"/"failed"
21902
+ * outpoint spent, <6 confs → "awaiting-confirmations" (x/6)
21903
+ * (c) outpoint unspent → invalidateStaleSpend → "invalidator-broadcast"
21904
+ */
21905
+ async clearStuckReservation(params) {
21906
+ this.ensureInitialized();
21907
+ const REQUIRED_CONFS = 6;
21908
+ const esploraBaseUrl = params.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
21909
+ const reconciled = await this.reconcilePendingWithdrawals(
21910
+ params.positionId,
21911
+ esploraBaseUrl ? { esploraBaseUrl } : void 0
21912
+ );
21913
+ const row = reconciled.find(
21914
+ (r) => r.spend.txid.toLowerCase() === params.utxoTxid.toLowerCase() && r.spend.vout === params.utxoVout
21915
+ );
21916
+ if (!row) {
21917
+ return { phase: "cleared" };
21918
+ }
21919
+ if (row.spendingTxid && esploraBaseUrl) {
21920
+ const base = esploraBaseUrl.replace(/\/+$/, "");
21921
+ const txRes = await fetch(`${base}/tx/${row.spendingTxid}`);
21922
+ const tipRes = await fetch(`${base}/blocks/tip/height`);
21923
+ let confirmations = 0;
21924
+ if (txRes.ok && tipRes.ok) {
21925
+ const tx = await txRes.json();
21926
+ const tip = Number(await tipRes.text());
21927
+ if (tx.status?.confirmed && typeof tx.status.block_height === "number" && Number.isFinite(tip)) {
21928
+ confirmations = Math.max(0, tip - tx.status.block_height + 1);
21929
+ }
21930
+ }
21931
+ if (confirmations < REQUIRED_CONFS) {
21932
+ return {
21933
+ phase: "awaiting-confirmations",
21934
+ invalidatorTxid: row.spendingTxid,
21935
+ confirmations,
21936
+ required: REQUIRED_CONFS
21937
+ };
21938
+ }
21939
+ const recovery = await this.recoverStaleSpend({
21940
+ positionId: params.positionId,
21941
+ utxoTxid: params.utxoTxid,
21942
+ utxoVout: params.utxoVout,
21943
+ invalidatorTxid: row.spendingTxid
21944
+ });
21945
+ if (recovery.success) {
21946
+ return {
21947
+ phase: "cleared",
21948
+ invalidatorTxid: row.spendingTxid,
21949
+ transactionHash: recovery.transactionHash,
21950
+ classification: recovery.classification
21951
+ };
21952
+ }
21953
+ return {
21954
+ phase: "failed",
21955
+ invalidatorTxid: row.spendingTxid,
21956
+ error: recovery.error ?? "stale-spend recovery failed"
21957
+ };
21958
+ }
21959
+ const invalidation = await this.invalidateStaleSpend({
21960
+ positionId: params.positionId,
21961
+ utxoTxid: params.utxoTxid,
21962
+ utxoVout: params.utxoVout
21963
+ });
21964
+ if (!invalidation.success) {
21965
+ return {
21966
+ phase: "failed",
21967
+ error: invalidation.error ?? "invalidator signing/broadcast failed"
21968
+ };
21969
+ }
21970
+ if (invalidation.alreadySpent) {
21971
+ return {
21972
+ phase: "awaiting-confirmations",
21973
+ invalidatorTxid: invalidation.alreadySpent.spendingTxid,
21974
+ confirmations: 0,
21975
+ required: REQUIRED_CONFS
21976
+ };
21977
+ }
21978
+ return {
21979
+ phase: "invalidator-broadcast",
21980
+ invalidatorTxid: invalidation.invalidatorTxid,
21981
+ valueMismatch: invalidation.valueMismatch
21982
+ };
21983
+ }
21494
21984
  /**
21495
21985
  * Withdraw BTC and execute transfer (Complete Flow)
21496
21986
  *
@@ -22581,7 +23071,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22581
23071
  * Convert decimal position ID to bytes32 format
22582
23072
  */
22583
23073
  toBytes32(value) {
22584
- return zeroPadValue4(toBeHex2(BigInt(value)), 32);
23074
+ return zeroPadValue5(toBeHex2(BigInt(value)), 32);
22585
23075
  }
22586
23076
  /**
22587
23077
  * Check if an error indicates a technical failure vs business logic rejection
@@ -22799,8 +23289,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22799
23289
  const MIN_REVEAL_DELAY = 60;
22800
23290
  const MAX_RANDOM_DELAY = 240;
22801
23291
  const entropyInput = positionId + quantumTimestamp.toString();
22802
- const hash = keccak2565(
22803
- toUtf8Bytes5(entropyInput)
23292
+ const hash = keccak2566(
23293
+ toUtf8Bytes6(entropyInput)
22804
23294
  );
22805
23295
  const randomValue = BigInt(hash);
22806
23296
  const randomDelay = Number(randomValue % BigInt(MAX_RANDOM_DELAY));
@@ -22825,12 +23315,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22825
23315
  const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
22826
23316
  const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
22827
23317
  const intentTimestamp = Math.floor(Date.now() / 1e3);
22828
- const intentActionHash = keccak2565(toUtf8Bytes5("liquidate-position"));
22829
- const intentHash = solidityPackedKeccak2564(
23318
+ const intentActionHash = keccak2566(toUtf8Bytes6("liquidate-position"));
23319
+ const intentHash = solidityPackedKeccak2565(
22830
23320
  ["bytes32", "uint256", "uint256", "address", "bytes32"],
22831
23321
  [canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
22832
23322
  );
22833
- const intentSignature = await signer.signMessage(getBytes4(intentHash));
23323
+ const intentSignature = await signer.signMessage(getBytes5(intentHash));
22834
23324
  const response = await fetch(endpoint, {
22835
23325
  method: "POST",
22836
23326
  headers: { "Content-Type": "application/json" },
@@ -22882,7 +23372,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22882
23372
  const vrfSeedRaw = await lmResult.value["vrfSeeds"](positionId);
22883
23373
  const vrfSeed = BigInt(vrfSeedRaw.toString());
22884
23374
  if (vrfSeed !== 0n) {
22885
- const finalEntropy = solidityPackedKeccak2564(
23375
+ const finalEntropy = solidityPackedKeccak2565(
22886
23376
  ["uint256", "uint256", "bytes32"],
22887
23377
  [vrfSeed, BigInt(quantumTimestamp.toString()), positionId]
22888
23378
  );
@@ -23203,6 +23693,7 @@ export {
23203
23693
  PKPManager,
23204
23694
  PaymentType,
23205
23695
  PositionStatus,
23696
+ RECOVERY_INVALIDATE_ACTION,
23206
23697
  SDKError,
23207
23698
  SDK_DEFAULTS,
23208
23699
  SEPOLIA_CONTRACTS,
@@ -23215,6 +23706,8 @@ export {
23215
23706
  assertProtocolNotPaused,
23216
23707
  assertSafeServiceEndpoint,
23217
23708
  buildBtcExecuteEnvelope,
23709
+ buildInvalidateMessageHash,
23710
+ buildInvalidateStaleSpendEnvelope,
23218
23711
  collectFailures,
23219
23712
  collectSuccesses,
23220
23713
  combine,
@@ -23226,6 +23719,7 @@ export {
23226
23719
  createPKPManager,
23227
23720
  createWithdrawalAddressModule,
23228
23721
  DiamondHandsSDK as default,
23722
+ defaultHttpGetJson,
23229
23723
  failure,
23230
23724
  fetchProtocolPauseStatus,
23231
23725
  firstSuccess,
@@ -23249,6 +23743,7 @@ export {
23249
23743
  mapError,
23250
23744
  match,
23251
23745
  numericToPositionStatus,
23746
+ reconcileAuthorizedSpend,
23252
23747
  setPositionDelegate,
23253
23748
  success,
23254
23749
  toPromise,