@gvnrdao/dh-sdk 0.0.320 → 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/browser/dist/397.browser.js +2 -0
- package/browser/dist/397.browser.js.LICENSE.txt +1 -0
- package/browser/dist/833.browser.js +2 -0
- package/browser/dist/833.browser.js.LICENSE.txt +1 -0
- package/browser/dist/browser.js +1 -1
- package/browser/dist/index.d.ts +8 -0
- package/browser/dist/index.d.ts.map +1 -0
- package/browser/dist/index.js +25 -0
- package/dist/constants/chunks/deployment-addresses.d.ts +2 -0
- package/dist/constants/chunks/network-configs.d.ts +2 -0
- package/dist/contracts/typechain-contracts/factories/src/agent/AgentDelegationRegistry__factory.d.ts +100 -1
- package/dist/contracts/typechain-contracts/src/agent/AgentDelegationRegistry.d.ts +101 -3
- package/dist/deployments.js +29 -3
- package/dist/deployments.mjs +29 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +951 -107
- package/dist/index.mjs +950 -107
- package/dist/interfaces/chunks/config.i.d.ts +2 -0
- package/dist/modules/diamond-hands-sdk.d.ts +225 -0
- package/dist/safe-delegation.d.ts +15 -0
- package/dist/safe-delegation.js +838 -0
- package/dist/safe-delegation.mjs +810 -0
- package/dist/utils/quantum-revert.utils.d.ts +21 -0
- package/dist/utils/quantum-submission.utils.d.ts +23 -3
- package/dist/utils/quantum-timing.d.ts +49 -2
- package/dist/utils/safe-agent-delegation.utils.d.ts +154 -0
- package/dist/utils/withdrawal-reconciliation.utils.d.ts +40 -14
- package/package.json +8 -3
|
@@ -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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
*/
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is this Safe delegated, and to which agent?" — the single implementation.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer that needs to know whether a multi-sig Safe can act through an
|
|
5
|
+
* AgentModule asks this: the frontend gate, the CLI, MCP. It was previously
|
|
6
|
+
* reimplemented per-consumer, and this session shows what that costs — the same
|
|
7
|
+
* invariant lived in two places twice, and both times only one copy was fixed
|
|
8
|
+
* when it turned out to be wrong.
|
|
9
|
+
*
|
|
10
|
+
* ── WHAT MAKES A DELEGATION REAL ───────────────────────────────────────────
|
|
11
|
+
* A module counts only when ALL of these hold:
|
|
12
|
+
* - it is enabled on the Safe (`getModulesPaginated`)
|
|
13
|
+
* - `safe()` is that Safe and `positionManager()` is this chain's
|
|
14
|
+
* - `AgentModuleFactory.isFromFactory(module)` claims it
|
|
15
|
+
*
|
|
16
|
+
* That last one is not decoration. The agent-module-executor Lit Action refuses
|
|
17
|
+
* at its second guard to sign through any module the factory disowns, so an
|
|
18
|
+
* unprovenanced module is one nothing can ever use. Measured on the Sepolia test
|
|
19
|
+
* Safe (2026-08-05): it carried a pre-factory module from an old e2e harness,
|
|
20
|
+
* correctly bound and pointing at an EOA agent nothing could sign with — and
|
|
21
|
+
* without the provenance check it read as a working delegation.
|
|
22
|
+
*
|
|
23
|
+
* ── WHY THE ANSWER IS TRI-STATE ────────────────────────────────────────────
|
|
24
|
+
* `unknown` is not a rounding error, it is the whole safety property. Callers
|
|
25
|
+
* act on "not-delegated" by BLOCKING loan creation, so a check that merely
|
|
26
|
+
* failed to run must never produce it. The distinctions below are load-bearing
|
|
27
|
+
* and were each learned from a live failure:
|
|
28
|
+
*
|
|
29
|
+
* - `safe()` unreadable → unknown. Every module here answers it, so a
|
|
30
|
+
* failure means we could not read, not that the
|
|
31
|
+
* module is wrong.
|
|
32
|
+
* - `positionManager()` absent → EVIDENCE. It is the discriminator (absent on
|
|
33
|
+
* VaultProvisionerModule), so a revert is a real
|
|
34
|
+
* answer: not an AgentModule.
|
|
35
|
+
* - provenance unreadable → unknown.
|
|
36
|
+
* - no factory on this chain → unknown, never "not-delegated". Mainnet has
|
|
37
|
+
* no AgentModuleFactory, and reporting that as
|
|
38
|
+
* not-delegated blocked vault creation behind a
|
|
39
|
+
* ceremony that cannot run there at all.
|
|
40
|
+
*
|
|
41
|
+
* ── NOT A SUBSTITUTE FOR THE LIT ACTION'S OWN CHECKS ───────────────────────
|
|
42
|
+
* The executor re-derives provenance and the registry binding itself. It must:
|
|
43
|
+
* it is the enforcement point and cannot trust anything a caller computed.
|
|
44
|
+
* Sharing this code with it would weaken it, so it stays independent.
|
|
45
|
+
*/
|
|
46
|
+
import { type Provider } from "ethers";
|
|
47
|
+
export type SafeDelegationStatus = "delegated" | "not-delegated" | "unknown";
|
|
48
|
+
export interface SafeAgentDelegation {
|
|
49
|
+
/** Lowercased Safe the answer describes. */
|
|
50
|
+
safeAddress: string;
|
|
51
|
+
/** Chain the answer describes — a module on one chain says nothing about another. */
|
|
52
|
+
chainId: number;
|
|
53
|
+
status: SafeDelegationStatus;
|
|
54
|
+
/** The AgentModule, or null when there isn't one (or we could not tell). */
|
|
55
|
+
moduleAddress: string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The agent EOA that SIGNS AND PAYS for delegated operations — read from the
|
|
58
|
+
* module, not the registry. They should agree, but the module's `agent()` is
|
|
59
|
+
* who `onlyAgent` actually admits, so it is the truth about who needs gas.
|
|
60
|
+
*/
|
|
61
|
+
agentAddress: string | null;
|
|
62
|
+
/** Registry expiry for the Safe's agent record, when readable. */
|
|
63
|
+
validUntil: number | null;
|
|
64
|
+
/** True when the Safe's module list exceeded one page — the answer may be partial. */
|
|
65
|
+
truncated: boolean;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the Safe's agent delegation from chain state.
|
|
69
|
+
*
|
|
70
|
+
* `provider` is the CALLER'S, deliberately: it must read the chain the caller is
|
|
71
|
+
* actually on. Resolving it internally would let an SDK configured for one chain
|
|
72
|
+
* answer about another, which is the exact confusion that made a live Sepolia
|
|
73
|
+
* delegation look absent while the wallet had silently defaulted to mainnet.
|
|
74
|
+
*
|
|
75
|
+
* Never throws — every failure resolves to `unknown`.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getSafeAgentDelegation(params: {
|
|
78
|
+
safeAddress: string;
|
|
79
|
+
chainId: number;
|
|
80
|
+
provider: Provider;
|
|
81
|
+
}): Promise<SafeAgentDelegation>;
|
|
82
|
+
/** One entry in a Safe transaction batch. */
|
|
83
|
+
export interface SafeDelegationCall {
|
|
84
|
+
to: string;
|
|
85
|
+
data: string;
|
|
86
|
+
value: string;
|
|
87
|
+
}
|
|
88
|
+
export interface SafeAgentDelegationDisablePlan {
|
|
89
|
+
/**
|
|
90
|
+
* The calls, in execution order. They must land ATOMICALLY — see below for
|
|
91
|
+
* why a half-applied revocation is worse than none.
|
|
92
|
+
*/
|
|
93
|
+
calls: SafeDelegationCall[];
|
|
94
|
+
/** The module being removed. */
|
|
95
|
+
moduleAddress: string;
|
|
96
|
+
/** The agent being revoked, when the module could name it. */
|
|
97
|
+
agentAddress: string | null;
|
|
98
|
+
/** The predecessor `disableModule` needs — SENTINEL when the module heads the list. */
|
|
99
|
+
prevModule: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Build the calls that undo a Safe's agent delegation.
|
|
103
|
+
*
|
|
104
|
+
* THE COUNTERPART TO {@link getSafeAgentDelegation}, and here rather than in any
|
|
105
|
+
* one consumer for the reason that reader is here: it needs the same module
|
|
106
|
+
* enumeration, and a second copy of that walk is how the two drift. The
|
|
107
|
+
* frontend, the CLI and MCP all revoke the same way or none of them can be
|
|
108
|
+
* trusted to.
|
|
109
|
+
*
|
|
110
|
+
* ── WHY TWO CALLS ──────────────────────────────────────────────────────────
|
|
111
|
+
* Neither alone is a full revocation:
|
|
112
|
+
*
|
|
113
|
+
* - `revokeAgent` flips the registry record to Revoked, which is what the Lit
|
|
114
|
+
* Action's `can*` gates read. After it, no mint/repay/renew/withdraw can be
|
|
115
|
+
* authorized for this agent.
|
|
116
|
+
* - `disableModule` removes the on-chain execution path, and it is the one
|
|
117
|
+
* that matters for the calls needing NO validator attestation: with the
|
|
118
|
+
* module still enabled, a live agent key can forward `approve` and
|
|
119
|
+
* `setPositionDelegate` through the Safe whatever the registry says
|
|
120
|
+
* (`AgentModule.execute` checks only `msg.sender == agent` and its own
|
|
121
|
+
* selector whitelist).
|
|
122
|
+
*
|
|
123
|
+
* Submit them as ONE batch. Revoking the record while leaving the module
|
|
124
|
+
* enabled reads as "not delegated" everywhere in the UI while those unattested
|
|
125
|
+
* paths stay open — the worst of both states.
|
|
126
|
+
*
|
|
127
|
+
* ── WHAT IT DOES NOT UNDO ──────────────────────────────────────────────────
|
|
128
|
+
* A `setPositionDelegate` the agent already installed on PositionDelegateRegistry
|
|
129
|
+
* SURVIVES both calls (vigil C-43). Clearing that is per-position and is not
|
|
130
|
+
* part of this plan.
|
|
131
|
+
*
|
|
132
|
+
* THROWS, unlike `getSafeAgentDelegation` — the asymmetry is deliberate. That
|
|
133
|
+
* one answers a question and "unknown" is a usable answer. This one produces a
|
|
134
|
+
* transaction the Safe's owners must gather to sign, so a guess costs them an
|
|
135
|
+
* approval round on something that must revert. Every failure is loud:
|
|
136
|
+
*
|
|
137
|
+
* - the chain has no AgentDelegationRegistry;
|
|
138
|
+
* - the Safe's module list cannot be read (never guess a predecessor: a wrong
|
|
139
|
+
* `prevModule` is Safe's GS103, AFTER the owners have signed);
|
|
140
|
+
* - the module is not enabled on the Safe (the caller is acting on a stale
|
|
141
|
+
* verdict).
|
|
142
|
+
*
|
|
143
|
+
* @param params.moduleAddress Optional. Supply the module from a verdict already
|
|
144
|
+
* on screen so the transaction describes what the user was looking at;
|
|
145
|
+
* omit it and the delegation is resolved here.
|
|
146
|
+
*/
|
|
147
|
+
export declare function buildSafeAgentDelegationDisableCalls(params: {
|
|
148
|
+
safeAddress: string;
|
|
149
|
+
chainId: number;
|
|
150
|
+
provider: Provider;
|
|
151
|
+
moduleAddress?: string;
|
|
152
|
+
/** bytes32 recorded against the revocation. Defaults to `bytes32("user-disabled")`. */
|
|
153
|
+
reason?: string;
|
|
154
|
+
}): Promise<SafeAgentDelegationDisablePlan>;
|
|
@@ -3,23 +3,36 @@
|
|
|
3
3
|
* (`BTCSpendAuthorizer.getAuthorizedSpends`) against BITCOIN truth before any
|
|
4
4
|
* UI offers "Execute" or any server invokes the TEE signer.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
6
|
+
* `satoshis` semantics — AGGREGATE, by design: the authorized `satoshis` is
|
|
7
|
+
* the TOTAL input value the consolidation will consume across the vault's
|
|
8
|
+
* confirmed UTXO set (btc-withdrawal.ts signs `vaultSatoshis = newCollateral +
|
|
9
|
+
* totalDeduction`), while `(txid, vout)` pins ONE representative outpoint.
|
|
10
|
+
* `BTCSpendAuthorizer.sol` enforces `targetAmount < satoshis`
|
|
11
|
+
* (TargetMustLeaveFeeRoom) — a withdrawal near the vault's full balance could
|
|
12
|
+
* never be authorized against a single small outpoint's value, so the
|
|
13
|
+
* aggregate is structurally required. NEVER compare `satoshis` to the
|
|
14
|
+
* representative outpoint's own value: for any vault holding more than one
|
|
15
|
+
* UTXO they legitimately differ (incident 2026-08-07, position 0x992d5c…, a
|
|
16
|
+
* 4-UTXO vault whose valid authorization was misclassified as corrupt).
|
|
17
|
+
*
|
|
18
|
+
* What IS invariant, and what this module verifies:
|
|
19
|
+
* - the funding tx exists and is confirmed (else UNFUNDED);
|
|
20
|
+
* - the representative `vout` exists on that tx (else CORRUPT);
|
|
21
|
+
* - the output at `vout` pays the VAULT address, when the caller supplies it
|
|
22
|
+
* (else CORRUPT — this is the real 2026-07-20 (vout, satoshis)-decoupling
|
|
23
|
+
* bug class: a vout pointing at an output the vault does not own);
|
|
24
|
+
* - `targetAmount < satoshis`, mirroring the contract invariant (else CORRUPT);
|
|
25
|
+
* - the outpoint's spend state (EXECUTED / SPENT_MISMATCH / EXECUTABLE).
|
|
13
26
|
*
|
|
14
27
|
* Statuses:
|
|
15
|
-
* - EXECUTABLE — outpoint confirmed, unspent,
|
|
28
|
+
* - EXECUTABLE — outpoint confirmed, unspent, record coherent → offer Execute.
|
|
16
29
|
* - EXECUTED — outpoint spent by a tx that pays the authorized target →
|
|
17
30
|
* auto-clear, show `spendingTxid` as the completion proof.
|
|
18
31
|
* - SPENT_MISMATCH — outpoint spent but the spending tx pays the target
|
|
19
32
|
* nothing → unexecutable; recoverStaleSpend clears it.
|
|
20
|
-
* - CORRUPT — authorization contradicts the chain (
|
|
21
|
-
*
|
|
22
|
-
*
|
|
33
|
+
* - CORRUPT — authorization contradicts the chain (vout out of range,
|
|
34
|
+
* output not owned by the vault, or targetAmount ≥
|
|
35
|
+
* satoshis) → operator recovery (admin clearing path);
|
|
23
36
|
* never Execute.
|
|
24
37
|
* - UNFUNDED — funding tx unknown/unconfirmed → wait; no Execute yet.
|
|
25
38
|
*
|
|
@@ -43,7 +56,11 @@ export interface ReconciledWithdrawal<T extends AuthorizedSpendLike = Authorized
|
|
|
43
56
|
status: PendingWithdrawalStatus;
|
|
44
57
|
/** Human-readable, single-sentence explanation of the classification. */
|
|
45
58
|
reason: string;
|
|
46
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Real value of the referenced outpoint, when the funding tx is known.
|
|
61
|
+
* Diagnostic only — for a multi-UTXO vault it is legitimately smaller than
|
|
62
|
+
* the aggregate `satoshis`.
|
|
63
|
+
*/
|
|
47
64
|
onChainOutputValue?: number;
|
|
48
65
|
/** The verified spending tx, for EXECUTED / SPENT_MISMATCH. */
|
|
49
66
|
spendingTxid?: string;
|
|
@@ -61,5 +78,14 @@ export type HttpGetJson = (url: string) => Promise<{
|
|
|
61
78
|
body: unknown;
|
|
62
79
|
}>;
|
|
63
80
|
export declare const defaultHttpGetJson: HttpGetJson;
|
|
64
|
-
/**
|
|
65
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Classify one authorized spend against the esplora at `esploraBaseUrl`.
|
|
83
|
+
*
|
|
84
|
+
* `opts.vaultAddress` enables the vault-ownership check (the strongest guard
|
|
85
|
+
* against the (vout, satoshis)-decoupling bug class). When absent the check is
|
|
86
|
+
* SKIPPED, not assumed — production callers (lit-ops-server, frontend) must
|
|
87
|
+
* supply it.
|
|
88
|
+
*/
|
|
89
|
+
export declare function reconcileAuthorizedSpend<T extends AuthorizedSpendLike>(spend: T, esploraBaseUrl: string, httpGetJson?: HttpGetJson, opts?: {
|
|
90
|
+
vaultAddress?: string;
|
|
91
|
+
}): Promise<ReconciledWithdrawal<T>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gvnrdao/dh-sdk",
|
|
3
|
-
"version": "0.0.
|
|
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",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"types": "./dist/deployments.d.ts",
|
|
30
30
|
"import": "./dist/deployments.mjs",
|
|
31
31
|
"require": "./dist/deployments.js"
|
|
32
|
+
},
|
|
33
|
+
"./safe-delegation": {
|
|
34
|
+
"types": "./dist/safe-delegation.d.ts",
|
|
35
|
+
"import": "./dist/safe-delegation.mjs",
|
|
36
|
+
"require": "./dist/safe-delegation.js"
|
|
32
37
|
}
|
|
33
38
|
},
|
|
34
39
|
"files": [
|
|
@@ -82,8 +87,8 @@
|
|
|
82
87
|
},
|
|
83
88
|
"sideEffects": false,
|
|
84
89
|
"dependencies": {
|
|
85
|
-
"@gvnrdao/dh-lit-actions": "^0.0.
|
|
86
|
-
"@gvnrdao/dh-lit-ops": "^0.0.
|
|
90
|
+
"@gvnrdao/dh-lit-actions": "^0.0.320",
|
|
91
|
+
"@gvnrdao/dh-lit-ops": "^0.0.311",
|
|
87
92
|
"@noble/hashes": "^1.5.0",
|
|
88
93
|
"axios": "^1.17.0",
|
|
89
94
|
"bech32": "^2.0.0",
|