@gvnrdao/dh-sdk 0.0.320 → 0.0.323

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.
@@ -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
- * Why this exists (incident 2026-07-22, position 0x992d5c…): the contract can
7
- * never know whether a Phase-2 BTC broadcast happened, and a pre-P6/#8
8
- * authorization could record a `(vout, satoshis)` pair that never matched the
9
- * chain (declared 96,049 vs on-chain 33,000). Executing such an entry can only
10
- * die at the signer's parent-fetch guard; and an entry whose outpoint was
11
- * already spent paying the target is DONE and must auto-clear — while an
12
- * entry whose funding tx merely confirmed must NOT be cleared as "complete".
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, values coherent → offer Execute.
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 (declared value ≠
21
- * real output value, targetAmount > real value, or vout
22
- * out of range) → operator recovery (admin clearing path);
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
- /** Real value of the referenced outpoint, when the funding tx is known. */
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
- /** Classify one authorized spend against the esplora at `esploraBaseUrl`. */
65
- export declare function reconcileAuthorizedSpend<T extends AuthorizedSpendLike>(spend: T, esploraBaseUrl: string, httpGetJson?: HttpGetJson): Promise<ReconciledWithdrawal<T>>;
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.320",
3
+ "version": "0.0.323",
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": [