@gvnrdao/dh-sdk 0.0.332 → 0.0.334

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.
@@ -85,6 +85,29 @@ export declare class WithdrawalAddressModule {
85
85
  * hard guarantee).
86
86
  */
87
87
  assertApprovedForWithdrawal(borrower: string, btcAddress: string): Promise<Result<void, SDKError>>;
88
+ /**
89
+ * Ask the node for the reason a failed write did not carry.
90
+ *
91
+ * A failing `eth_estimateGas` frequently answers with NOTHING to decode — ethers renders that
92
+ * as `missing revert data ... data=null, reason=null, revert=null`, which names no cause and
93
+ * cannot distinguish "the contract rejected this" from "the RPC hiccuped". An `eth_call` of the
94
+ * same calldata usually does carry the revert reason or custom-error selector, so this runs one
95
+ * and folds the answer into the reported error.
96
+ *
97
+ * Three deliberate properties:
98
+ *
99
+ * 1. It runs ONLY after the write has already failed. Gating the write on a static call would
100
+ * let a flaky endpoint — precisely the condition this exists to diagnose — block an add that
101
+ * would otherwise have succeeded.
102
+ * 2. It costs NO extra signature. `staticCall` is `eth_call`: a node read, never a wallet prompt.
103
+ * 3. It can never throw. A diagnostic that escapes would replace the real cause with itself.
104
+ * (The `try/catch` here is the authorized exception to CLAUDE.md's forbidden-patterns rule;
105
+ * it swallows nothing — every branch returns text that is reported alongside the original.)
106
+ *
107
+ * A static call that SUCCEEDS is itself a finding: the contract accepts this call, so the
108
+ * failure lay outside contract logic (RPC, gas, or nonce).
109
+ */
110
+ private staticCallDiagnostic;
88
111
  private txFailure;
89
112
  }
90
113
  export declare function createWithdrawalAddressModule(config: WithdrawalAddressModuleConfig): WithdrawalAddressModule;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * EIP-712 type definitions for borrower authorization messages — SDK copy.
3
+ *
4
+ * ## Why this is a copy, and why that is safe
5
+ *
6
+ * The single source of truth lives in `@gvnrdao/dh-lit-actions`
7
+ * (`lit-actions/src/constants/borrower-authorization-types.ts`), which the LIT
8
+ * Actions' dual-accept verifier reads. The SDK cannot import it yet: the
9
+ * published `dh-lit-actions@0.0.321` predates those constants, and bumping this
10
+ * package's dependency to an unpublished version makes `npm ci` fail with E404
11
+ * (proven 2026-08-27). So this module re-declares the four field lists — with a
12
+ * **pinned-vector parity suite**
13
+ * (`sdk/tests/shared/unit/borrower-authorization-712.test.ts`) asserting the
14
+ * domain separator, every typehash and every digest are byte-identical to the
15
+ * pins in `lit-actions/tests/unit/borrower-authorization-types.unit.test.ts`.
16
+ * Drift breaks a test suite before it can break a production signature — the
17
+ * same cross-package discipline as `recovery-auth-hash-parity`.
18
+ *
19
+ * **Fold this back into a re-export when `dh-lit-actions@>=0.0.322` publishes**
20
+ * (the deferred publish follow-up); the parity suite then becomes redundant
21
+ * protection rather than the only protection.
22
+ *
23
+ * ## What these are
24
+ *
25
+ * The wallet prompt fired BEFORE every loan transaction. Encoding it as typed
26
+ * data (instead of `personal_sign` over a precomputed keccak digest) is what
27
+ * lets MetaMask render `RepayRequest { positionId, paymentAmount,
28
+ * quantumTimestamp }` instead of an opaque 32-byte hex string. The ERC-7730
29
+ * descriptor (`clear-signing/pending/eip712-LoanOperationsManager.json`) keys
30
+ * its formats by these `encodeType` strings verbatim.
31
+ *
32
+ * No Solidity contract recomputes these digests: the borrower signature is
33
+ * consumed entirely inside the LIT Actions' `AuthorizationModule`. The
34
+ * `verifyingContract` (the LoanOperationsManager proxy) is truthful as the
35
+ * module that owns the operations, not as a `_hashTypedDataV4` call site.
36
+ */
37
+ import { type Signer, type TypedDataField } from 'ethers';
38
+ /** Shared with the LIT runtime — see `PROTOCOL_EIP712_DOMAIN_NAME` there. */
39
+ export declare const BORROWER_AUTHORIZATION_712_DOMAIN_NAME = "DiamondHands.Protocol";
40
+ export declare const BORROWER_AUTHORIZATION_712_DOMAIN_VERSION = "1";
41
+ export type BorrowerAuthorization712PrimaryType = 'MintRequest' | 'RepayRequest' | 'RenewRequest' | 'WithdrawRequest';
42
+ /**
43
+ * Field lists per primary type. **Order is part of the digest** — any change
44
+ * here must land in the same coordinated step as the lit-actions constants,
45
+ * the descriptor, and a CID rotation (EIP712_DOMAIN_SPEC.md §4.1).
46
+ */
47
+ export declare const BORROWER_AUTHORIZATION_712_FIELDS: Readonly<Record<BorrowerAuthorization712PrimaryType, readonly TypedDataField[]>>;
48
+ export interface BorrowerAuthorization712Domain {
49
+ readonly name: string;
50
+ readonly version: string;
51
+ readonly chainId: number;
52
+ readonly verifyingContract: string;
53
+ }
54
+ /**
55
+ * The `types` argument for `signer.signTypedData(domain, types, message)`.
56
+ * Returns ONLY the requested primary type — handing the signer all four would
57
+ * let a mis-shaped message encode as a different operation without complaint.
58
+ */
59
+ export declare function borrowerAuthorization712Types(primaryType: BorrowerAuthorization712PrimaryType): Record<string, TypedDataField[]>;
60
+ /**
61
+ * Build the EIP-712 domain for a chain.
62
+ *
63
+ * `verifyingContract` is the LoanOperationsManagerModule proxy from the SDK's
64
+ * own synced deployment constants (`scripts/sync-deployments.js` — the same
65
+ * generator that feeds the LIT Actions' pinned map, so the two sides of the
66
+ * digest cannot desync on a redeploy). Throws for a chain with no known LOM —
67
+ * there is no legitimate borrower authorization to build there.
68
+ */
69
+ export declare function buildBorrowerAuthorization712Domain(chainId: number): BorrowerAuthorization712Domain;
70
+ /** Compute the EIP-712 digest — the precomputed-signature path's hash. */
71
+ export declare function borrowerAuthorization712Digest(primaryType: BorrowerAuthorization712PrimaryType, chainId: number, message: Record<string, unknown>): string;
72
+ /**
73
+ * Sign a borrower authorization as typed data.
74
+ *
75
+ * Fails fast on a signer without `signTypedData` — no capability fallback to
76
+ * `personal_sign`: a signer that cannot produce typed data must go through an
77
+ * `authorizationProvider` (the Safe/agent seam), where the signature is
78
+ * produced by a party that can. A silent downgrade here would quietly
79
+ * reintroduce the opaque-hash prompt this whole change removes.
80
+ */
81
+ export declare function signBorrowerAuthorization712(signer: Signer, primaryType: BorrowerAuthorization712PrimaryType, chainId: number, message: Record<string, unknown>): Promise<string>;
@@ -24,9 +24,14 @@ export declare const MINT_UCD_GAS_CEILING: bigint;
24
24
  * gasLimit — no estimateGas runs on this path). The on-chain flow now spans PositionManager →
25
25
  * CollateralManager → LoanOperationsManager → BTCSpendAuthorizer plus the audit
26
26
  * #3 Chainlink feed-staleness gate and the BitcoinWithdrawalAddressRegistry
27
- * allowlist STATICCALL; observed end-to-end cost is ~1.0–1.3M gas, so the prior
28
- * hardcoded 1,000,000 limit caused a nested out-of-gas. 3M gives ~2x headroom
29
- * while staying well under the block gas limit.
27
+ * allowlist STATICCALL; observed end-to-end cost was ~1.0–1.3M gas when this
28
+ * was sized, so the prior hardcoded 1,000,000 limit caused a nested out-of-gas.
29
+ * A mainnet withdrawBTC measured 750,919 gas on 2026-09-04 (2 vault inputs,
30
+ * 1 prior reservation; tx 0xa244a184…) — recorded as a data point, NOT a new
31
+ * ceiling basis: cost grows with the number of open reservations
32
+ * (`_computeAuthorizedSpendsHash` is O(N)), so the ceiling stays sized to the
33
+ * observed MAX. 3M gives ~2x headroom over it while staying well under the
34
+ * block gas limit.
30
35
  */
31
36
  export declare const WITHDRAW_BTC_GAS_CEILING: bigint;
32
37
  /**
@@ -86,12 +91,60 @@ export declare const LIQUIDATION_COMMIT_GAS_CEILING: bigint;
86
91
  * provisional until re-measured against a real liquidation.
87
92
  */
88
93
  export declare const LIQUIDATION_REVEAL_GAS_CEILING: bigint;
89
- export declare function resolveEip1559FeeFields(provider: Provider): Promise<{
94
+ /**
95
+ * Priority-fee (tip) policy for every timing-sensitive broadcast in this file.
96
+ *
97
+ * History that sizes these numbers (docs/withdraw-btc-gas-cost-recommendations-2026-09-05.md):
98
+ * - 2026-07-02: a 0 gwei tip (the read RPC reported a real 0n
99
+ * eth_maxPriorityFeePerGas) sat unmined 119–143 s → QuantumOutsideWindow.
100
+ * - 2026-08-07: a 0.0007 gwei tip was skipped by one NEAR-EMPTY block, mined
101
+ * 24 s later → DeadZoneViolation (see quantum-timing.ts).
102
+ * - 2026-09-04: a mainnet withdrawBTC went out with ethers' hardcoded 1 gwei
103
+ * fallback tip (the wallet provider did not answer eth_maxPriorityFeePerGas)
104
+ * while the market p50 tip was ~0.03 gwei — 93% of a $1.99 tx was tip.
105
+ *
106
+ * So the tip is read from the market (eth_feeHistory on the app's READ RPC,
107
+ * never the wallet provider) at a percentile that buys next-block inclusion,
108
+ * then clamped: the floor keeps builders from skipping us (a quantum miss
109
+ * wastes the Lit signatures, the Chipotle spend and the reverted tx's base
110
+ * fee — far more than the tip), the cap stops a spiking feeHistory from
111
+ * overpaying. At ~751k gas the floor costs ≈ $0.18 (ETH $2,458).
112
+ */
113
+ export declare const PRIORITY_FEE_FLOOR_WEI: bigint;
114
+ export declare const PRIORITY_FEE_CAP_WEI: bigint;
115
+ /** Blocks of eth_feeHistory sampled; the median across them absorbs a single spike block. */
116
+ export declare const FEE_HISTORY_BLOCK_COUNT = 20;
117
+ /**
118
+ * Reward percentile per block. p75 (not p50) because these sends are
119
+ * quantum-bounded: paying above the median tip buys inclusion in the next
120
+ * slot or two, which is what the 60 s window actually needs.
121
+ */
122
+ export declare const FEE_HISTORY_REWARD_PERCENTILE = 75;
123
+ /** The subset of ethers' JsonRpcApiProvider these helpers need — raw JSON-RPC access. */
124
+ export interface JsonRpcSendable {
125
+ send(method: string, params: unknown[]): Promise<unknown>;
126
+ }
127
+ /**
128
+ * Resolve EIP-1559 fee fields from the app's READ RPC.
129
+ *
130
+ * `feeProvider` must be a JSON-RPC provider (ethers `JsonRpcProvider` or any
131
+ * object exposing `send`), NOT the wallet's `BrowserProvider`: wallet
132
+ * providers routinely fail `eth_maxPriorityFeePerGas`, and ethers'
133
+ * `getFeeData()` then silently substitutes a 1 gwei tip — the 2026-09-04
134
+ * overpayment. This helper never calls `getFeeData()`.
135
+ *
136
+ * One `eth_feeHistory` round-trip replaces the three RPC calls `getFeeData()`
137
+ * made (block, gasPrice, priorityFee), so it is also faster on the
138
+ * quantum-gated path that runs right before the send.
139
+ */
140
+ export declare function resolveEip1559FeeFields(feeProvider: Provider): Promise<{
90
141
  maxFeePerGas: bigint;
91
142
  maxPriorityFeePerGas: bigint;
92
143
  }>;
93
144
  export declare function sendEip1559Transaction(params: {
94
145
  signer: Signer;
146
+ /** The SDK READ provider (JSON-RPC). Fee fields come from here, never from `signer.provider`. */
147
+ feeProvider: Provider;
95
148
  to: string;
96
149
  data: string;
97
150
  gasLimit: bigint;
@@ -18,14 +18,18 @@ export interface ExtendOwnerAuthorization {
18
18
  callerAddress: string;
19
19
  }
20
20
  /**
21
- * Build the extend authorization message hash without signing.
21
+ * Build the extend authorization digest without signing.
22
22
  *
23
23
  * Use this when signing with a smart contract wallet (e.g. Safe) that
24
- * needs to wrap the hash in its own domain before signing.
24
+ * needs to wrap the digest in its own domain before signing.
25
+ *
26
+ * Since the EIP-712 cutover this is the TYPED-DATA digest
27
+ * (`RenewRequest(bytes32 positionId,uint256 selectedTerm,uint256
28
+ * quantumTimestamp)`) — same shape, different hash (plan step 18).
25
29
  *
26
30
  * @param positionId - Position identifier
27
31
  * @param newTerm - Extension term in months (number)
28
- * @param chainId - Chain ID for cross-chain replay protection
32
+ * @param chainId - Chain ID (selects the EIP-712 domain)
29
33
  * @returns { hash, timestamp } — pass these to Safe for signing
30
34
  */
31
35
  export declare function buildExtendAuthorizationHash(positionId: string, newTerm: number, chainId: number): {
@@ -35,19 +39,16 @@ export declare function buildExtendAuthorizationHash(positionId: string, newTerm
35
39
  /**
36
40
  * Generate extend position authorization signature
37
41
  *
38
- * Creates a signature that matches the format expected by
39
- * AuthorizationModule.verifyExtendAuthorization in lit-actions.
40
- *
41
- * Message structure:
42
- * solidityKeccak256(
43
- * ["bytes32", "uint256", "uint256", "uint256", "bytes32"],
44
- * [positionId, timestamp, chainId, newTerm, actionHash]
45
- * )
42
+ * Creates an EIP-712 signature the dual-accept
43
+ * AuthorizationModule.verifyExtendAuthorization verifies on its typed arm —
44
+ * rendered by the wallet as `RenewRequest { positionId, selectedTerm,
45
+ * quantumTimestamp }` under the DiamondHands.Protocol/LOM domain
46
+ * (see utils/borrower-authorization-712.ts).
46
47
  *
47
- * Where:
48
- * - actionHash = keccak256("extend-position")
49
- * - Signer address is recovered from signature by LIT Action
50
- * - LIT Action validates recovered address === position owner
48
+ * The WIRE OBJECT IS UNCHANGED (plan step 19): the transported fields keep
49
+ * their legacy names (`timestamp`, `newTerm`, `action: "extend-position"`) so
50
+ * every server hop and the validator's legacy arm keep working — only the
51
+ * signature's encoding differs, and the verifier tries both.
51
52
  *
52
53
  * @param positionId - Position identifier
53
54
  * @param newTerm - Extension term in months (number)
@@ -18,17 +18,23 @@ export interface MintOwnerAuthorization {
18
18
  signature: string;
19
19
  }
20
20
  /**
21
- * Build the mint authorization message hash without signing.
21
+ * Build the mint authorization digest without signing.
22
22
  *
23
23
  * Use this when signing with a smart contract wallet (e.g. Safe) that
24
- * needs to wrap the hash in its own domain before signing.
24
+ * needs to wrap the digest in its own domain before signing.
25
25
  *
26
- * Returns the raw hash AND the timestamp so both can be passed to
26
+ * Since the EIP-712 cutover this is the TYPED-DATA digest
27
+ * (`MintRequest(bytes32 positionId,uint256 mintAmount,uint256 quantumTimestamp)`
28
+ * under the DiamondHands.Protocol/LOM domain) — same shape, different hash
29
+ * (plan step 18). The LIT Action's dual-accept verifier resolves EIP-1271
30
+ * queries against this digest on the typed arm.
31
+ *
32
+ * Returns the digest AND the timestamp so both can be passed to
27
33
  * generateMintAuthorization via the pre-computed signature overload.
28
34
  *
29
35
  * @param positionId - Position identifier
30
36
  * @param amount - Amount to mint in wei (bigint)
31
- * @param chainId - Chain ID for cross-chain replay protection
37
+ * @param chainId - Chain ID (selects the EIP-712 domain)
32
38
  * @returns { hash, timestamp } — pass these to Safe for signing
33
39
  */
34
40
  export declare function buildMintAuthorizationHash(positionId: string, amount: bigint, chainId: number): {
@@ -38,19 +44,22 @@ export declare function buildMintAuthorizationHash(positionId: string, amount: b
38
44
  /**
39
45
  * Generate mint authorization signature
40
46
  *
41
- * Creates a signature that matches the format expected by
42
- * AuthorizationModule.verifyMintAuthorization in lit-actions.
47
+ * Creates an EIP-712 signature the dual-accept
48
+ * AuthorizationModule.verifyMintAuthorization in lit-actions verifies on its
49
+ * typed arm — and, unlike the legacy `personal_sign`-over-a-digest form, one
50
+ * the wallet can RENDER: MetaMask shows
51
+ * `MintRequest { positionId, mintAmount, quantumTimestamp }` instead of an
52
+ * opaque 32-byte hex string.
43
53
  *
44
- * Message structure:
45
- * solidityKeccak256(
46
- * ["bytes32", "uint256", "uint256", "uint256", "bytes32"],
47
- * [positionId, timestamp, chainId, amount, actionHash]
48
- * )
54
+ * Typed message (see utils/borrower-authorization-712.ts):
55
+ * MintRequest(bytes32 positionId,uint256 mintAmount,uint256 quantumTimestamp)
56
+ * under domain { name: "DiamondHands.Protocol", version: "1", chainId,
57
+ * verifyingContract: LoanOperationsManagerModule }.
49
58
  *
50
- * Where:
51
- * - actionHash = keccak256("mint-ucd")
52
- * - Signer address is recovered from signature by LIT Action
53
- * - LIT Action validates recovered address === position owner
59
+ * The WIRE OBJECT IS UNCHANGED (plan step 19): the transported fields
60
+ * (positionId, timestamp, chainId, amount, action) keep their names and
61
+ * values so the LIT Action's legacy arm and every server hop keep working —
62
+ * only the signature's encoding differs, and the verifier tries both.
54
63
  *
55
64
  * @param positionId - Position identifier
56
65
  * @param amount - Amount to mint in wei (bigint)
@@ -99,19 +108,14 @@ export interface BalanceConfirmationAuthorization {
99
108
  /**
100
109
  * Generate payment authorization signature
101
110
  *
102
- * Creates a signature that matches the format expected by
103
- * process-payment-validator LIT Action.
104
- *
105
- * Message structure:
106
- * solidityKeccak256(
107
- * ["bytes32", "uint256", "uint256", "uint256", "bytes32"],
108
- * [positionId, timestamp, chainId, amount, actionHash]
109
- * )
111
+ * Creates an EIP-712 signature the dual-accept process-payment-validator
112
+ * verifies on its typed arm — rendered by the wallet as
113
+ * `RepayRequest { positionId, paymentAmount, quantumTimestamp }` under the
114
+ * DiamondHands.Protocol/LOM domain (see utils/borrower-authorization-712.ts).
110
115
  *
111
- * Where:
112
- * - actionHash = keccak256("make-payment")
113
- * - Signer address is recovered from signature by LIT Action
114
- * - LIT Action validates recovered address === position owner
116
+ * The WIRE OBJECT IS UNCHANGED (plan step 19): fields keep their legacy names
117
+ * (`timestamp`, `amount`, `action: "make-payment"`) so every server hop and the
118
+ * validator's legacy arm keep working; only the signature's encoding differs.
115
119
  */
116
120
  export declare function generatePaymentAuthorization(positionId: string, amount: bigint, chainId: number, signerOrPrecomputed: Signer | {
117
121
  timestamp: number;
@@ -173,14 +177,19 @@ export interface WithdrawOwnerAuthorization {
173
177
  signature: string;
174
178
  }
175
179
  /**
176
- * Build the withdraw authorization message hash without signing.
180
+ * Build the withdraw authorization digest without signing.
177
181
  *
178
182
  * Use this when signing with a smart contract wallet (e.g. Safe) that
179
- * needs to wrap the hash in its own domain before signing.
183
+ * needs to wrap the digest in its own domain before signing.
184
+ *
185
+ * Since the EIP-712 cutover this is the TYPED-DATA digest
186
+ * (`WithdrawRequest(bytes32 positionId,uint256 totalDeduction,string
187
+ * withdrawalAddress,uint256 quantumTimestamp)`) — same shape, different hash
188
+ * (plan step 18).
180
189
  *
181
190
  * @param positionId - Position identifier
182
191
  * @param amount - Amount to withdraw in satoshis (bigint)
183
- * @param chainId - Chain ID for cross-chain replay protection
192
+ * @param chainId - Chain ID (selects the EIP-712 domain)
184
193
  * @param destinationAddress - Bitcoin destination address
185
194
  * @returns { hash, timestamp } — pass these to Safe for signing
186
195
  */
@@ -191,19 +200,17 @@ export declare function buildWithdrawAuthorizationHash(positionId: string, amoun
191
200
  /**
192
201
  * Generate withdrawal authorization signature
193
202
  *
194
- * Creates a signature that matches the format expected by
195
- * AuthorizationModule.verifyWithdrawAuthorization in lit-actions.
196
- *
197
- * Message structure:
198
- * solidityKeccak256(
199
- * ["bytes32", "uint256", "uint256", "uint256", "string", "bytes32"],
200
- * [positionId, timestamp, chainId, amount, destinationAddress, actionHash]
201
- * )
202
- *
203
- * Where:
204
- * - actionHash = keccak256("withdraw-btc")
205
- * - Signer address is recovered from signature by LIT Action
206
- * - LIT Action validates recovered address === position owner
203
+ * Creates an EIP-712 signature the dual-accept
204
+ * AuthorizationModule.verifyWithdrawAuthorization verifies on its typed arm —
205
+ * rendered by the wallet as `WithdrawRequest { positionId, totalDeduction,
206
+ * withdrawalAddress, quantumTimestamp }`, which puts the actual Bitcoin
207
+ * destination on the confirmation screen. A frontend that silently substitutes
208
+ * a different destination is contradicted by the user's own wallet.
209
+ *
210
+ * The WIRE OBJECT IS UNCHANGED (plan step 19): the transported fields keep
211
+ * their legacy names (`amount`, `action: "withdraw-btc"`; `destinationAddress`
212
+ * is re-attached by the caller as `withdrawalAddress`) — only the signature's
213
+ * encoding differs, and the verifier tries both.
207
214
  *
208
215
  * @param positionId - Position identifier
209
216
  * @param amount - Amount to withdraw in satoshis (bigint)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.332",
3
+ "version": "0.0.334",
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.321",
91
- "@gvnrdao/dh-lit-ops": "^0.0.314",
90
+ "@gvnrdao/dh-lit-actions": "^0.0.322",
91
+ "@gvnrdao/dh-lit-ops": "^0.0.315",
92
92
  "@noble/hashes": "^1.5.0",
93
93
  "axios": "^1.17.0",
94
94
  "bech32": "^2.0.0",