@gvnrdao/dh-sdk 0.0.304 → 0.0.306

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
@@ -444,9 +444,13 @@ var init_deployment_addresses = __esm({
444
444
  reason: "Validator gas fix (#420): BitcoinAddressValidator packed-table lookups; VERSION 1.0.0 -> 1.0.1"
445
445
  },
446
446
  UCDController: {
447
+ previousImplementation: "0x16f94AcC78cE87bEeBCfF90552091B3EfC071E48",
447
448
  newImplementation: "0x28B8cAB89D89B3F83581FD2290de2Ce84535bd0A",
448
- upgradedBy: "Reconciled from chain \u2014 Upgraded event log unavailable; provenance omitted (never fabricated)",
449
- reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)"
449
+ upgradedAt: "2026-07-06T10:41:59.000Z",
450
+ upgradedBy: "AdminModule/Safe audit-batch upgradeToAndCall; executed by 0xEba7BfAeB35ee18408b424Ee0BB45AdDf776589f in tx 0xf6609d3fa43b460d953c85d8d7265b59bf177723042db79a6a700571bad777b7 (block 25472962)",
451
+ reason: "Reconciled from chain \u2014 mainnet audit rollout batch (cross-ref onchain-auth/.safe-batches/mainnet-audit-execute.json)",
452
+ executeTx: "0xf6609d3fa43b460d953c85d8d7265b59bf177723042db79a6a700571bad777b7",
453
+ block: 25472962
450
454
  },
451
455
  PositionManagerCoreModule: {
452
456
  previousImplementation: "0x6C8b0FB93F8d0239254aefaC24d4264cA2F8AAeB",
@@ -573,45 +577,47 @@ var init_deployment_addresses = __esm({
573
577
  };
574
578
  LOCALHOST_DEPLOYMENT = {
575
579
  network: "localhost",
576
- chainId: 31337,
577
- timestamp: "2026-06-11T12:30:59.589Z",
580
+ chainId: 1337,
581
+ timestamp: "2026-07-20T19:45:58.514Z",
578
582
  deployer: "",
579
583
  contracts: {
580
- MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
581
- UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
584
+ MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
585
+ LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
586
+ UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
582
587
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
583
588
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
584
- PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
585
- TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
586
- LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
587
- BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
588
- CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
589
- LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
590
- CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
591
- AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
592
- PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
593
- PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
594
- OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
595
- 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"
596
601
  }
597
602
  };
598
603
  LOCALHOST_CONTRACTS = {
599
- MessageHashBuilder: "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8",
600
- UpgradeValidator: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
604
+ MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
605
+ LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
606
+ UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
601
607
  UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
602
608
  UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
603
- PositionManagerCoreModule: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0",
604
- TermManagerModule: "0x9A676e781A523b5d0C0e43731313A708CB607508",
605
- LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
606
- BTCSpendAuthorizer: "0x59b670e9fA9D0A427751Af201D676719a970857b",
607
- CollateralManagerModule: "0xa85233C63b9Ee964Add6F2cffe00Fd84eb32338f",
608
- LiquidationManagerModule: "0x7a2088a1bFc9d81c55368AE168C2C02570cB814F",
609
- CircuitBreakerModule: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
610
- AdminModule: "0xc5a5C42992dECbae36851359345FE25997F5C42d",
611
- PositionManagerViews: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
612
- PositionManager: "0xf5059a5D33d5853360D16C683c16e67980206f36",
613
- OperationAuthorizationRegistry: "0xF8e31cb472bc70500f08Cd84917E5A1912Ec8397",
614
- 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"
615
621
  };
616
622
  ALL_DEPLOYMENTS = {
617
623
  sepolia: SEPOLIA_DEPLOYMENT,
@@ -1293,7 +1299,7 @@ var require_pkg_src = __commonJS({
1293
1299
  description: "Production Pkp Validator",
1294
1300
  version: "1.0.0",
1295
1301
  deployed: true,
1296
- deployedAt: 1783106284785,
1302
+ deployedAt: 1784569687492,
1297
1303
  size: 4140,
1298
1304
  hash: "4c0d65bf1f33cdac22a78fd8c4764319cc3fe00650e0fe7fa8a6090d9a318a7b",
1299
1305
  validatorWalletAddress: "0x82db9f515957aa380b596c6ea2bd30cef181f236",
@@ -1370,17 +1376,17 @@ var require_pkg_src = __commonJS({
1370
1376
  }
1371
1377
  },
1372
1378
  btcWithdrawal: {
1373
- cid: "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2",
1379
+ cid: "QmexvpfHPacjsVcqNHHLyJhSnnAPLgbWmWd9N3NDLDvYds",
1374
1380
  authorizedCidHex: cidToHex(
1375
- "QmYG1o9qMBZsEMDE9DNsJMzQmvxfj6jTu9V37WwuJZAtC2"
1381
+ "QmexvpfHPacjsVcqNHHLyJhSnnAPLgbWmWd9N3NDLDvYds"
1376
1382
  ),
1377
1383
  name: "Btc Withdrawal",
1378
1384
  description: "Production Btc Withdrawal",
1379
- version: "1.2.1",
1385
+ version: "1.2.3",
1380
1386
  deployed: true,
1381
- deployedAt: 1784316549409,
1382
- size: 90692,
1383
- hash: "ca7276763142fe1fc812b70f6319973000faf338a91c531f898f8f6406035c64",
1387
+ deployedAt: 1784585746901,
1388
+ size: 92027,
1389
+ hash: "7b4f4eab1f0c08b8b5b1c9cdb1064dd403ec13f57765919f62dec67fb8ba3c69",
1384
1390
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1385
1391
  pkp: {
1386
1392
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -3752,12 +3758,23 @@ ${auth.clientId}`;
3752
3758
  if (!data.vout || !Array.isArray(data.vout)) {
3753
3759
  return null;
3754
3760
  }
3755
- return data.vout.map((output, index) => ({
3756
- index,
3757
- scriptpubkey: output.scriptpubkey,
3758
- scriptpubkey_address: output.scriptpubkey_address,
3759
- value: output.value ?? 0
3760
- }));
3761
+ return data.vout.map((output, index) => {
3762
+ if (output.value === void 0) {
3763
+ throw new Error(
3764
+ `getTransactionOutputs(${txid}): output ${index} is missing its value \u2014 malformed provider response, refusing to return outputs that cannot be value-verified`
3765
+ );
3766
+ }
3767
+ assertSaneSatoshis(
3768
+ output.value,
3769
+ `getTransactionOutputs(${txid}) output ${index}`
3770
+ );
3771
+ return {
3772
+ index,
3773
+ scriptpubkey: output.scriptpubkey,
3774
+ scriptpubkey_address: output.scriptpubkey_address,
3775
+ value: output.value
3776
+ };
3777
+ });
3761
3778
  } catch (error) {
3762
3779
  clearTimeout(timeoutId);
3763
3780
  if (isAbortError(error)) {
@@ -5519,7 +5536,12 @@ var init_network_configs = __esm({
5519
5536
  NETWORK_CONFIGS = {
5520
5537
  1: getMainnetConfig(),
5521
5538
  11155111: getSepoliaConfig(),
5522
- 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 }
5523
5545
  };
5524
5546
  DEFAULT_NETWORK_CONFIG = {
5525
5547
  /** Fallback when chain is unknown — production Chipotle. */
@@ -6859,12 +6881,20 @@ async function assertSafeQuantumSubmission(opts) {
6859
6881
  ...opts.sleep ? { sleep: opts.sleep } : {}
6860
6882
  };
6861
6883
  await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6884
+ const SAME_BLOCK_INTERACTION_SELECTOR = "0x3b53d1db";
6885
+ const laneGuardActive = opts.laneGuardActive === true;
6862
6886
  for (let sim = 1; ; sim++) {
6863
6887
  try {
6864
6888
  await provider.call({ to, from, data });
6865
6889
  break;
6866
6890
  } catch (simError) {
6867
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
+ }
6868
6898
  if (decoded.selector === DEAD_ZONE_VIOLATION_SELECTOR && sim <= maxResimulations) {
6869
6899
  const sigQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6870
6900
  const realCurrentQuantum = Math.floor(now() / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
@@ -6900,6 +6930,28 @@ async function assertSafeQuantumSubmission(opts) {
6900
6930
  await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
6901
6931
  }
6902
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
+
6903
6955
  // src/utils/btc-withdrawal-message.ts
6904
6956
  import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
6905
6957
  var QUANTUM_SECONDS = 60;
@@ -15761,7 +15813,7 @@ function createMockTokenManager(config) {
15761
15813
 
15762
15814
  // src/modules/diamond-hands-sdk.ts
15763
15815
  var import_dh_lit_actions2 = __toESM(require_pkg_src());
15764
- import { LitOps } from "@gvnrdao/dh-lit-ops";
15816
+ import { LitOps, broadcastSignedBitcoinTransaction } from "@gvnrdao/dh-lit-ops";
15765
15817
 
15766
15818
  // src/utils/telegram-messaging.utils.ts
15767
15819
  var TelegramBot = null;
@@ -16246,6 +16298,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16246
16298
  this.litOps = new LitOps({
16247
16299
  mode: config.mode || "standalone",
16248
16300
  network: config.litNetwork || DEFAULT_LIT_NETWORK,
16301
+ // Completes the module's chainId resolution chain (jsParams shapes →
16302
+ // config.chainId) — e.g. the lane validator-override chain binding
16303
+ // reads it when a jsParams shape lacks chainId.
16304
+ chainId: config.chainId,
16249
16305
  signer: config.mode === "standalone" ? config.litOpsSigner : void 0,
16250
16306
  serviceEndpoint: isServiceModeConfig(config) ? config.serviceEndpoint : void 0,
16251
16307
  // Hand lit-ops a session-aware auth getter so every request it makes
@@ -16923,6 +16979,19 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
16923
16979
  }
16924
16980
  const MAX_FULL_RETRIES = 3;
16925
16981
  for (let fullRetry = 1; fullRetry <= MAX_FULL_RETRIES; fullRetry++) {
16982
+ if (await this.isLocalLaneGuardActive()) {
16983
+ const phase = Math.floor(Date.now() / 1e3) % 60;
16984
+ if (phase > 30) {
16985
+ const waitMs = (60 - phase + 1) * 1e3;
16986
+ if (this.config.debug) {
16987
+ log.info(
16988
+ `\u23F3 Quantum-phase alignment (lane-only): ${phase}s into the quantum \u2014 waiting ${Math.ceil(waitMs / 1e3)}s for the next boundary before authorization`,
16989
+ {}
16990
+ );
16991
+ }
16992
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
16993
+ }
16994
+ }
16926
16995
  if (fullRetry > 1 && this.config.debug) {
16927
16996
  log.info(
16928
16997
  `\u{1F504} Full mint retry ${fullRetry}/${MAX_FULL_RETRIES} due to timestamp staleness...`,
@@ -17671,6 +17740,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
17671
17740
  from: fromAddress,
17672
17741
  data: mintCalldata,
17673
17742
  quantumTimestamp: Number(validationResponse.timestamp),
17743
+ laneGuardActive: await this.isLocalLaneGuardActive(),
17674
17744
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
17675
17745
  });
17676
17746
  } catch (simError) {
@@ -18966,6 +19036,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
18966
19036
  from: await extendSigner.getAddress(),
18967
19037
  data: extendCalldata,
18968
19038
  quantumTimestamp: Number(quantumTimestamp),
19039
+ laneGuardActive: await this.isLocalLaneGuardActive(),
18969
19040
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
18970
19041
  });
18971
19042
  } catch (simError) {
@@ -19866,6 +19937,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
19866
19937
  from: paymentFrom,
19867
19938
  data: paymentCalldata,
19868
19939
  quantumTimestamp: Number(quantumTimestamp),
19940
+ laneGuardActive: await this.isLocalLaneGuardActive(),
19869
19941
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
19870
19942
  });
19871
19943
  } catch (simError) {
@@ -20605,6 +20677,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20605
20677
  from: await withdrawSigner.getAddress(),
20606
20678
  data: withdrawCalldata,
20607
20679
  quantumTimestamp: Number(withdrawalParams.quantumTimestamp),
20680
+ laneGuardActive: await this.isLocalLaneGuardActive(),
20608
20681
  onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
20609
20682
  });
20610
20683
  tx = await sendEip1559Transaction({
@@ -20682,7 +20755,9 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20682
20755
  gasUsed: receipt.gasUsed?.toString() || "unknown",
20683
20756
  utxoTxid: validationResponse.utxoTxid,
20684
20757
  utxoVout: validationResponse.utxoVout,
20685
- utxoSatoshis: validationResponse.utxoSatoshis
20758
+ utxoSatoshis: validationResponse.utxoSatoshis,
20759
+ // Phase B: full consensus-validated spend set (absent on older actions).
20760
+ confirmedUtxos: validationResponse.confirmedUtxos
20686
20761
  };
20687
20762
  } catch (error) {
20688
20763
  const errorMsg = error instanceof Error ? error.message : String(error);
@@ -20757,15 +20832,180 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20757
20832
  * @param request - Execution request with positionId, utxoIdentifier, and networkFee
20758
20833
  * @returns Execution result with Bitcoin txid
20759
20834
  */
20760
- async executeBTCWithdrawal(request) {
20761
- this.ensureInitialized();
20762
- if (this.config.mode !== "service") {
20835
+ /**
20836
+ * STANDALONE Phase 2: sign + broadcast the withdrawal Bitcoin transaction
20837
+ * in-process the same orchestration lit-ops-server performs, composed
20838
+ * from the same primitives (`litOps.signBTCTransaction`, which has always
20839
+ * had a standalone path, and `broadcastSignedBitcoinTransaction`).
20840
+ * Both modes must support withdrawal execution (Master mandate 2026-07-21).
20841
+ *
20842
+ * The service path's post-broadcast CRIT-2 re-verification exists to catch
20843
+ * a LYING SERVER; here the broadcast happens locally from the signatures we
20844
+ * just obtained, so the esplora-returned txid is already first-hand.
20845
+ */
20846
+ async executeBTCWithdrawalStandalonePhase2(request, envelope, txid, vout, chainId) {
20847
+ const positionDetails = await this.getPositionDetailsView(
20848
+ request.positionId
20849
+ );
20850
+ if (!positionDetails) {
20851
+ return { success: false, error: `Position ${request.positionId} not found` };
20852
+ }
20853
+ const pkpId = positionDetails.pkpId;
20854
+ const vaultAddress = positionDetails.vaultAddress;
20855
+ if (!pkpId || pkpId === ZeroHash) {
20856
+ return { success: false, error: "Position has no PKP" };
20857
+ }
20858
+ if (!vaultAddress) {
20859
+ return { success: false, error: "Position has no vault address" };
20860
+ }
20861
+ const pkpCache = this.cacheManager.getCache("pkp-data", {
20862
+ maxSize: 500,
20863
+ ttlMs: 24 * 60 * 60 * 1e3
20864
+ });
20865
+ let publicKey = pkpCache.get(request.positionId)?.publicKey;
20866
+ if (!publicKey) {
20867
+ const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
20868
+ pkpId
20869
+ });
20870
+ publicKey = derived.publicKey;
20871
+ }
20872
+ if (!publicKey) {
20873
+ return { success: false, error: "Failed to resolve PKP public key" };
20874
+ }
20875
+ const chipotlePkpAddress = computeAddress(publicKey);
20876
+ const STANDALONE_CHAIN_NAMES = {
20877
+ 1: "ethereum",
20878
+ 11155111: "sepolia",
20879
+ 1337: "hardhat",
20880
+ 31337: "hardhat"
20881
+ };
20882
+ const chainName = STANDALONE_CHAIN_NAMES[chainId];
20883
+ if (!chainName) {
20763
20884
  return {
20764
20885
  success: false,
20765
- error: "BTC withdrawal Phase 2 requires service mode. Set sdkConfig.mode = 'service' and serviceEndpoint to a lit-ops-server."
20886
+ error: `Unsupported chainId ${chainId} for standalone BTC withdrawal (supported: 1, 11155111, 1337, 31337)`
20766
20887
  };
20767
20888
  }
20768
- if (!this.config.serviceEndpoint) {
20889
+ const bitcoinNetworkName = chainId === 1 ? "mainnet" : "regtest";
20890
+ const bitcoin = await import("bitcoinjs-lib");
20891
+ const bitcoinNetwork = chainId === 1 ? bitcoin.networks.bitcoin : chainId === 1337 || chainId === 31337 ? bitcoin.networks.regtest : bitcoin.networks.testnet;
20892
+ let scriptPubKeyBuffer;
20893
+ if (vaultAddress.startsWith("bcrt1q")) {
20894
+ const bech322 = await import("bech32");
20895
+ const decoded = bech322.bech32.decode(vaultAddress);
20896
+ const witnessProgram = Buffer.from(
20897
+ bech322.bech32.fromWords(decoded.words.slice(1))
20898
+ );
20899
+ if (witnessProgram.length !== 20) {
20900
+ return {
20901
+ success: false,
20902
+ error: `Vault address ${vaultAddress} decodes to a ${witnessProgram.length}-byte witness program \u2014 expected 20-byte P2WPKH`
20903
+ };
20904
+ }
20905
+ scriptPubKeyBuffer = Buffer.concat([
20906
+ Buffer.from([0, witnessProgram.length]),
20907
+ witnessProgram
20908
+ ]);
20909
+ } else {
20910
+ scriptPubKeyBuffer = bitcoin.address.toOutputScript(
20911
+ vaultAddress,
20912
+ bitcoinNetwork
20913
+ );
20914
+ }
20915
+ const scriptPubKey = scriptPubKeyBuffer.toString("hex");
20916
+ const executionUtxos = (envelope.utxos && envelope.utxos.length ? envelope.utxos : [{ txid, vout, value: request.utxoSatoshis }]).map((u) => ({ ...u, scriptPubKey }));
20917
+ const dc = this.config.contractAddresses || {};
20918
+ const signerContractAddresses = {
20919
+ LoanOperationsManagerModule: dc.loanOperationsManager ?? "",
20920
+ PositionManager: dc.positionManager ?? "",
20921
+ BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
20922
+ PositionDelegateRegistry: dc.positionDelegateRegistry ?? "",
20923
+ OperationAuthorizationRegistry: dc.operationAuthorizationRegistry ?? "",
20924
+ ...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {}
20925
+ };
20926
+ const signResult = await this.litOps.signBTCTransaction({
20927
+ auth: {
20928
+ positionId: envelope.positionId,
20929
+ utxo: { txid, vout, value: request.utxoSatoshis, scriptPubKey },
20930
+ utxos: executionUtxos,
20931
+ networkFee: envelope.networkFee,
20932
+ targetAddress: envelope.targetAddress,
20933
+ targetAmount: envelope.targetAmount,
20934
+ timestamp: envelope.timestamp,
20935
+ signature: envelope.userSignature,
20936
+ chainId
20937
+ },
20938
+ publicKey,
20939
+ pkpId: chipotlePkpAddress,
20940
+ chain: chainName,
20941
+ bitcoinNetwork: bitcoinNetworkName,
20942
+ rpcUrl: this.config.ethRpcUrl,
20943
+ contractAddresses: signerContractAddresses
20944
+ });
20945
+ if (!signResult.success) {
20946
+ return {
20947
+ success: false,
20948
+ error: signResult.error ?? "BTC transaction signing failed"
20949
+ };
20950
+ }
20951
+ if (!signResult.unsignedTxHex || !signResult.signatures?.length) {
20952
+ return {
20953
+ success: false,
20954
+ error: "BTC signing returned incomplete data (missing unsignedTxHex/signatures)"
20955
+ };
20956
+ }
20957
+ if (!signResult.userReceivesAmount) {
20958
+ return { success: false, error: "BTC signing did not return userReceivesAmount" };
20959
+ }
20960
+ const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
20961
+ if (!broadcastUrl) {
20962
+ return {
20963
+ success: false,
20964
+ error: "Standalone Phase 2 broadcast requires config.bitcoinProviders[0].url (esplora base)"
20965
+ };
20966
+ }
20967
+ try {
20968
+ const { txid: btcTxid } = await broadcastSignedBitcoinTransaction({
20969
+ signatures: signResult.signatures,
20970
+ utxos: executionUtxos,
20971
+ pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
20972
+ destination: envelope.targetAddress,
20973
+ userReceivesAmount: signResult.userReceivesAmount,
20974
+ changeAmount: signResult.changeAmount,
20975
+ pkpBtcAddress: signResult.pkpBtcAddress,
20976
+ bitcoinNetwork: bitcoinNetworkName,
20977
+ bitcoinRpcUrl: broadcastUrl,
20978
+ ...this.config.debug ? {
20979
+ debug: (level, msg, data) => {
20980
+ if (level === "warn")
20981
+ log.warn(msg, { data });
20982
+ else if (level === "error")
20983
+ log.error(msg, { data });
20984
+ else
20985
+ log.info(msg, { data });
20986
+ }
20987
+ } : {}
20988
+ });
20989
+ if (this.config.debug) {
20990
+ log.info("\u2705 Standalone Phase 2 broadcast successful", { txid: btcTxid });
20991
+ }
20992
+ return {
20993
+ success: true,
20994
+ txid: btcTxid,
20995
+ amountSent: signResult.userReceivesAmount,
20996
+ networkFee: signResult.networkFee,
20997
+ destination: envelope.targetAddress
20998
+ };
20999
+ } catch (broadcastError) {
21000
+ return {
21001
+ success: false,
21002
+ error: `BTC broadcast failed: ${broadcastError instanceof Error ? broadcastError.message : String(broadcastError)}`
21003
+ };
21004
+ }
21005
+ }
21006
+ async executeBTCWithdrawal(request) {
21007
+ this.ensureInitialized();
21008
+ if (this.config.mode === "service" && !this.config.serviceEndpoint) {
20769
21009
  return { success: false, error: "serviceEndpoint required for BTC withdrawal" };
20770
21010
  }
20771
21011
  if (!request.positionId) {
@@ -20820,6 +21060,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
20820
21060
  signer,
20821
21061
  ...request.utxos ? { utxos: request.utxos } : {}
20822
21062
  });
21063
+ if (this.config.mode !== "service") {
21064
+ return await this.executeBTCWithdrawalStandalonePhase2(
21065
+ request,
21066
+ envelope,
21067
+ txid,
21068
+ vout,
21069
+ chainId
21070
+ );
21071
+ }
20823
21072
  const headers = {
20824
21073
  "Content-Type": "application/json",
20825
21074
  ...await this.getAuthHeader()
@@ -21365,7 +21614,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
21365
21614
  log.info("\u{1F50D} Found vault UTXO for execution", { utxoIdentifier });
21366
21615
  }
21367
21616
  let executionUtxos;
21368
- const gatheredUtxos = await this.fetchConfirmedVaultUtxos(positionId);
21617
+ const gatheredUtxos = authResult.confirmedUtxos && authResult.confirmedUtxos.length > 0 ? authResult.confirmedUtxos.map((u) => ({
21618
+ txid: u.txid,
21619
+ vout: Number(u.vout),
21620
+ value: Number(u.value),
21621
+ confirmations: 0
21622
+ })) : await this.fetchConfirmedVaultUtxos(positionId);
21369
21623
  if (gatheredUtxos.length > 0) {
21370
21624
  const normTxid = (t) => String(t).toLowerCase().replace(/^0x/, "");
21371
21625
  const byOutpoint = /* @__PURE__ */ new Map();
@@ -22132,6 +22386,25 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22132
22386
  const chain = this.config.chain?.toLowerCase() || "";
22133
22387
  return chain.includes("mainnet");
22134
22388
  }
22389
+ /**
22390
+ * Local-lane activation, verified against the LIVE node at call time (not
22391
+ * the chainId cached at init — a provider that switched networks after
22392
+ * init fails the fresh probe). Zero RPC cost outside a lane process tree:
22393
+ * without LIT_LANE_GUARD=1 this short-circuits to false before any probe.
22394
+ */
22395
+ async isLocalLaneGuardActive() {
22396
+ if (!laneGuardEnvEnabled())
22397
+ return false;
22398
+ let provider;
22399
+ try {
22400
+ provider = this.config.provider ?? this.getProviderOrThrow();
22401
+ } catch {
22402
+ return false;
22403
+ }
22404
+ return isLaneGuardActiveOnProvider(
22405
+ provider
22406
+ );
22407
+ }
22135
22408
  /**
22136
22409
  * Get Bitcoin network based on chain.
22137
22410
  *
@@ -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;
@@ -233,6 +233,16 @@ export interface BTCWithdrawalResult {
233
233
  utxoVout?: number;
234
234
  /** Full satoshi value of the authorized UTXO; needed by Phase 2 broadcast. */
235
235
  utxoSatoshis?: string;
236
+ /**
237
+ * Phase B: the FULL consensus-validated confirmed UTXO set the Lit Action
238
+ * spends, so a multi-UTXO consolidation withdrawal can sign the whole set
239
+ * without the temporary /vault-utxos endpoint. Absent on older Lit Actions.
240
+ */
241
+ confirmedUtxos?: Array<{
242
+ txid: string;
243
+ vout: number;
244
+ value: string;
245
+ }>;
236
246
  error?: string;
237
247
  }
238
248
  /**
@@ -475,6 +475,18 @@ export declare class DiamondHandsSDK {
475
475
  * @param request - Execution request with positionId, utxoIdentifier, and networkFee
476
476
  * @returns Execution result with Bitcoin txid
477
477
  */
478
+ /**
479
+ * STANDALONE Phase 2: sign + broadcast the withdrawal Bitcoin transaction
480
+ * in-process — the same orchestration lit-ops-server performs, composed
481
+ * from the same primitives (`litOps.signBTCTransaction`, which has always
482
+ * had a standalone path, and `broadcastSignedBitcoinTransaction`).
483
+ * Both modes must support withdrawal execution (Master mandate 2026-07-21).
484
+ *
485
+ * The service path's post-broadcast CRIT-2 re-verification exists to catch
486
+ * a LYING SERVER; here the broadcast happens locally from the signatures we
487
+ * just obtained, so the esplora-returned txid is already first-hand.
488
+ */
489
+ private executeBTCWithdrawalStandalonePhase2;
478
490
  executeBTCWithdrawal(request: {
479
491
  positionId: string;
480
492
  utxoIdentifier: string;
@@ -944,6 +956,13 @@ export declare class DiamondHandsSDK {
944
956
  * Check if running on production network
945
957
  */
946
958
  private isProductionNetwork;
959
+ /**
960
+ * Local-lane activation, verified against the LIVE node at call time (not
961
+ * the chainId cached at init — a provider that switched networks after
962
+ * init fails the fresh probe). Zero RPC cost outside a lane process tree:
963
+ * without LIT_LANE_GUARD=1 this short-circuits to false before any probe.
964
+ */
965
+ private isLocalLaneGuardActive;
947
966
  /**
948
967
  * Get Bitcoin network based on chain.
949
968
  *
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Local-verification-lane guard — the ONLY file in the SDK allowed to read
3
+ * the `LIT_LANE_*` environment (enforced by
4
+ * `contracts/tests/unit/lane-guard-env-allowlist.unit.test.ts`).
5
+ *
6
+ * The lane hooks (quantum-phase alignment before mint authorization, the
7
+ * simulated-SameBlockInteraction tolerance) exist solely to absorb local
8
+ * hardhat artifacts: tunnel latency and automine's stale-latest-block
9
+ * simulation. They are never correct on a live network, so activation is
10
+ * TRIPLY bound, verified against the live node at the moment of use:
11
+ *
12
+ * 1. `LIT_LANE_GUARD=1` — exported only by `scripts/lane-env-guard.sh`
13
+ * for the detached local lane's process tree, AND
14
+ * 2. a fresh `eth_chainId` probe (NOT ethers' init-time network cache, so a
15
+ * provider that switched networks after init cannot keep the hooks
16
+ * alive) returns a hardhat chainId (1337 `npx hardhat node`, 31337
17
+ * in-process default), AND
18
+ * 3. the node self-identifies as Hardhat via `web3_clientVersion` — a
19
+ * live/private network that merely reuses chainId 1337/31337 does not.
20
+ *
21
+ * Any probe failure, missing `send` capability, or unexpected answer is
22
+ * treated as NOT the lane — fail closed. The on-chain guards remain fully
23
+ * authoritative in every mode regardless.
24
+ */
25
+ /** Minimal raw-RPC seam — satisfied by ethers v6 JsonRpcApiProvider. */
26
+ export interface RawRpcSender {
27
+ send?(method: string, params: unknown[]): Promise<unknown>;
28
+ }
29
+ /** The env half of the guard: is the lane environment exported at all? */
30
+ export declare function laneGuardEnvEnabled(): boolean;
31
+ /**
32
+ * Full lane-guard check against the LIVE node. Cheap short-circuit (no RPC)
33
+ * when the lane env is not exported — the two probes only ever run inside a
34
+ * lane process tree.
35
+ */
36
+ export declare function isLaneGuardActiveOnProvider(provider: RawRpcSender | undefined | null): Promise<boolean>;
@@ -61,6 +61,14 @@ export interface SafeQuantumSubmissionOpts {
61
61
  sleep?: (ms: number) => Promise<void>;
62
62
  /** Debug sink; called with human-readable progress lines. */
63
63
  onDebug?: (msg: string) => void;
64
+ /**
65
+ * Local-lane activation, computed by the caller at submission time via
66
+ * `isLaneGuardActiveOnProvider` (lane env exported + live node probes as a
67
+ * hardhat chain/client). Only consulted by the local-lane
68
+ * SameBlockInteraction tolerance; omitted or false keeps the fail-fast
69
+ * behavior on every decodable revert.
70
+ */
71
+ laneGuardActive?: boolean;
64
72
  }
65
73
  /**
66
74
  * Gate + pre-send simulation loop. Resolves when it is safe to broadcast;