@gvnrdao/dh-sdk 0.0.323 → 0.0.327

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.
@@ -2,6 +2,7 @@
2
2
  * Loan Operations Interfaces
3
3
  */
4
4
  import { LoanStatus } from "../../types/loanStatus";
5
+ import type { AuthorizationProvider } from "../../utils/authorization-provider.utils";
5
6
  export type BitcoinAddresses = {
6
7
  mainnet: string;
7
8
  testnet: string;
@@ -176,6 +177,18 @@ export interface UCDMintRequest {
176
177
  stubbedAvailableBtcSats?: string;
177
178
  stubbedAuthorizedSpendsHash?: string;
178
179
  };
180
+ /**
181
+ * Supplies the borrower authorization envelope instead of the connected
182
+ * signer — the seam a delegated Safe uses so its agent signs instantly
183
+ * rather than the SDK asking for an owner quorum inside a quantum window.
184
+ *
185
+ * Omit it and nothing changes: the SDK signs with the connected signer
186
+ * exactly as before, which is what every EOA borrower does.
187
+ *
188
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
189
+ * not a `{ timestamp, signature }` value.
190
+ */
191
+ authorizationProvider?: AuthorizationProvider;
179
192
  }
180
193
  /**
181
194
  * UCD Mint Result Interface
@@ -213,6 +226,18 @@ export interface RenewPositionResult {
213
226
  export interface RenewPositionRequest {
214
227
  positionId: string;
215
228
  selectedTerm: number;
229
+ /**
230
+ * Supplies the borrower authorization envelope instead of the connected
231
+ * signer — the seam a delegated Safe uses so its agent signs instantly
232
+ * rather than the SDK asking for an owner quorum inside a quantum window.
233
+ *
234
+ * Omit it and nothing changes: the SDK signs with the connected signer
235
+ * exactly as before, which is what every EOA borrower does.
236
+ *
237
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
238
+ * not a `{ timestamp, signature }` value.
239
+ */
240
+ authorizationProvider?: AuthorizationProvider;
216
241
  }
217
242
  /**
218
243
  * BTC Withdrawal Result Interface
@@ -282,6 +307,18 @@ export interface PartialPaymentRequest {
282
307
  positionId: string;
283
308
  paymentAmount: number;
284
309
  rpcUrl?: string;
310
+ /**
311
+ * Supplies the borrower authorization envelope instead of the connected
312
+ * signer — the seam a delegated Safe uses so its agent signs instantly
313
+ * rather than the SDK asking for an owner quorum inside a quantum window.
314
+ *
315
+ * Omit it and nothing changes: the SDK signs with the connected signer
316
+ * exactly as before, which is what every EOA borrower does.
317
+ *
318
+ * See `utils/authorization-provider.utils.ts` for why this is a callback and
319
+ * not a `{ timestamp, signature }` value.
320
+ */
321
+ authorizationProvider?: AuthorizationProvider;
285
322
  }
286
323
  /**
287
324
  * Confirm Balance Result Interface
@@ -18,6 +18,7 @@ import { SDKError } from "../utils/error-handler";
18
18
  import type { CreateLoanRequest, CreateLoanResult, LoanDataDetail, UCDMintRequest, UCDMintResult, PartialPaymentRequest, PartialPaymentResult, BTCWithdrawalResult, RenewPositionRequest, RenewPositionResult, LiquidationRequest, LiquidationResult, ConfirmBalanceRequest, ConfirmBalanceResult, TermsWithFeesResult } from "../interfaces/chunks/loan-operations.i";
19
19
  import type { DiamondHandsSDKConfig } from "../interfaces/chunks/config.i";
20
20
  import type { PKPData } from "../interfaces/chunks/pkp-integration.i";
21
+ import { type AuthorizationProvider } from "../utils/authorization-provider.utils";
21
22
  import type { DhServerLoginPayload } from "../utils/eip712-login";
22
23
  import { type ReconciledWithdrawal } from "../utils/withdrawal-reconciliation.utils";
23
24
  import { ContractManager } from "./contract/contract-manager.module";
@@ -540,6 +541,24 @@ export declare class DiamondHandsSDK {
540
541
  * address strings, query the subgraph `WithdrawalAddressBook`.
541
542
  */
542
543
  getApprovedWithdrawalAddresses(user?: string): Promise<Result<import("./withdrawal-address/withdrawal-address.module").WithdrawalAddressEntry[], SDKError>>;
544
+ /**
545
+ * Recover the revert reason of an already-mined, already-reverted transaction.
546
+ *
547
+ * A status-0 receipt carries no revert data, and ethers does not re-fetch it, so
548
+ * the reason has to be recovered by replaying the exact calldata as an `eth_call`
549
+ * pinned to the block that mined it — the only block whose `block.timestamp`
550
+ * reproduces a timing-dependent revert such as `DeadZoneViolation()`.
551
+ *
552
+ * Caveat: the replay runs against end-of-block state rather than the state at the
553
+ * transaction's own index, so for a state-dependent revert the recovered name is
554
+ * diagnostic, not proof. Timing reverts (the case this exists for) are unaffected —
555
+ * they depend only on the block timestamp.
556
+ *
557
+ * @returns the decoded error name, its raw selector if unknown, or `null` when the
558
+ * replay yields no revert data at all (never throws — a failed diagnosis
559
+ * must not mask the underlying failure).
560
+ */
561
+ private decodeMinedRevert;
543
562
  /**
544
563
  * Withdraw Bitcoin from a position
545
564
  *
@@ -563,7 +582,15 @@ export declare class DiamondHandsSDK {
563
582
  * @param customBitcoinRpcUrl - Optional custom Bitcoin RPC URL (for local testing)
564
583
  * @returns Withdrawal result with transaction details
565
584
  */
566
- withdrawBTC(positionId: string, withdrawalAddress: string, withdrawalAmount: number): Promise<BTCWithdrawalResult>;
585
+ withdrawBTC(positionId: string, withdrawalAddress: string, withdrawalAmount: number,
586
+ /**
587
+ * An options bag rather than a fifth positional argument: this method is
588
+ * already three positional strings/numbers deep, and the next caller to
589
+ * add one would have to guess the order.
590
+ */
591
+ options?: {
592
+ authorizationProvider?: AuthorizationProvider;
593
+ }): Promise<BTCWithdrawalResult>;
567
594
  /**
568
595
  * TEMPORARY (multi-UTXO withdrawal fix, Phase A): fetch the vault's confirmed
569
596
  * UTXO set from lit-ops-server (`GET /api/lit/vault-utxos`) so the caller can
@@ -721,6 +748,11 @@ export declare class DiamondHandsSDK {
721
748
  * (`MAX_ATTESTATION_AGE = 1 hour`) and replay-protects via
722
749
  * `authorizedAt` binding.
723
750
  *
751
+ * WHO PAYS: in **service mode** lit-ops-server performs BOTH steps and its
752
+ * relayer funds the transaction — the borrower is never prompted, because the
753
+ * Bitcoin withdrawal this cleans up after has already settled. In **direct
754
+ * mode** (CLI / tests / hardhat) the SDK's own signer submits step 2.
755
+ *
724
756
  * `invalidatorTxid` is either:
725
757
  * - the borrower's own authorized broadcast (classified "consumed-as-authorized"
726
758
  * by the LIT validator), once it has ≥6 confirmations on the BTC
@@ -1053,6 +1085,16 @@ export declare class DiamondHandsSDK {
1053
1085
  agentActive: boolean | null;
1054
1086
  /** Agent expiry (unix seconds) — the ONLY time bound on every grant. Null without `user`. */
1055
1087
  agentValidUntil: number | null;
1088
+ /** SCOPE_WITHDRAW granted. Always false where the registry predates v2.1.0. */
1089
+ withdrawEnabled: boolean;
1090
+ /** Does the DEPLOYED registry honour SCOPE_WITHDRAW at all (v2.1.0+)? */
1091
+ withdrawScopeSupported: boolean;
1092
+ /**
1093
+ * Grant's post-withdrawal collateral floor in bps; 0 when WITHDRAW is not granted.
1094
+ * INERT AT ZERO DEBT — a ratio cannot bound a withdrawal once debt is 0. Never present this
1095
+ * as a cap on how much an agent may withdraw.
1096
+ */
1097
+ minWithdrawRatioBps: number;
1056
1098
  }>;
1057
1099
  /** Governance floor-of-floors for agent mints (bps). The UI's minimum for the floor editor. */
1058
1100
  getAgentMintFloorMinBps(): Promise<number>;
@@ -1143,7 +1185,56 @@ export declare class DiamondHandsSDK {
1143
1185
  hash: string;
1144
1186
  blockNumber: number;
1145
1187
  }>;
1146
- /** Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on. */
1188
+ /**
1189
+ * Grant the agent the WITHDRAW scope for a position (registry v2.1.0+). Borrower-signed.
1190
+ *
1191
+ * CONTRACT WALLETS ONLY, and this is enforced here rather than on-chain. The withdrawal
1192
+ * validator's only non-borrower arm is `checkSafeAgentWithdrawAuthorization`, which requires a
1193
+ * factory-provenanced `AgentModule` whose `safe()` is the borrower — something an EOA cannot
1194
+ * have. The registry deliberately does NOT check wallet type ("Callable by any borrower (this
1195
+ * function does not itself require a Safe)"), so an EOA can record a grant that no code path
1196
+ * will ever satisfy. Refusing here keeps the supported path from writing a dead record.
1197
+ *
1198
+ * That refusal is ergonomics, NOT enforcement — `enableAutoWithdraw` is `external` and reachable
1199
+ * from any wallet. The real reason delegated withdrawal is closed to EOAs is that they can sign
1200
+ * inside the ~44s window themselves, so delegating buys them nothing and only adds risk.
1201
+ *
1202
+ * @param minWithdrawRatioBps Post-withdrawal collateral-ratio floor for AGENT withdrawals.
1203
+ * Clamped by the same governance minimum as the mint floor. **Inert at zero debt** — it
1204
+ * is a ratio, and the post-withdrawal ratio is infinite for any amount once debt is 0.
1205
+ * The operative bound there is the quorum-gated destination allowlist. Do not surface
1206
+ * this to users as a cap on how much an agent may withdraw.
1207
+ * There is no in-place raise: changing it means disable-then-re-enable, deliberately —
1208
+ * on the one scope that moves custodied BTC, incident response can only disable outright.
1209
+ */
1210
+ enableAutoWithdraw(positionId: string, minWithdrawRatioBps: number, options?: {
1211
+ agentValiditySeconds?: number;
1212
+ }): Promise<{
1213
+ hash: string;
1214
+ blockNumber: number;
1215
+ agentAddress?: string;
1216
+ }>;
1217
+ /**
1218
+ * Revoke the WITHDRAW scope for a position (also zeroes its withdraw floor). Borrower-signed.
1219
+ *
1220
+ * No wallet-type check: revoking is the safe direction, and an EOA that recorded a grant before
1221
+ * this guard existed must be able to clear it.
1222
+ */
1223
+ disableAutoWithdraw(positionId: string): Promise<{
1224
+ hash: string;
1225
+ blockNumber: number;
1226
+ }>;
1227
+ /**
1228
+ * Layer 2 of the withdrawal-delegation gating: refuse to write a grant that could never be
1229
+ * satisfied. See `enableAutoWithdraw` for why this lives here and not in the registry.
1230
+ */
1231
+ private assertBorrowerIsContractWallet;
1232
+ /**
1233
+ * Pre-flight the registry's `FloorBelowProtocolMin` guard with a message a user can act on.
1234
+ *
1235
+ * `agentMintFloorMinBps` is the governance floor-of-floors for BOTH the mint and withdraw
1236
+ * grants — one lever, one invariant — so `scope` only shapes the message.
1237
+ */
1147
1238
  private assertMintFloorAtOrAboveProtocolMin;
1148
1239
  /**
1149
1240
  * Get Bitcoin balance for an address
@@ -326,7 +326,7 @@ var SEPOLIA_DEPLOYMENT = {
326
326
  CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
327
327
  CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01",
328
328
  AGENT_DELEGATION_REGISTRY: "0x6AE7fc6bE0925126b9D25b1B7Ce5AD5d127ec025",
329
- AGENT_DELEGATION_REGISTRY_IMPL: "0x80C47350274513688e348D02506477A96B4afCCb",
329
+ AGENT_DELEGATION_REGISTRY_IMPL: "0xE931E4b9ceF2A48Ac7D57192da4B523756cFC310",
330
330
  DH_AGENT_DELEGATE: "0xa5A14489ae883960df6E1463A41a47aBE9Ecc5AF"
331
331
  }
332
332
  };
@@ -538,7 +538,7 @@ var MAINNET_DEPLOYMENT = {
538
538
  var LOCALHOST_DEPLOYMENT = {
539
539
  network: "localhost",
540
540
  chainId: 1337,
541
- timestamp: "2026-07-23T17:13:42.777Z",
541
+ timestamp: "2026-07-20T19:45:58.514Z",
542
542
  deployer: "",
543
543
  contracts: {
544
544
  MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
@@ -558,30 +558,6 @@ var LOCALHOST_DEPLOYMENT = {
558
558
  PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
559
559
  OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
560
560
  PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
561
- },
562
- latestEnv: {
563
- UCD_TOKEN: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
564
- UCD_CONTROLLER: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
565
- POSITION_MANAGER: "0x851356ae760d987E095750cCeb3bC6014560891C",
566
- PKP_VALIDATION_REGISTRY: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
567
- PKP_VALIDATION_CID_V1: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
568
- PKP_VALIDATION_CID_V3: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
569
- PKP_ETH_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
570
- OPERATION_AUTHORIZATION_REGISTRY: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
571
- UCD_MINT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
572
- UCD_MINT_VALIDATOR_VERSION: 1,
573
- BTC_WITHDRAWAL_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
574
- BTC_WITHDRAWAL_VALIDATOR_VERSION: 3,
575
- UPDATE_BALANCE_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
576
- UPDATE_BALANCE_VALIDATOR_VERSION: 1,
577
- PROCESS_PAYMENT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
578
- PROCESS_PAYMENT_VALIDATOR_VERSION: 1,
579
- EXTEND_POSITION_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
580
- EXTEND_POSITION_VALIDATOR_VERSION: 1,
581
- POSITION_MANAGER_CORE_MODULE: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
582
- LOAN_OPERATIONS_MANAGER_MODULE: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
583
- TERM_MANAGER_MODULE: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
584
- COMMUNITY_MANAGER_MODULE: ""
585
561
  }
586
562
  };
587
563
  var ALL_DEPLOYMENTS = {
@@ -299,7 +299,7 @@ var SEPOLIA_DEPLOYMENT = {
299
299
  CARRY_AGENT_REGISTRY: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
300
300
  CARRY_AGENT_REGISTRY_IMPL: "0x1b11a3D5e46F3672E643c997f0d569DFf0b64a01",
301
301
  AGENT_DELEGATION_REGISTRY: "0x6AE7fc6bE0925126b9D25b1B7Ce5AD5d127ec025",
302
- AGENT_DELEGATION_REGISTRY_IMPL: "0x80C47350274513688e348D02506477A96B4afCCb",
302
+ AGENT_DELEGATION_REGISTRY_IMPL: "0xE931E4b9ceF2A48Ac7D57192da4B523756cFC310",
303
303
  DH_AGENT_DELEGATE: "0xa5A14489ae883960df6E1463A41a47aBE9Ecc5AF"
304
304
  }
305
305
  };
@@ -511,7 +511,7 @@ var MAINNET_DEPLOYMENT = {
511
511
  var LOCALHOST_DEPLOYMENT = {
512
512
  network: "localhost",
513
513
  chainId: 1337,
514
- timestamp: "2026-07-23T17:13:42.777Z",
514
+ timestamp: "2026-07-20T19:45:58.514Z",
515
515
  deployer: "",
516
516
  contracts: {
517
517
  MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
@@ -531,30 +531,6 @@ var LOCALHOST_DEPLOYMENT = {
531
531
  PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
532
532
  OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
533
533
  PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
534
- },
535
- latestEnv: {
536
- UCD_TOKEN: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
537
- UCD_CONTROLLER: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
538
- POSITION_MANAGER: "0x851356ae760d987E095750cCeb3bC6014560891C",
539
- PKP_VALIDATION_REGISTRY: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A",
540
- PKP_VALIDATION_CID_V1: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
541
- PKP_VALIDATION_CID_V3: "0x12203af2054b0ab6e18d898ddfc7985593439fb82d22336133387c1169f98d3702ab",
542
- PKP_ETH_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
543
- OPERATION_AUTHORIZATION_REGISTRY: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
544
- UCD_MINT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
545
- UCD_MINT_VALIDATOR_VERSION: 1,
546
- BTC_WITHDRAWAL_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
547
- BTC_WITHDRAWAL_VALIDATOR_VERSION: 3,
548
- UPDATE_BALANCE_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
549
- UPDATE_BALANCE_VALIDATOR_VERSION: 1,
550
- PROCESS_PAYMENT_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
551
- PROCESS_PAYMENT_VALIDATOR_VERSION: 1,
552
- EXTEND_POSITION_VALIDATOR_ADDRESS: "0x406f6a9855de2f5577b1525f0600461f02859bd0",
553
- EXTEND_POSITION_VALIDATOR_VERSION: 1,
554
- POSITION_MANAGER_CORE_MODULE: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
555
- LOAN_OPERATIONS_MANAGER_MODULE: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
556
- TERM_MANAGER_MODULE: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
557
- COMMUNITY_MANAGER_MODULE: ""
558
534
  }
559
535
  };
560
536
  var ALL_DEPLOYMENTS = {
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Borrower authorization, supplied by something other than the connected signer.
3
+ *
4
+ * WHY THIS EXISTS. Every loan operation carries an envelope the validator
5
+ * recovers a signer from, and the SDK builds it by calling
6
+ * `signer.signMessage()`. For an EOA that is one instant prompt. For a k-of-n
7
+ * Safe it is an EIP-1271 signature — a full owner quorum — and the operation
8
+ * also needs a second quorum to send the transaction, both inside a quantum
9
+ * window of 180s (~44s for withdrawals). That cannot be gathered, so a genuine
10
+ * multi-owner Safe could not complete a loan operation at all.
11
+ *
12
+ * The fix is for something that CAN sign instantly to sign the envelope: the
13
+ * Safe's delegated agent. This is the seam that lets a caller supply it.
14
+ *
15
+ * A PROVIDER, NOT A VALUE. The obvious API — hand in `{ timestamp, signature }`
16
+ * — is wrong, and the reason is the retry loops. Every operation re-signs on
17
+ * retry precisely because a quantum window can lapse between building the
18
+ * envelope and landing the transaction. A fixed value would be replayed on
19
+ * attempt 2 carrying attempt 1's timestamp, so the envelope would already be
20
+ * dead: a recoverable timing miss turned into a hard failure. A provider is
21
+ * called again per attempt and returns a fresh signature over a fresh window.
22
+ *
23
+ * THE SDK OWNS THE TIMESTAMP. It computes the quantum-aligned value and hands
24
+ * it to the provider, rather than accepting one back unseen, because the same
25
+ * value must be embedded in the protocol call that follows. Likewise `amount`
26
+ * is the SDK's own computed wei — a caller re-deriving it and differing by one
27
+ * wei produces a signature that recovers to a different address, which surfaces
28
+ * as an opaque authorization failure with nothing to point at.
29
+ */
30
+ /** Everything the authorization envelope binds, as the SDK computed it. */
31
+ export interface AuthorizationContext {
32
+ positionId: string;
33
+ /** Quantum-aligned, and the value the protocol call will carry. */
34
+ timestamp: number;
35
+ /** Wei, as a decimal string. The SDK's own value — never re-derive it. */
36
+ amount: string;
37
+ /** Renewals only: the term the envelope binds instead of an amount. */
38
+ selectedTerm?: number;
39
+ /** Withdrawals only: the destination the envelope binds. */
40
+ destinationAddress?: string;
41
+ }
42
+ /**
43
+ * Returns a signature over the envelope described by `ctx`. The returned
44
+ * `timestamp` is echoed back rather than assumed so a provider that had to
45
+ * re-align it can say so — but it SHOULD be the one it was given.
46
+ */
47
+ export type AuthorizationProvider = (ctx: AuthorizationContext) => Promise<{
48
+ timestamp: number;
49
+ signature: string;
50
+ }>;
51
+ /**
52
+ * Resolve what the `generate*Authorization` helpers should be handed: either a
53
+ * precomputed `{ timestamp, signature }` from the provider, or the live signer
54
+ * exactly as before.
55
+ *
56
+ * Call this INSIDE a retry loop, never above one — that is the whole point.
57
+ *
58
+ * @param nextQuantumTimestamp The SDK's quantum-aligned timestamp for THIS
59
+ * attempt. Passed in rather than computed here so the caller keeps a
60
+ * single source for the value it will also send on-chain.
61
+ */
62
+ export declare function resolveAuthorizationInput<TSigner>(provider: AuthorizationProvider | undefined, signer: TSigner, ctx: AuthorizationContext): Promise<TSigner | {
63
+ timestamp: number;
64
+ signature: string;
65
+ }>;
@@ -113,25 +113,19 @@ export interface BalanceConfirmationAuthorization {
113
113
  * - Signer address is recovered from signature by LIT Action
114
114
  * - LIT Action validates recovered address === position owner
115
115
  */
116
- export declare function generatePaymentAuthorization(positionId: string, amount: bigint, chainId: number, signer: Signer): Promise<PaymentOwnerAuthorization>;
116
+ export declare function generatePaymentAuthorization(positionId: string, amount: bigint, chainId: number, signerOrPrecomputed: Signer | {
117
+ timestamp: number;
118
+ signature: string;
119
+ }): Promise<PaymentOwnerAuthorization>;
117
120
  /**
118
- * Generate extend position authorization signature
119
- *
120
- * Creates a signature that matches the format expected by
121
- * extend-position-validator LIT Action.
122
- *
123
- * Message structure:
124
- * solidityKeccak256(
125
- * ["bytes32", "uint256", "uint256", "uint256", "bytes32"],
126
- * [positionId, timestamp, chainId, selectedTerm, actionHash]
127
- * )
121
+ * NOTE: `generateExtendAuthorization` does NOT live here.
128
122
  *
129
- * Where:
130
- * - actionHash = keccak256("extend-position")
131
- * - Signer address is recovered from signature by LIT Action
132
- * - LIT Action validates recovered address === position owner
123
+ * It used to — a second copy, without the precomputed-signature union, whose
124
+ * import in `diamond-hands-sdk.ts` was commented out ("Not used yet"). The live
125
+ * one is `utils/extend-authorization.utils.ts`, and that is the only one. The
126
+ * duplicate is removed rather than left dormant because an edit landing in the
127
+ * wrong copy is a silent no-op: the code looks changed, the behaviour does not.
133
128
  */
134
- export declare function generateExtendAuthorization(positionId: string, selectedTerm: number, chainId: number, signer: Signer): Promise<ExtendOwnerAuthorization>;
135
129
  /**
136
130
  * Optional Chipotle service fallback for `getPKPPublicKeyFromTokenId`.
137
131
  *
@@ -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.327",
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.312",
92
92
  "@noble/hashes": "^1.5.0",
93
93
  "axios": "^1.17.0",
94
94
  "bech32": "^2.0.0",