@gvnrdao/dh-sdk 0.0.305 → 0.0.307

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
@@ -577,45 +577,47 @@ var init_deployment_addresses = __esm({
577
577
  };
578
578
  LOCALHOST_DEPLOYMENT = {
579
579
  network: "localhost",
580
- chainId: 31337,
581
- timestamp: "2026-06-11T12:30:59.589Z",
580
+ chainId: 1337,
581
+ timestamp: "2026-07-20T19:45:58.514Z",
582
582
  deployer: "",
583
583
  contracts: {
584
- MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
585
- UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
584
+ MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
585
+ LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
586
+ UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
586
587
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
587
588
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
588
- PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
589
- TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
590
- LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
591
- BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
592
- CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
593
- LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
594
- CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
595
- AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
596
- PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
597
- PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
598
- OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
599
- PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
589
+ PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
590
+ TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
591
+ LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
592
+ BTCSpendAuthorizer: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d",
593
+ CollateralManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
594
+ LiquidationManagerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
595
+ CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
596
+ AdminModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
597
+ PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
598
+ PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
599
+ OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
600
+ PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
600
601
  }
601
602
  };
602
603
  LOCALHOST_CONTRACTS = {
603
- MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
604
- UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
604
+ MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
605
+ LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
606
+ UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
605
607
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
606
608
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
607
- PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
608
- TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
609
- LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
610
- BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
611
- CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
612
- LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
613
- CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
614
- AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
615
- PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
616
- PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
617
- OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
618
- PKPValidation: "0xc0F115A19107322cFBf1cDBC7ea011C19EbDB4F8"
609
+ PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
610
+ TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
611
+ LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
612
+ BTCSpendAuthorizer: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d",
613
+ CollateralManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
614
+ LiquidationManagerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
615
+ CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
616
+ AdminModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
617
+ PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
618
+ PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
619
+ OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
620
+ PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
619
621
  };
620
622
  ALL_DEPLOYMENTS = {
621
623
  sepolia: SEPOLIA_DEPLOYMENT,
@@ -5534,7 +5536,12 @@ var init_network_configs = __esm({
5534
5536
  NETWORK_CONFIGS = {
5535
5537
  1: getMainnetConfig(),
5536
5538
  11155111: getSepoliaConfig(),
5537
- 1337: getLocalhostConfig()
5539
+ 1337: getLocalhostConfig(),
5540
+ // In-process hardhat default chainId — same localhost deployment as 1337
5541
+ // (`npx hardhat node`), so consumers on either local chain resolve config.
5542
+ // chainId overridden so the resolved config self-describes the chain that
5543
+ // was actually looked up (downstream bind/log/validate reads it).
5544
+ 31337: { ...getLocalhostConfig(), chainId: 31337 }
5538
5545
  };
5539
5546
  DEFAULT_NETWORK_CONFIG = {
5540
5547
  /** Fallback when chain is unknown — production Chipotle. */
@@ -6874,12 +6881,20 @@ async function assertSafeQuantumSubmission(opts) {
6874
6881
  ...opts.sleep ? { sleep: opts.sleep } : {}
6875
6882
  };
6876
6883
  await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6884
+ const SAME_BLOCK_INTERACTION_SELECTOR = "0x3b53d1db";
6885
+ const laneGuardActive = opts.laneGuardActive === true;
6877
6886
  for (let sim = 1; ; sim++) {
6878
6887
  try {
6879
6888
  await provider.call({ to, from, data });
6880
6889
  break;
6881
6890
  } catch (simError) {
6882
6891
  const decoded = decodeQuantumRevert(simError);
6892
+ if (laneGuardActive && decoded.selector === SAME_BLOCK_INTERACTION_SELECTOR) {
6893
+ onDebug(
6894
+ "\u21AA\uFE0F [lane-only] Simulated SameBlockInteraction is a stale-latest-block artifact (the simulated block contains this flow's own previous tx) \u2014 proceeding to broadcast; the on-chain guard stays authoritative"
6895
+ );
6896
+ break;
6897
+ }
6883
6898
  if (decoded.selector === DEAD_ZONE_VIOLATION_SELECTOR && sim <= maxResimulations) {
6884
6899
  const sigQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6885
6900
  const realCurrentQuantum = Math.floor(now() / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
@@ -6915,6 +6930,144 @@ async function assertSafeQuantumSubmission(opts) {
6915
6930
  await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6916
6931
  }
6917
6932
 
6933
+ // src/utils/lane-guard.utils.ts
6934
+ var HARDHAT_CHAIN_IDS = /* @__PURE__ */ new Set([1337, 31337]);
6935
+ function laneGuardEnvEnabled() {
6936
+ return typeof process !== "undefined" && process.env?.LIT_LANE_GUARD === "1";
6937
+ }
6938
+ async function isLaneGuardActiveOnProvider(provider) {
6939
+ if (!laneGuardEnvEnabled())
6940
+ return false;
6941
+ if (!provider || typeof provider.send !== "function")
6942
+ return false;
6943
+ try {
6944
+ const [chainIdHex, clientVersion] = await Promise.all([
6945
+ provider.send("eth_chainId", []),
6946
+ provider.send("web3_clientVersion", [])
6947
+ ]);
6948
+ const chainId = Number(chainIdHex);
6949
+ return HARDHAT_CHAIN_IDS.has(chainId) && typeof clientVersion === "string" && clientVersion.toLowerCase().includes("hardhat");
6950
+ } catch {
6951
+ return false;
6952
+ }
6953
+ }
6954
+
6955
+ // src/utils/withdrawal-reconciliation.utils.ts
6956
+ var defaultHttpGetJson = async (url) => {
6957
+ const res = await fetch(url, { headers: { accept: "application/json" } });
6958
+ let body = null;
6959
+ try {
6960
+ body = await res.json();
6961
+ } catch {
6962
+ body = null;
6963
+ }
6964
+ return { status: res.status, body };
6965
+ };
6966
+ async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = defaultHttpGetJson) {
6967
+ const base = esploraBaseUrl.replace(/\/+$/, "");
6968
+ const fundingRes = await httpGetJson(`${base}/tx/${spend.txid}`);
6969
+ if (fundingRes.status === 404) {
6970
+ return {
6971
+ spend,
6972
+ status: "UNFUNDED",
6973
+ reason: `Funding tx ${spend.txid} not found on the network`
6974
+ };
6975
+ }
6976
+ if (fundingRes.status !== 200) {
6977
+ throw new Error(
6978
+ `Withdrawal reconciliation: esplora /tx/${spend.txid} returned HTTP ${fundingRes.status}`
6979
+ );
6980
+ }
6981
+ const funding = fundingRes.body;
6982
+ if (!funding?.status?.confirmed) {
6983
+ return {
6984
+ spend,
6985
+ status: "UNFUNDED",
6986
+ reason: `Funding tx ${spend.txid} is not confirmed yet`
6987
+ };
6988
+ }
6989
+ const output = funding.vout?.[spend.vout];
6990
+ if (!output || typeof output.value !== "number") {
6991
+ return {
6992
+ spend,
6993
+ status: "CORRUPT",
6994
+ reason: `Authorized vout ${spend.vout} does not exist on tx ${spend.txid} (${funding.vout?.length ?? 0} outputs)`
6995
+ };
6996
+ }
6997
+ const onChainOutputValue = output.value;
6998
+ if (onChainOutputValue !== spend.satoshis) {
6999
+ return {
7000
+ spend,
7001
+ status: "CORRUPT",
7002
+ onChainOutputValue,
7003
+ reason: `Authorization declares ${spend.satoshis} sats for ${spend.txid}:${spend.vout} but the on-chain output is ${onChainOutputValue} sats`
7004
+ };
7005
+ }
7006
+ if (spend.targetAmount > onChainOutputValue) {
7007
+ return {
7008
+ spend,
7009
+ status: "CORRUPT",
7010
+ onChainOutputValue,
7011
+ reason: `Authorized targetAmount ${spend.targetAmount} sats exceeds the ${onChainOutputValue}-sat output it is authorized against`
7012
+ };
7013
+ }
7014
+ const outspendRes = await httpGetJson(
7015
+ `${base}/tx/${spend.txid}/outspend/${spend.vout}`
7016
+ );
7017
+ if (outspendRes.status !== 200) {
7018
+ throw new Error(
7019
+ `Withdrawal reconciliation: esplora outspend for ${spend.txid}:${spend.vout} returned HTTP ${outspendRes.status}`
7020
+ );
7021
+ }
7022
+ const outspend = outspendRes.body;
7023
+ if (outspend?.spent) {
7024
+ const spendingTxid = typeof outspend.txid === "string" && outspend.txid ? outspend.txid : null;
7025
+ if (spendingTxid) {
7026
+ const spendingRes = await httpGetJson(`${base}/tx/${spendingTxid}`);
7027
+ if (spendingRes.status === 200) {
7028
+ const spendingTx = spendingRes.body;
7029
+ const includesOutpoint = (spendingTx.vin ?? []).some(
7030
+ (v) => v.txid === spend.txid && Number(v.vout) === spend.vout
7031
+ );
7032
+ if (includesOutpoint) {
7033
+ const paidToTargetSats = (spendingTx.vout ?? []).filter(
7034
+ (o) => (o.scriptpubkey_address ?? "").toLowerCase() === spend.targetAddress.toLowerCase()
7035
+ ).reduce((sum, o) => sum + (o.value ?? 0), 0);
7036
+ if (paidToTargetSats > 0) {
7037
+ return {
7038
+ spend,
7039
+ status: "EXECUTED",
7040
+ spendingTxid,
7041
+ paidToTargetSats,
7042
+ onChainOutputValue,
7043
+ reason: `Outpoint spent by ${spendingTxid}, paying ${paidToTargetSats} sats to the authorized target \u2014 withdrawal complete`
7044
+ };
7045
+ }
7046
+ return {
7047
+ spend,
7048
+ status: "SPENT_MISMATCH",
7049
+ spendingTxid,
7050
+ onChainOutputValue,
7051
+ reason: `Outpoint spent by ${spendingTxid} which pays the authorized target nothing \u2014 authorization is unexecutable`
7052
+ };
7053
+ }
7054
+ }
7055
+ }
7056
+ return {
7057
+ spend,
7058
+ status: "EXECUTABLE",
7059
+ onChainOutputValue,
7060
+ reason: "Esplora claims the outpoint is spent but no spending tx provably includes it (known regtest-faucet artifact) \u2014 treated as unspent"
7061
+ };
7062
+ }
7063
+ return {
7064
+ spend,
7065
+ status: "EXECUTABLE",
7066
+ onChainOutputValue,
7067
+ reason: "Outpoint confirmed, unspent, and coherent with the authorization"
7068
+ };
7069
+ }
7070
+
6918
7071
  // src/utils/btc-withdrawal-message.ts
6919
7072
  import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
6920
7073
  var QUANTUM_SECONDS = 60;
@@ -15776,7 +15929,7 @@ function createMockTokenManager(config) {
15776
15929
 
15777
15930
  // src/modules/diamond-hands-sdk.ts
15778
15931
  var import_dh_lit_actions2 = __toESM(require_pkg_src());
15779
- import { LitOps } from "@gvnrdao/dh-lit-ops";
15932
+ import { LitOps, broadcastSignedBitcoinTransaction } from "@gvnrdao/dh-lit-ops";
15780
15933
 
15781
15934
  // src/utils/telegram-messaging.utils.ts
15782
15935
  var TelegramBot = null;
@@ -16261,6 +16414,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16261
16414
  this.litOps = new LitOps({
16262
16415
  mode: config.mode || "standalone",
16263
16416
  network: config.litNetwork || DEFAULT_LIT_NETWORK,
16417
+ // Completes the module's chainId resolution chain (jsParams shapes →
16418
+ // config.chainId) — e.g. the lane validator-override chain binding
16419
+ // reads it when a jsParams shape lacks chainId.
16420
+ chainId: config.chainId,
16264
16421
  signer: config.mode === "standalone" ? config.litOpsSigner : void 0,
16265
16422
  serviceEndpoint: isServiceModeConfig(config) ? config.serviceEndpoint : void 0,
16266
16423
  // Hand lit-ops a session-aware auth getter so every request it makes
@@ -16938,6 +17095,19 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16938
17095
  }
16939
17096
  const MAX_FULL_RETRIES = 3;
16940
17097
  for (let fullRetry = 1; fullRetry <= MAX_FULL_RETRIES; fullRetry++) {
17098
+ if (await this.isLocalLaneGuardActive()) {
17099
+ const phase = Math.floor(Date.now() / 1e3) % 60;
17100
+ if (phase > 30) {
17101
+ const waitMs = (60 - phase + 1) * 1e3;
17102
+ if (this.config.debug) {
17103
+ log.info(
17104
+ `\u23F3 Quantum-phase alignment (lane-only): ${phase}s into the quantum \u2014 waiting ${Math.ceil(waitMs / 1e3)}s for the next boundary before authorization`,
17105
+ {}
17106
+ );
17107
+ }
17108
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
17109
+ }
17110
+ }
16941
17111
  if (fullRetry > 1 && this.config.debug) {
16942
17112
  log.info(
16943
17113
  `\u{1F504} Full mint retry ${fullRetry}/${MAX_FULL_RETRIES} due to timestamp staleness...`,
@@ -17686,6 +17856,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17686
17856
  from: fromAddress,
17687
17857
  data: mintCalldata,
17688
17858
  quantumTimestamp: Number(validationResponse.timestamp),
17859
+ laneGuardActive: await this.isLocalLaneGuardActive(),
17689
17860
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
17690
17861
  });
17691
17862
  } catch (simError) {
@@ -18981,6 +19152,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
18981
19152
  from: await extendSigner.getAddress(),
18982
19153
  data: extendCalldata,
18983
19154
  quantumTimestamp: Number(quantumTimestamp),
19155
+ laneGuardActive: await this.isLocalLaneGuardActive(),
18984
19156
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
18985
19157
  });
18986
19158
  } catch (simError) {
@@ -19881,6 +20053,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19881
20053
  from: paymentFrom,
19882
20054
  data: paymentCalldata,
19883
20055
  quantumTimestamp: Number(quantumTimestamp),
20056
+ laneGuardActive: await this.isLocalLaneGuardActive(),
19884
20057
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
19885
20058
  });
19886
20059
  } catch (simError) {
@@ -20620,6 +20793,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20620
20793
  from: await withdrawSigner.getAddress(),
20621
20794
  data: withdrawCalldata,
20622
20795
  quantumTimestamp: Number(withdrawalParams.quantumTimestamp),
20796
+ laneGuardActive: await this.isLocalLaneGuardActive(),
20623
20797
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
20624
20798
  });
20625
20799
  tx = await sendEip1559Transaction({
@@ -20774,15 +20948,180 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20774
20948
  * @param request - Execution request with positionId, utxoIdentifier, and networkFee
20775
20949
  * @returns Execution result with Bitcoin txid
20776
20950
  */
20777
- async executeBTCWithdrawal(request) {
20778
- this.ensureInitialized();
20779
- if (this.config.mode !== "service") {
20951
+ /**
20952
+ * STANDALONE Phase 2: sign + broadcast the withdrawal Bitcoin transaction
20953
+ * in-process — the same orchestration lit-ops-server performs, composed
20954
+ * from the same primitives (`litOps.signBTCTransaction`, which has always
20955
+ * had a standalone path, and `broadcastSignedBitcoinTransaction`).
20956
+ * Both modes must support withdrawal execution (Master mandate 2026-07-21).
20957
+ *
20958
+ * The service path's post-broadcast CRIT-2 re-verification exists to catch
20959
+ * a LYING SERVER; here the broadcast happens locally from the signatures we
20960
+ * just obtained, so the esplora-returned txid is already first-hand.
20961
+ */
20962
+ async executeBTCWithdrawalStandalonePhase2(request, envelope, txid, vout, chainId) {
20963
+ const positionDetails = await this.getPositionDetailsView(
20964
+ request.positionId
20965
+ );
20966
+ if (!positionDetails) {
20967
+ return { success: false, error: `Position ${request.positionId} not found` };
20968
+ }
20969
+ const pkpId = positionDetails.pkpId;
20970
+ const vaultAddress = positionDetails.vaultAddress;
20971
+ if (!pkpId || pkpId === ZeroHash) {
20972
+ return { success: false, error: "Position has no PKP" };
20973
+ }
20974
+ if (!vaultAddress) {
20975
+ return { success: false, error: "Position has no vault address" };
20976
+ }
20977
+ const pkpCache = this.cacheManager.getCache("pkp-data", {
20978
+ maxSize: 500,
20979
+ ttlMs: 24 * 60 * 60 * 1e3
20980
+ });
20981
+ let publicKey = pkpCache.get(request.positionId)?.publicKey;
20982
+ if (!publicKey) {
20983
+ const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
20984
+ pkpId
20985
+ });
20986
+ publicKey = derived.publicKey;
20987
+ }
20988
+ if (!publicKey) {
20989
+ return { success: false, error: "Failed to resolve PKP public key" };
20990
+ }
20991
+ const chipotlePkpAddress = computeAddress(publicKey);
20992
+ const STANDALONE_CHAIN_NAMES = {
20993
+ 1: "ethereum",
20994
+ 11155111: "sepolia",
20995
+ 1337: "hardhat",
20996
+ 31337: "hardhat"
20997
+ };
20998
+ const chainName = STANDALONE_CHAIN_NAMES[chainId];
20999
+ if (!chainName) {
20780
21000
  return {
20781
21001
  success: false,
20782
- error: "BTC withdrawal Phase 2 requires service mode. Set sdkConfig.mode = 'service' and serviceEndpoint to a lit-ops-server."
21002
+ error: `Unsupported chainId ${chainId} for standalone BTC withdrawal (supported: 1, 11155111, 1337, 31337)`
20783
21003
  };
20784
21004
  }
20785
- if (!this.config.serviceEndpoint) {
21005
+ const bitcoinNetworkName = chainId === 1 ? "mainnet" : "regtest";
21006
+ const bitcoin = await import("bitcoinjs-lib");
21007
+ const bitcoinNetwork = chainId === 1 ? bitcoin.networks.bitcoin : chainId === 1337 || chainId === 31337 ? bitcoin.networks.regtest : bitcoin.networks.testnet;
21008
+ let scriptPubKeyBuffer;
21009
+ if (vaultAddress.startsWith("bcrt1q")) {
21010
+ const bech322 = await import("bech32");
21011
+ const decoded = bech322.bech32.decode(vaultAddress);
21012
+ const witnessProgram = Buffer.from(
21013
+ bech322.bech32.fromWords(decoded.words.slice(1))
21014
+ );
21015
+ if (witnessProgram.length !== 20) {
21016
+ return {
21017
+ success: false,
21018
+ error: `Vault address ${vaultAddress} decodes to a ${witnessProgram.length}-byte witness program \u2014 expected 20-byte P2WPKH`
21019
+ };
21020
+ }
21021
+ scriptPubKeyBuffer = Buffer.concat([
21022
+ Buffer.from([0, witnessProgram.length]),
21023
+ witnessProgram
21024
+ ]);
21025
+ } else {
21026
+ scriptPubKeyBuffer = bitcoin.address.toOutputScript(
21027
+ vaultAddress,
21028
+ bitcoinNetwork
21029
+ );
21030
+ }
21031
+ const scriptPubKey = scriptPubKeyBuffer.toString("hex");
21032
+ const executionUtxos = (envelope.utxos && envelope.utxos.length ? envelope.utxos : [{ txid, vout, value: request.utxoSatoshis }]).map((u) => ({ ...u, scriptPubKey }));
21033
+ const dc = this.config.contractAddresses || {};
21034
+ const signerContractAddresses = {
21035
+ LoanOperationsManagerModule: dc.loanOperationsManager ?? "",
21036
+ PositionManager: dc.positionManager ?? "",
21037
+ BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
21038
+ PositionDelegateRegistry: dc.positionDelegateRegistry ?? "",
21039
+ OperationAuthorizationRegistry: dc.operationAuthorizationRegistry ?? "",
21040
+ ...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {}
21041
+ };
21042
+ const signResult = await this.litOps.signBTCTransaction({
21043
+ auth: {
21044
+ positionId: envelope.positionId,
21045
+ utxo: { txid, vout, value: request.utxoSatoshis, scriptPubKey },
21046
+ utxos: executionUtxos,
21047
+ networkFee: envelope.networkFee,
21048
+ targetAddress: envelope.targetAddress,
21049
+ targetAmount: envelope.targetAmount,
21050
+ timestamp: envelope.timestamp,
21051
+ signature: envelope.userSignature,
21052
+ chainId
21053
+ },
21054
+ publicKey,
21055
+ pkpId: chipotlePkpAddress,
21056
+ chain: chainName,
21057
+ bitcoinNetwork: bitcoinNetworkName,
21058
+ rpcUrl: this.config.ethRpcUrl,
21059
+ contractAddresses: signerContractAddresses
21060
+ });
21061
+ if (!signResult.success) {
21062
+ return {
21063
+ success: false,
21064
+ error: signResult.error ?? "BTC transaction signing failed"
21065
+ };
21066
+ }
21067
+ if (!signResult.unsignedTxHex || !signResult.signatures?.length) {
21068
+ return {
21069
+ success: false,
21070
+ error: "BTC signing returned incomplete data (missing unsignedTxHex/signatures)"
21071
+ };
21072
+ }
21073
+ if (!signResult.userReceivesAmount) {
21074
+ return { success: false, error: "BTC signing did not return userReceivesAmount" };
21075
+ }
21076
+ const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
21077
+ if (!broadcastUrl) {
21078
+ return {
21079
+ success: false,
21080
+ error: "Standalone Phase 2 broadcast requires config.bitcoinProviders[0].url (esplora base)"
21081
+ };
21082
+ }
21083
+ try {
21084
+ const { txid: btcTxid } = await broadcastSignedBitcoinTransaction({
21085
+ signatures: signResult.signatures,
21086
+ utxos: executionUtxos,
21087
+ pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
21088
+ destination: envelope.targetAddress,
21089
+ userReceivesAmount: signResult.userReceivesAmount,
21090
+ changeAmount: signResult.changeAmount,
21091
+ pkpBtcAddress: signResult.pkpBtcAddress,
21092
+ bitcoinNetwork: bitcoinNetworkName,
21093
+ bitcoinRpcUrl: broadcastUrl,
21094
+ ...this.config.debug ? {
21095
+ debug: (level, msg, data) => {
21096
+ if (level === "warn")
21097
+ log.warn(msg, { data });
21098
+ else if (level === "error")
21099
+ log.error(msg, { data });
21100
+ else
21101
+ log.info(msg, { data });
21102
+ }
21103
+ } : {}
21104
+ });
21105
+ if (this.config.debug) {
21106
+ log.info("\u2705 Standalone Phase 2 broadcast successful", { txid: btcTxid });
21107
+ }
21108
+ return {
21109
+ success: true,
21110
+ txid: btcTxid,
21111
+ amountSent: signResult.userReceivesAmount,
21112
+ networkFee: signResult.networkFee,
21113
+ destination: envelope.targetAddress
21114
+ };
21115
+ } catch (broadcastError) {
21116
+ return {
21117
+ success: false,
21118
+ error: `BTC broadcast failed: ${broadcastError instanceof Error ? broadcastError.message : String(broadcastError)}`
21119
+ };
21120
+ }
21121
+ }
21122
+ async executeBTCWithdrawal(request) {
21123
+ this.ensureInitialized();
21124
+ if (this.config.mode === "service" && !this.config.serviceEndpoint) {
20786
21125
  return { success: false, error: "serviceEndpoint required for BTC withdrawal" };
20787
21126
  }
20788
21127
  if (!request.positionId) {
@@ -20837,6 +21176,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20837
21176
  signer,
20838
21177
  ...request.utxos ? { utxos: request.utxos } : {}
20839
21178
  });
21179
+ if (this.config.mode !== "service") {
21180
+ return await this.executeBTCWithdrawalStandalonePhase2(
21181
+ request,
21182
+ envelope,
21183
+ txid,
21184
+ vout,
21185
+ chainId
21186
+ );
21187
+ }
20840
21188
  const headers = {
20841
21189
  "Content-Type": "application/json",
20842
21190
  ...await this.getAuthHeader()
@@ -21096,6 +21444,41 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21096
21444
  )
21097
21445
  }));
21098
21446
  }
21447
+ /**
21448
+ * Reconcile every pending withdrawal against BITCOIN truth (incident
21449
+ * 2026-07-22): `getPendingWithdrawals` only reflects on-chain
21450
+ * authorizations, and the contract can never know whether the Phase-2 BTC
21451
+ * broadcast happened. Callers MUST use the returned `status` to decide what
21452
+ * to offer:
21453
+ * - EXECUTABLE → offer Execute (the only status that may).
21454
+ * - EXECUTED → auto-clear; `spendingTxid` is the completion proof.
21455
+ * - SPENT_MISMATCH → unexecutable; offer cancelPendingWithdrawal.
21456
+ * - 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.
21459
+ * - UNFUNDED → funding tx unknown/unconfirmed; wait.
21460
+ *
21461
+ * @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
21462
+ * `/v1/proxy/esplora/<network>`). Falls back to
21463
+ * `config.bitcoinProviders[0].url`; throws when neither is configured.
21464
+ */
21465
+ async reconcilePendingWithdrawals(positionId, opts) {
21466
+ this.ensureInitialized();
21467
+ const esploraBaseUrl = opts?.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
21468
+ if (!esploraBaseUrl) {
21469
+ throw new SDKError({
21470
+ code: "SDK_ESPLORA_NOT_CONFIGURED",
21471
+ message: "reconcilePendingWithdrawals requires opts.esploraBaseUrl or config.bitcoinProviders[0].url",
21472
+ category: "CONFIGURATION" /* CONFIGURATION */,
21473
+ severity: "HIGH" /* HIGH */,
21474
+ originalError: new Error("esplora base URL not configured")
21475
+ });
21476
+ }
21477
+ const spends = await this.getPendingWithdrawals(positionId);
21478
+ return Promise.all(
21479
+ spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
21480
+ );
21481
+ }
21099
21482
  /**
21100
21483
  * Cancel a pending BTC withdrawal
21101
21484
  *
@@ -22154,6 +22537,25 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22154
22537
  const chain = this.config.chain?.toLowerCase() || "";
22155
22538
  return chain.includes("mainnet");
22156
22539
  }
22540
+ /**
22541
+ * Local-lane activation, verified against the LIVE node at call time (not
22542
+ * the chainId cached at init — a provider that switched networks after
22543
+ * init fails the fresh probe). Zero RPC cost outside a lane process tree:
22544
+ * without LIT_LANE_GUARD=1 this short-circuits to false before any probe.
22545
+ */
22546
+ async isLocalLaneGuardActive() {
22547
+ if (!laneGuardEnvEnabled())
22548
+ return false;
22549
+ let provider;
22550
+ try {
22551
+ provider = this.config.provider ?? this.getProviderOrThrow();
22552
+ } catch {
22553
+ return false;
22554
+ }
22555
+ return isLaneGuardActiveOnProvider(
22556
+ provider
22557
+ );
22558
+ }
22157
22559
  /**
22158
22560
  * Get Bitcoin network based on chain.
22159
22561
  *
@@ -22975,6 +23377,7 @@ export {
22975
23377
  createPKPManager,
22976
23378
  createWithdrawalAddressModule,
22977
23379
  DiamondHandsSDK as default,
23380
+ defaultHttpGetJson,
22978
23381
  failure,
22979
23382
  fetchProtocolPauseStatus,
22980
23383
  firstSuccess,
@@ -22998,6 +23401,7 @@ export {
22998
23401
  mapError,
22999
23402
  match,
23000
23403
  numericToPositionStatus,
23404
+ reconcileAuthorizedSpend,
23001
23405
  setPositionDelegate,
23002
23406
  success,
23003
23407
  toPromise,
@@ -89,6 +89,18 @@ interface BaseSDKConfig {
89
89
  litActionCid?: string;
90
90
  litNodeConnectTimeoutMs?: number;
91
91
  subgraphUrl?: string;
92
+ /**
93
+ * Standalone/local-lane Bitcoin providers (esplora bases). LIT Actions pull
94
+ * every registered provider from the on-chain BitcoinProviderRegistry and
95
+ * the server's executePendingWithdrawal handles service-mode broadcast —
96
+ * this field never feeds those paths. It drives standalone-mode flows only:
97
+ * the standalone Phase 2 withdrawal broadcast, hardhat-lane per-call
98
+ * overrides, and `network` as a bitcoinNetwork inference fallback.
99
+ */
100
+ bitcoinProviders?: Array<{
101
+ url: string;
102
+ network?: "mainnet" | "testnet" | "regtest";
103
+ }>;
92
104
  priceProviders?: PriceProviderConfig[];
93
105
  /** PKP Ethereum address for signed price oracle (Lit jsParams pkpId); optional */
94
106
  priceOraclePkpId?: string;