@gvnrdao/dh-sdk 0.0.323 → 0.0.324

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
@@ -1385,30 +1385,30 @@ var require_pkg_src = __commonJS({
1385
1385
  }
1386
1386
  },
1387
1387
  btcTransactionSigner: {
1388
- cid: "QmPncBUbxN9giWntJcvBMFW9GDAgA3DwgAkUAHxSsY67ic",
1388
+ cid: "QmakWzTce8NH1Jkgv1xwg83Bg9WmF2UG1Mq76pDYDSwwUp",
1389
1389
  authorizedCidHex: cidToHex(
1390
- "QmPncBUbxN9giWntJcvBMFW9GDAgA3DwgAkUAHxSsY67ic"
1390
+ "QmakWzTce8NH1Jkgv1xwg83Bg9WmF2UG1Mq76pDYDSwwUp"
1391
1391
  ),
1392
1392
  name: "Btc Transaction Signer",
1393
1393
  description: "Production Btc Transaction Signer",
1394
1394
  version: "1.1.1",
1395
1395
  deployed: true,
1396
- deployedAt: 1785226367707,
1397
- size: 66624,
1398
- hash: "9e0d9909519d5133bab03a9491566870b08d8b42c80e2d768bd24f02946cc781"
1396
+ deployedAt: 1786113907707,
1397
+ size: 67999,
1398
+ hash: "83f3644c68819215e05ba8db3225571354a009458fc1437f606df6cb9142d3f9"
1399
1399
  },
1400
1400
  ucdMintValidator: {
1401
- cid: "QmPRLCfseSWCAS4PpvQWt9aEd6rXqWYG4JyGTn26KYk6xc",
1401
+ cid: "Qmd8jgKRceLvf5aoTtwdXG2JqMoBK5cRJwrvEtQkgvbFbx",
1402
1402
  authorizedCidHex: cidToHex(
1403
- "QmPRLCfseSWCAS4PpvQWt9aEd6rXqWYG4JyGTn26KYk6xc"
1403
+ "Qmd8jgKRceLvf5aoTtwdXG2JqMoBK5cRJwrvEtQkgvbFbx"
1404
1404
  ),
1405
1405
  name: "Ucd Mint Validator",
1406
1406
  description: "Production Ucd Mint Validator",
1407
1407
  version: "1.2.1",
1408
1408
  deployed: true,
1409
- deployedAt: 1785226381631,
1410
- size: 81658,
1411
- hash: "d8ea21622885c3d26fdb64e2ba9d2f9460753a5cd7934b2928858899537443eb",
1409
+ deployedAt: 1786113908489,
1410
+ size: 82739,
1411
+ hash: "2bdd9aebdaad58a43a4731d5b7284f2531b7a9caaf8caf5719d891c4ed33cb2e",
1412
1412
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1413
1413
  pkp: {
1414
1414
  publicKey: "0x041ab17cd91fc5c0b761eea6092d032807561b621b82c488826776e04a9158d61ba64d809a4729f0501289d980732cb06d8dfd06999dd9c8efd2f495dad78b31bb",
@@ -1416,17 +1416,17 @@ var require_pkg_src = __commonJS({
1416
1416
  }
1417
1417
  },
1418
1418
  processPaymentValidator: {
1419
- cid: "QmNUzmHQHsaKThGkfZiD3xZrN64qZfn4P6J4MqWgvkeEsX",
1419
+ cid: "QmNXVdysXDhCaQ1uNE2Vgjbuzy6yUEnnVgHzmDfNKXMU6z",
1420
1420
  authorizedCidHex: cidToHex(
1421
- "QmNUzmHQHsaKThGkfZiD3xZrN64qZfn4P6J4MqWgvkeEsX"
1421
+ "QmNXVdysXDhCaQ1uNE2Vgjbuzy6yUEnnVgHzmDfNKXMU6z"
1422
1422
  ),
1423
1423
  name: "Process Payment Validator",
1424
1424
  description: "Production Process Payment Validator",
1425
1425
  version: "1.2.1",
1426
1426
  deployed: true,
1427
- deployedAt: 1785226385509,
1428
- size: 77655,
1429
- hash: "3c2af3c3e62e8e606d35f81fd7e6f4943f8516a9bef74f4150c8c1767cbd316f",
1427
+ deployedAt: 1786113909138,
1428
+ size: 78736,
1429
+ hash: "17191ecc9c009e0fa5ab52c876330beb2a9a75d30cccb92e7c0c04f169df088b",
1430
1430
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1431
1431
  pkp: {
1432
1432
  publicKey: "0x04cfccb9b6c7addc79469c0e9fafebef4c70801302c62cdb26b610723a9b8527d23f98a40158c7340d83fd0e62d02b5ec74c12f7a86d98e8c6973b4811110f19ad",
@@ -1434,17 +1434,17 @@ var require_pkg_src = __commonJS({
1434
1434
  }
1435
1435
  },
1436
1436
  extendPositionValidator: {
1437
- cid: "QmY2t41w1maaoDmdRPx6wTfciHPQAydSpLSKwKVFSS2DPM",
1437
+ cid: "QmYwBsyrhGfHWyRKx549G4tKHxsBLAuRUHcecigeUCkFav",
1438
1438
  authorizedCidHex: cidToHex(
1439
- "QmY2t41w1maaoDmdRPx6wTfciHPQAydSpLSKwKVFSS2DPM"
1439
+ "QmYwBsyrhGfHWyRKx549G4tKHxsBLAuRUHcecigeUCkFav"
1440
1440
  ),
1441
1441
  name: "Extend Position Validator",
1442
1442
  description: "Production Extend Position Validator",
1443
1443
  version: "1.2.1",
1444
1444
  deployed: true,
1445
- deployedAt: 1785226398291,
1446
- size: 75434,
1447
- hash: "ba55532fbeaf6e65d180b869fd69ee06393d574daa0ace524a9e831d846539a5",
1445
+ deployedAt: 1786113909872,
1446
+ size: 76515,
1447
+ hash: "1d4e085335af80f194a663a577876902c83015177e95906a0b461fa93bd5da08",
1448
1448
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1449
1449
  pkp: {
1450
1450
  publicKey: "0x04e52338f6a0c3362800f1a94c4c8f08be5030dde0fa217388c53d1c1a4790f4443c9f2886b2d2d8f4b75a9a156e9b028011b1a1cf7648da0e556aa980ec692836",
@@ -1452,9 +1452,9 @@ var require_pkg_src = __commonJS({
1452
1452
  }
1453
1453
  },
1454
1454
  btcWithdrawal: {
1455
- cid: "QmcBg8vPXsCmiKDk28bJVTqDpTrwCzSYAGu5V6ABVVPuK5",
1455
+ cid: "QmZuR3qSBajutXfRd91ffpyFrRMgmaMh7fDEHRiQQF8Ljp",
1456
1456
  authorizedCidHex: cidToHex(
1457
- "QmcBg8vPXsCmiKDk28bJVTqDpTrwCzSYAGu5V6ABVVPuK5"
1457
+ "QmZuR3qSBajutXfRd91ffpyFrRMgmaMh7fDEHRiQQF8Ljp"
1458
1458
  ),
1459
1459
  name: "Btc Withdrawal",
1460
1460
  description: "Production Btc Withdrawal",
@@ -1464,9 +1464,9 @@ var require_pkg_src = __commonJS({
1464
1464
  // than the one it replaced, which a rotation ceremony would treat as a rollback.
1465
1465
  version: "1.2.4",
1466
1466
  deployed: true,
1467
- deployedAt: 1785226412397,
1468
- size: 92754,
1469
- hash: "d3a8ec62e0b1e6d0566a154a884bf7ee4e25ef6dc2dd84fde98211d25c1dede5",
1467
+ deployedAt: 1786113910531,
1468
+ size: 95313,
1469
+ hash: "ce12f11e3b5469953bc919586e4f3b8b5d594224b58b799dd9a3b60e3749559c",
1470
1470
  validatorWalletAddress: "0x82DB9F515957aA380B596C6eA2Bd30CEF181f236",
1471
1471
  pkp: {
1472
1472
  publicKey: "0x043616787c5432415c24378c4ef48de2bcd6bb7b575b837e3cff09171802662da7105d79586c7659677a0ecbaddac4cce06cb2a11f69a16fa0c4d7002ac7d51a4d",
@@ -1577,6 +1577,21 @@ var require_pkg_src = __commonJS({
1577
1577
  size: 1046,
1578
1578
  hash: "03ee5be239c91c735d2c95db89a0d1b77e602cfc18af8eb64b3281fddcd90a72"
1579
1579
  },
1580
+ agentModuleExecutor: {
1581
+ // Bound to the isolated Sepolia agent group 7 ("DH-Agent-Sepolia"), which
1582
+ // holds this action and no PKPs until an agent is minted into it. NOT yet
1583
+ // bound on mainnet — the Base `DH-Agent-Mainnet` group is created at rollout.
1584
+ cid: "QmVgjGXUuxoGf8XyddW8a7btaazT9yEotpKoBgb65oPvcF",
1585
+ authorizedCidHex: cidToHex(
1586
+ "QmVgjGXUuxoGf8XyddW8a7btaazT9yEotpKoBgb65oPvcF"
1587
+ ),
1588
+ name: "Agent Module Executor",
1589
+ description: "Capability gate for multi-sig Safe agent delegation: binds the agent PKP to the AgentDelegationRegistry record before signing (authorize | execute modes)",
1590
+ version: "1.0.0",
1591
+ deployed: true,
1592
+ size: 11785,
1593
+ hash: "fa294e164273c2158f9ef11a0ccfd735c343c0e3759fcb4b37e0f8a5d1ab0d29"
1594
+ },
1580
1595
  staleSpendRecoveryValidator: {
1581
1596
  cid: "QmXQWT8V4wjuvH8BbC7xrkYxsVu5AKfvfWcS7jnYfrexYm",
1582
1597
  authorizedCidHex: cidToHex(
@@ -1596,17 +1611,17 @@ var require_pkg_src = __commonJS({
1596
1611
  }
1597
1612
  },
1598
1613
  btcUtxoInvalidator: {
1599
- cid: "QmZE422cZsEjYN8pE6AmpkgFoZCK4UAfdDuzoVRRsMJbwm",
1614
+ cid: "Qmf4Ze8yhnQhHbnX5RPgJrC7Jw1aA3gbpfpd52kFg7fd1m",
1600
1615
  authorizedCidHex: cidToHex(
1601
- "QmZE422cZsEjYN8pE6AmpkgFoZCK4UAfdDuzoVRRsMJbwm"
1616
+ "Qmf4Ze8yhnQhHbnX5RPgJrC7Jw1aA3gbpfpd52kFg7fd1m"
1602
1617
  ),
1603
1618
  name: "Btc Utxo Invalidator",
1604
1619
  description: "Production Btc Utxo Invalidator",
1605
1620
  version: "1.0.0",
1606
1621
  deployed: true,
1607
- deployedAt: 1785226423791,
1608
- size: 61472,
1609
- hash: "75eca528d8ac30ae8f21caaae206ccab162a83a048d622868e60f645f0c475e2"
1622
+ deployedAt: 1786113911948,
1623
+ size: 62553,
1624
+ hash: "1dcea2b49e545c423195be22e072dc5e3039ed1c95a8490b23505956029f5b33"
1610
1625
  }
1611
1626
  };
1612
1627
  function getDeploymentsForNetwork(network) {
@@ -6164,7 +6179,10 @@ import { keccak256, toUtf8Bytes, solidityPackedKeccak256, getBytes, Contract } f
6164
6179
  // src/utils/quantum-timing.ts
6165
6180
  var QUANTUM_WINDOW_SECONDS = 60;
6166
6181
  var DEAD_ZONE_SECONDS = 8;
6167
- var INCLUSION_LATENCY_BUDGET = 16;
6182
+ var SLOT_SECONDS = 12;
6183
+ var INCLUSION_SLOT_TOLERANCE = 3;
6184
+ var INCLUSION_LATENCY_BUDGET = SLOT_SECONDS * INCLUSION_SLOT_TOLERANCE;
6185
+ var MAX_GATE_POLLS = 12;
6168
6186
  var POST_BOUNDARY_SKEW_MARGIN = 3;
6169
6187
  function calculateNextQuantumTimestamp() {
6170
6188
  const now = Math.floor(Date.now() / 1e3);
@@ -6197,8 +6215,17 @@ function validateQuantumTiming(signedTimestamp, _bufferSeconds = 30) {
6197
6215
  async function awaitSafeSubmissionWindow(quantumTimestamp, opts = {}) {
6198
6216
  const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
6199
6217
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
6200
- const nowSec = now();
6201
6218
  const signatureQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6219
+ if (opts.chainNow) {
6220
+ const chainResult = await awaitSafeSubmissionWindowOnChain(
6221
+ signatureQuantum,
6222
+ opts.chainNow,
6223
+ sleep
6224
+ );
6225
+ if (chainResult)
6226
+ return chainResult;
6227
+ }
6228
+ const nowSec = now();
6202
6229
  const currentQuantum = Math.floor(nowSec / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6203
6230
  const secsToBoundary = QUANTUM_WINDOW_SECONDS - nowSec % QUANTUM_WINDOW_SECONDS;
6204
6231
  const isNextQuantumSig = signatureQuantum === currentQuantum + QUANTUM_WINDOW_SECONDS;
@@ -6215,6 +6242,44 @@ async function awaitSafeSubmissionWindow(quantumTimestamp, opts = {}) {
6215
6242
  }
6216
6243
  return { waited: false, waitedSeconds: 0 };
6217
6244
  }
6245
+ async function awaitSafeSubmissionWindowOnChain(signatureQuantum, chainNow, sleep) {
6246
+ let waitedSeconds = 0;
6247
+ for (let poll = 0; poll < MAX_GATE_POLLS; poll++) {
6248
+ let head;
6249
+ try {
6250
+ head = await chainNow();
6251
+ } catch {
6252
+ head = null;
6253
+ }
6254
+ if (head === null || !Number.isFinite(head)) {
6255
+ return waitedSeconds > 0 ? { waited: true, waitedSeconds } : null;
6256
+ }
6257
+ const chainQuantum = Math.floor(head / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
6258
+ if (signatureQuantum <= chainQuantum) {
6259
+ return { waited: waitedSeconds > 0, waitedSeconds };
6260
+ }
6261
+ const earliestMine = head + SLOT_SECONDS;
6262
+ const latestMine = head + SLOT_SECONDS * INCLUSION_SLOT_TOLERANCE;
6263
+ const safeBeforeDeadZone = latestMine < signatureQuantum - DEAD_ZONE_SECONDS;
6264
+ const safeAfterBoundary = earliestMine >= signatureQuantum;
6265
+ if (safeBeforeDeadZone || safeAfterBoundary) {
6266
+ return { waited: waitedSeconds > 0, waitedSeconds };
6267
+ }
6268
+ const sleepSeconds = Math.max(
6269
+ 1,
6270
+ Math.min(SLOT_SECONDS, signatureQuantum - head)
6271
+ );
6272
+ log.debug(
6273
+ `[Quantum Timing] \u23F3 Deferring send ${sleepSeconds}s to clear the quantum dead zone (sig quantum ${signatureQuantum}, chain head ${head}, insured mine window [${earliestMine}, ${latestMine}])`
6274
+ );
6275
+ await sleep(sleepSeconds * 1e3);
6276
+ waitedSeconds += sleepSeconds;
6277
+ }
6278
+ log.debug(
6279
+ `[Quantum Timing] \u26A0\uFE0F Dead-zone gate exhausted ${MAX_GATE_POLLS} chain polls (sig quantum ${signatureQuantum}) \u2014 proceeding`
6280
+ );
6281
+ return { waited: waitedSeconds > 0, waitedSeconds };
6282
+ }
6218
6283
 
6219
6284
  // src/utils/mint-authorization.utils.ts
6220
6285
  var PKP_NFT_ABI = [
@@ -7171,6 +7236,20 @@ function decodeQuantumRevert(e) {
7171
7236
  }
7172
7237
  return { data, selector, name };
7173
7238
  }
7239
+ var RETRYABLE_QUANTUM_ERROR_NAMES = [
7240
+ "DeadZoneViolation",
7241
+ "QuantumOutsideWindow",
7242
+ "QuantumAlreadyUsed",
7243
+ "QuantumExpired"
7244
+ ];
7245
+ function isRetryableQuantumFailure(message) {
7246
+ if (!message)
7247
+ return false;
7248
+ if (message.includes("Transaction timeout") || message.includes("Transaction confirmation failed") || message.includes("Transaction receipt not found")) {
7249
+ return false;
7250
+ }
7251
+ return RETRYABLE_QUANTUM_ERROR_NAMES.some((name) => message.includes(name)) || message.includes("Transaction reverted");
7252
+ }
7174
7253
 
7175
7254
  // src/utils/quantum-submission.utils.ts
7176
7255
  var MAX_DEADZONE_RESIMULATIONS = 3;
@@ -7192,9 +7271,14 @@ async function assertSafeQuantumSubmission(opts) {
7192
7271
  const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
7193
7272
  const onDebug = opts.onDebug ?? (() => {
7194
7273
  });
7274
+ const chainNow = opts.chainNow ?? (typeof provider.getBlock === "function" ? async () => {
7275
+ const block = await provider.getBlock("latest");
7276
+ return typeof block?.timestamp === "number" ? block.timestamp : null;
7277
+ } : void 0);
7195
7278
  const gateOpts = {
7196
7279
  now,
7197
- ...opts.sleep ? { sleep: opts.sleep } : {}
7280
+ ...opts.sleep ? { sleep: opts.sleep } : {},
7281
+ ...chainNow ? { chainNow } : {}
7198
7282
  };
7199
7283
  await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
7200
7284
  const SAME_BLOCK_INTERACTION_SELECTOR = "0x3b53d1db";
@@ -22218,10 +22302,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22218
22302
  isRetryablePaymentQuantumFailure(result) {
22219
22303
  if (result.success || !result.error)
22220
22304
  return false;
22221
- const e = result.error;
22222
- if (e.includes("Transaction timeout"))
22223
- return false;
22224
- return e.includes("DeadZoneViolation") || e.includes("QuantumOutsideWindow") || e.includes("QuantumAlreadyUsed") || e.includes("QuantumExpired") || e === "Transaction reverted";
22305
+ return isRetryableQuantumFailure(result.error);
22225
22306
  }
22226
22307
  /**
22227
22308
  * One payment attempt: user auth → Lit Action authorization → dead-zone gate →
@@ -22860,6 +22941,37 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
22860
22941
  }
22861
22942
  return this.withdrawalAddressModule.getEntries(target);
22862
22943
  }
22944
+ /**
22945
+ * Recover the revert reason of an already-mined, already-reverted transaction.
22946
+ *
22947
+ * A status-0 receipt carries no revert data, and ethers does not re-fetch it, so
22948
+ * the reason has to be recovered by replaying the exact calldata as an `eth_call`
22949
+ * pinned to the block that mined it — the only block whose `block.timestamp`
22950
+ * reproduces a timing-dependent revert such as `DeadZoneViolation()`.
22951
+ *
22952
+ * Caveat: the replay runs against end-of-block state rather than the state at the
22953
+ * transaction's own index, so for a state-dependent revert the recovered name is
22954
+ * diagnostic, not proof. Timing reverts (the case this exists for) are unaffected —
22955
+ * they depend only on the block timestamp.
22956
+ *
22957
+ * @returns the decoded error name, its raw selector if unknown, or `null` when the
22958
+ * replay yields no revert data at all (never throws — a failed diagnosis
22959
+ * must not mask the underlying failure).
22960
+ */
22961
+ async decodeMinedRevert(params) {
22962
+ try {
22963
+ await this.getProviderOrThrow().call({
22964
+ to: params.to,
22965
+ from: params.from,
22966
+ data: params.data,
22967
+ blockTag: params.blockNumber
22968
+ });
22969
+ return null;
22970
+ } catch (replayError) {
22971
+ const decoded = decodeQuantumRevert(replayError);
22972
+ return decoded.name ?? decoded.selector ?? null;
22973
+ }
22974
+ }
22863
22975
  /**
22864
22976
  * Withdraw Bitcoin from a position
22865
22977
  *
@@ -23433,9 +23545,23 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
23433
23545
  txHash: tx.hash
23434
23546
  });
23435
23547
  }
23548
+ const withdrawProvider = this.getProviderOrThrow();
23549
+ const withdrawTimeoutMs = this.config.transactionTimeoutMs || 3e5;
23436
23550
  let receipt;
23437
23551
  try {
23438
- receipt = await tx.wait();
23552
+ receipt = await Promise.race([
23553
+ withdrawProvider.waitForTransaction(tx.hash, 1),
23554
+ new Promise(
23555
+ (_, reject) => setTimeout(
23556
+ () => reject(
23557
+ new Error(
23558
+ `Transaction timeout after ${withdrawTimeoutMs}ms. Hash: ${tx.hash}`
23559
+ )
23560
+ ),
23561
+ withdrawTimeoutMs
23562
+ )
23563
+ )
23564
+ ]);
23439
23565
  } catch (waitError) {
23440
23566
  const waitErrorMsg = waitError instanceof Error ? waitError.message : String(waitError);
23441
23567
  if (this.config.debug) {
@@ -23452,7 +23578,26 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
23452
23578
  if (!receipt) {
23453
23579
  return {
23454
23580
  success: false,
23455
- error: `Transaction confirmation returned no receipt. Hash: ${tx.hash}`
23581
+ error: `Transaction receipt not found for hash: ${tx.hash}`
23582
+ };
23583
+ }
23584
+ if (receipt.status === 0) {
23585
+ const revertName = await this.decodeMinedRevert({
23586
+ to: positionManagerAddress,
23587
+ from: await withdrawSigner.getAddress(),
23588
+ data: withdrawCalldata,
23589
+ blockNumber: receipt.blockNumber
23590
+ });
23591
+ if (this.config.debug) {
23592
+ log.error("\u274C withdrawBTC reverted on-chain", {
23593
+ txHash: receipt.hash,
23594
+ blockNumber: receipt.blockNumber,
23595
+ revert: revertName ?? "undecodable"
23596
+ });
23597
+ }
23598
+ return {
23599
+ success: false,
23600
+ error: `Transaction reverted: ${revertName ?? "unknown reason"} (tx: ${receipt.hash})`
23456
23601
  };
23457
23602
  }
23458
23603
  if (this.config.debug) {
@@ -24580,13 +24725,11 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
24580
24725
  if (authResult.success) {
24581
24726
  break;
24582
24727
  }
24583
- const isQuantumError = authResult.error?.includes(
24584
- "QuantumOutsideWindow"
24585
- );
24728
+ const isQuantumError = isRetryableQuantumFailure(authResult.error);
24586
24729
  if (isQuantumError && wAttempt < MAX_WITHDRAWAL_QUANTUM_RETRIES) {
24587
24730
  if (this.config.debug) {
24588
24731
  log.warn(
24589
- `\u26A0\uFE0F Phase 1 QuantumOutsideWindow (attempt ${wAttempt}/${MAX_WITHDRAWAL_QUANTUM_RETRIES}). LIT action took too long. Retrying with fresh quantum...`
24732
+ `\u26A0\uFE0F Phase 1 quantum-timing failure (attempt ${wAttempt}/${MAX_WITHDRAWAL_QUANTUM_RETRIES}): ${authResult.error}. Retrying with a fresh quantum...`
24590
24733
  );
24591
24734
  }
24592
24735
  await new Promise((resolve) => setTimeout(resolve, 2e3));
@@ -540,6 +540,24 @@ export declare class DiamondHandsSDK {
540
540
  * address strings, query the subgraph `WithdrawalAddressBook`.
541
541
  */
542
542
  getApprovedWithdrawalAddresses(user?: string): Promise<Result<import("./withdrawal-address/withdrawal-address.module").WithdrawalAddressEntry[], SDKError>>;
543
+ /**
544
+ * Recover the revert reason of an already-mined, already-reverted transaction.
545
+ *
546
+ * A status-0 receipt carries no revert data, and ethers does not re-fetch it, so
547
+ * the reason has to be recovered by replaying the exact calldata as an `eth_call`
548
+ * pinned to the block that mined it — the only block whose `block.timestamp`
549
+ * reproduces a timing-dependent revert such as `DeadZoneViolation()`.
550
+ *
551
+ * Caveat: the replay runs against end-of-block state rather than the state at the
552
+ * transaction's own index, so for a state-dependent revert the recovered name is
553
+ * diagnostic, not proof. Timing reverts (the case this exists for) are unaffected —
554
+ * they depend only on the block timestamp.
555
+ *
556
+ * @returns the decoded error name, its raw selector if unknown, or `null` when the
557
+ * replay yields no revert data at all (never throws — a failed diagnosis
558
+ * must not mask the underlying failure).
559
+ */
560
+ private decodeMinedRevert;
543
561
  /**
544
562
  * Withdraw Bitcoin from a position
545
563
  *
@@ -39,3 +39,24 @@ export interface DecodedRevert {
39
39
  export declare function decodeQuantumRevert(e: any): DecodedRevert;
40
40
  /** True iff the error decodes to `DeadZoneViolation()` from any provider nesting. */
41
41
  export declare function isDeadZoneViolation(e: any): boolean;
42
+ /**
43
+ * Whether a failed quantum-signed operation can be safely retried with a fresh
44
+ * signature. Shared by the payment and BTC-withdrawal retry loops so the two paths
45
+ * cannot drift apart again (they had: withdrawal recognised only
46
+ * `QuantumOutsideWindow`, and never decoded a mined revert at all, so the mainnet
47
+ * `DeadZoneViolation` of 2026-08-07 surfaced as a hard failure on a flow that
48
+ * payments recover from automatically).
49
+ *
50
+ * **Retryable** — the operation provably did not happen:
51
+ * - a pre-send simulation quantum error (nothing was ever broadcast), or
52
+ * - a mined revert (`status === 0`): atomic, so no funds moved, no authorization
53
+ * was recorded, and the quantum replay lane is untouched. Re-signing is safe.
54
+ *
55
+ * **NOT retryable** — the outcome is unknown, so a resubmit risks double-spending:
56
+ * - a confirmation timeout (the tx may still be pending), or
57
+ * - a confirmation failure with no receipt (transport died mid-wait).
58
+ *
59
+ * Callers MUST therefore only produce a "Transaction reverted" message after
60
+ * observing a receipt with `status === 0`.
61
+ */
62
+ export declare function isRetryableQuantumFailure(message: string | null | undefined): boolean;
@@ -37,13 +37,21 @@ export declare class QuantumRevertError extends Error {
37
37
  constructor(errorName: string | null, selector: string | null, data: string | null, cause: unknown);
38
38
  }
39
39
  export interface SafeQuantumSubmissionOpts {
40
- /** Provider used for the `eth_call` simulation (only `.call` is needed). */
40
+ /**
41
+ * Provider used for the `eth_call` simulation and — when it exposes `getBlock`
42
+ * (every ethers provider does) — for the dead-zone gate's chain clock. Without
43
+ * `getBlock` the gate silently degrades to the client's local clock, which on the
44
+ * browser path is exactly the skew we are trying to design out.
45
+ */
41
46
  provider: {
42
47
  call(tx: {
43
48
  to: string;
44
49
  from: string;
45
50
  data: string;
46
51
  }): Promise<string>;
52
+ getBlock?(blockTag: string): Promise<{
53
+ timestamp: number;
54
+ } | null | undefined>;
47
55
  };
48
56
  /** Target contract address. */
49
57
  to: string;
@@ -59,6 +67,12 @@ export interface SafeQuantumSubmissionOpts {
59
67
  now?: () => number;
60
68
  /** Sleep seam, forwarded to the gate. */
61
69
  sleep?: (ms: number) => Promise<void>;
70
+ /**
71
+ * Chain-clock seam (latest mined block timestamp, whole seconds). Defaults to
72
+ * `provider.getBlock("latest")` when the provider exposes it. Pass explicitly to
73
+ * override in tests, or `null`-resolving to force the local-clock fallback.
74
+ */
75
+ chainNow?: () => Promise<number | null>;
62
76
  /** Debug sink; called with human-readable progress lines. */
63
77
  onDebug?: (msg: string) => void;
64
78
  /**
@@ -79,7 +93,9 @@ export interface SafeQuantumSubmissionOpts {
79
93
  * 1. **Gate** (`awaitSafeSubmissionWindow`): a NEXT-quantum signature mined in
80
94
  * the last DEAD_ZONE_SECONDS of the current quantum reverts
81
95
  * `DeadZoneViolation()` on-chain, so defer the send across the boundary
82
- * when inclusion could land there.
96
+ * when inclusion could land there. Anchored on `provider.getBlock("latest")`
97
+ * — the same clock `block.timestamp` comes from — so a skewed client clock
98
+ * cannot wave a doomed send through.
83
99
  * 2. **Simulate** (`eth_call`) and route the outcome:
84
100
  * - success → safe to broadcast.
85
101
  * - `DeadZoneViolation()` → `eth_call` runs against the LATEST block's
@@ -87,7 +103,11 @@ export interface SafeQuantumSubmissionOpts {
87
103
  * the signature is already the CURRENT quantum in real time it can never
88
104
  * be dead-zoned on-chain — stale-block artifact, proceed. Otherwise the
89
105
  * signature is still NEXT near the boundary: re-gate across it and
90
- * re-simulate (bounded by `maxResimulations`).
106
+ * re-simulate (bounded by `maxResimulations`). This artifact test must use
107
+ * the LOCAL clock, not the chain head: the chain head IS the stale value
108
+ * the simulation reverted against, so anchoring it there would classify
109
+ * every artifact as real. A fast client clock wrongly reaching "artifact"
110
+ * is caught by the chain-anchored final re-gate in step 3.
91
111
  * - any other DECODABLE revert → throw `QuantumRevertError` (fail fast: no
92
112
  * broadcast, no gas burned on a doomed tx).
93
113
  * - UNDECODABLE failure (no revert data: RPC timeout, rate limit, provider
@@ -28,13 +28,38 @@
28
28
  */
29
29
  export declare const QUANTUM_WINDOW_SECONDS = 60;
30
30
  export declare const DEAD_ZONE_SECONDS = 8;
31
+ /** Ethereum slot cadence. Block timestamps are multiples of 12s from genesis. */
32
+ export declare const SLOT_SECONDS = 12;
33
+ /**
34
+ * Slots of inclusion latency we insure against. A broadcast is NOT guaranteed to
35
+ * land in the very next slot: builders skip low-tip transactions and slots are
36
+ * missed, so 2–3 slot inclusion is routine on mainnet.
37
+ *
38
+ * Sized from a real mainnet failure (tx 0x5933d64d…, 2026-08-07): a NEXT-quantum
39
+ * withdrawal was broadcast with ~25s to the boundary — outside the old 16s budget,
40
+ * so the gate let it through — then sat out one near-empty block (0.0007 gwei tip)
41
+ * and was mined 24s later at second 59, one second inside the trailing dead zone.
42
+ * `DeadZoneViolation()`. 3 slots (36s) covers that skip with a slot to spare.
43
+ */
44
+ export declare const INCLUSION_SLOT_TOLERANCE = 3;
31
45
  /**
32
46
  * Seconds of mainnet inclusion latency we insure against BEFORE the quantum
33
- * boundary. ~1.3 of Ethereum's 12s slots. A NEXT/PAST-quantum send within
47
+ * boundary. A NEXT/PAST-quantum send within
34
48
  * `DEAD_ZONE_SECONDS + INCLUSION_LATENCY_BUDGET` of the boundary is deferred so it
35
49
  * cannot be mined in the trailing dead zone `[boundary - 8, boundary)`.
50
+ *
51
+ * Used by the LOCAL-CLOCK fallback path only. When a chain clock is available the
52
+ * gate reasons in whole slots instead (see `awaitSafeSubmissionWindow`), which is
53
+ * both tighter and immune to client clock skew.
36
54
  */
37
- export declare const INCLUSION_LATENCY_BUDGET = 16;
55
+ export declare const INCLUSION_LATENCY_BUDGET: number;
56
+ /**
57
+ * Cap on chain-clock polls while deferring across a boundary. Each poll sleeps at
58
+ * most one slot, so this bounds a deferral at ~`MAX_GATE_POLLS` slots — comfortably
59
+ * more than the ~44s worst-case wait, while refusing to spin forever on a stuck or
60
+ * lying RPC.
61
+ */
62
+ export declare const MAX_GATE_POLLS = 12;
38
63
  /**
39
64
  * Seconds of client-clock-vs-chain skew guard we add AFTER the boundary when
40
65
  * deferring. A CURRENT-quantum signature is never dead-zoned, so this only needs to
@@ -108,6 +133,16 @@ export interface SafeSubmissionOptions {
108
133
  now?: () => number;
109
134
  /** Sleeps for `ms` milliseconds. Defaults to `setTimeout`. */
110
135
  sleep?: (ms: number) => Promise<void>;
136
+ /**
137
+ * Latest MINED block timestamp, in whole seconds — the only clock the on-chain
138
+ * dead-zone rule is expressed in. Supply this wherever a provider is available:
139
+ * the local-clock path below cannot see client clock skew, and the primary
140
+ * consumer of this gate is a browser wallet whose clock is not trustworthy.
141
+ *
142
+ * Resolve to `null` (or throw) to fall back to the local clock — a flaky RPC
143
+ * must never block a valid money-path operation.
144
+ */
145
+ chainNow?: () => Promise<number | null>;
111
146
  }
112
147
  export interface SafeSubmissionResult {
113
148
  /** Whether the send was deferred across the quantum boundary. */
@@ -136,6 +171,18 @@ export interface SafeSubmissionResult {
136
171
  * acceptance window (`QuantumOutsideWindow`). They must be handled by re-signing
137
172
  * upstream. In practice the SDK only ever signs CURRENT/NEXT.
138
173
  *
174
+ * Two implementations, same predicate:
175
+ *
176
+ * - **Chain-anchored** (`opts.chainNow` supplied — always prefer this): reasons in
177
+ * whole slots off the latest mined block timestamp `H`, the same clock the
178
+ * contract's `block.timestamp` comes from. A broadcast issued now can only be
179
+ * mined at `H + 12k`, so it is safe iff EVERY plausible `k` lands clear of the
180
+ * dead zone. No client clock is consulted at all — only sleep *durations*, which
181
+ * are immune to an offset clock.
182
+ * - **Local-clock fallback** (no `chainNow`, or the chain read failed): the original
183
+ * `Date.now()` math with the widened {@link INCLUSION_LATENCY_BUDGET}. Retained so
184
+ * a flaky RPC degrades rather than blocks, but it cannot see client clock skew.
185
+ *
139
186
  * @param quantumTimestamp The quantum timestamp embedded in the LIT-signed payload.
140
187
  * @returns whether it waited, and for how long.
141
188
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.323",
3
+ "version": "0.0.324",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol - Bitcoin-backed lending with LIT Protocol PKPs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -87,8 +87,8 @@
87
87
  },
88
88
  "sideEffects": false,
89
89
  "dependencies": {
90
- "@gvnrdao/dh-lit-actions": "^0.0.319",
91
- "@gvnrdao/dh-lit-ops": "^0.0.310",
90
+ "@gvnrdao/dh-lit-actions": "^0.0.320",
91
+ "@gvnrdao/dh-lit-ops": "^0.0.311",
92
92
  "@noble/hashes": "^1.5.0",
93
93
  "axios": "^1.17.0",
94
94
  "bech32": "^2.0.0",