@gvnrdao/dh-sdk 0.0.337 → 0.0.338

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.
@@ -26,7 +26,7 @@
26
26
  * data (instead of `personal_sign` over a precomputed keccak digest) is what
27
27
  * lets MetaMask render `RepayRequest { positionId, paymentAmount,
28
28
  * quantumTimestamp }` instead of an opaque 32-byte hex string. The ERC-7730
29
- * descriptor (`clear-signing/pending/eip712-LoanOperationsManager.json`) keys
29
+ * descriptor (`clear-signing/registry/diamond-hands/eip712-LoanOperationsManager.json`) keys
30
30
  * its formats by these `encodeType` strings verbatim.
31
31
  *
32
32
  * No Solidity contract recomputes these digests: the borrower signature is
@@ -10,6 +10,15 @@ export interface DhServerLoginMessage {
10
10
  nonce: string;
11
11
  /** Audit M-8: service endpoint URL this login targets (replay-binds to one service). */
12
12
  audience?: string;
13
+ /**
14
+ * Human-readable sentence the wallet renders on the signing prompt instead of four
15
+ * opaque machine fields. Opt-in — see `buildSignedLoginPayload`'s `withStatement`.
16
+ *
17
+ * NOT free text: the servers rebuild the expected value from the allowlisted
18
+ * `audience` and reject anything else, so it must come from
19
+ * `canonicalLoginStatement`.
20
+ */
21
+ statement?: string;
13
22
  }
14
23
  export interface DhServerLoginPayload {
15
24
  chainId: number;
@@ -24,6 +33,20 @@ export declare function buildLoginDomain(chainId: number): {
24
33
  export declare const LOGIN_TYPES: Record<string, TypedDataField[]>;
25
34
  /** Audit M-8: login types with the audience binding. */
26
35
  export declare const LOGIN_TYPES_WITH_AUDIENCE: Record<string, TypedDataField[]>;
36
+ /**
37
+ * Login types carrying the human-readable `statement` as well.
38
+ *
39
+ * `statement` is FIRST because signing UIs render struct members in declaration order
40
+ * and the prose is what the user actually reads. The position is hashed into the
41
+ * typeHash, so this order is consensus with both servers, not formatting.
42
+ */
43
+ export declare const LOGIN_TYPES_WITH_STATEMENT: Record<string, TypedDataField[]>;
44
+ /**
45
+ * The statement to sign alongside `audience`, or null when the audience has no host
46
+ * (in which case the caller must sign the shape without a statement — a statement the
47
+ * server cannot rebuild is a signature it will reject).
48
+ */
49
+ export declare function canonicalLoginStatement(audience: string): string | null;
27
50
  /**
28
51
  * Sign a fresh login envelope. Caller POSTs the returned `payload` to the
29
52
  * server's `/api/auth/login` route.
@@ -43,4 +66,14 @@ export declare function buildSignedLoginPayload(signer: Signer, chainId: number,
43
66
  reuse?: {
44
67
  issuedAt: number;
45
68
  nonce: string;
46
- }): Promise<DhServerLoginPayload>;
69
+ },
70
+ /**
71
+ * Add the human-readable `statement` the wallet renders on the prompt. OFF by default:
72
+ * it changes the digest, so a client that opts in requires servers that already accept
73
+ * the statement-bearing shape. The headless consumers (CLI, MCP, dh-cr-monitor) have no
74
+ * signing UI to improve and should leave it off; a browser passes true.
75
+ *
76
+ * Ignored without an `audience` — the servers derive the expected text from the
77
+ * audience host, so a statement they cannot rebuild is a signature they will reject.
78
+ */
79
+ withStatement?: boolean): Promise<DhServerLoginPayload>;
@@ -71,6 +71,21 @@ export declare function sessionStoreKey(address: string, chainId: number, endpoi
71
71
  export interface PendingLoginEnvelope {
72
72
  issuedAt: number;
73
73
  nonce: string;
74
+ /**
75
+ * Whether the envelope this record stands for was signed WITH the human-readable
76
+ * statement.
77
+ *
78
+ * Part of the record because it is part of the DIGEST, not incidental metadata.
79
+ * A Safe identifies a queued message purely by its EIP-712 hash, and the presence
80
+ * of `statement` selects the field set — so replaying this nonce under the other
81
+ * shape produces a DIFFERENT hash, which Safe shows as a new message needing its
82
+ * FIRST signature. Every owner signature already collected is orphaned, and a
83
+ * 2-of-N can never reach threshold.
84
+ *
85
+ * Absent on records written before statements existed, which reads as `false` —
86
+ * correct, because that is exactly what those envelopes were signed as.
87
+ */
88
+ withStatement?: boolean;
74
89
  }
75
90
  /**
76
91
  * How long an unsigned envelope stays reusable FOR A CONTRACT WALLET. Must stay
@@ -45,6 +45,17 @@ export interface ServerSessionOptions {
45
45
  * (CLI, MCP, cr-monitor) omit it and keep the endpoint binding unchanged.
46
46
  */
47
47
  loginAudience?: string;
48
+ /**
49
+ * Add the human-readable `statement` to the login envelope — the sentence a
50
+ * wallet renders instead of four opaque fields.
51
+ *
52
+ * OFF by default. `statement` changes the EIP-712 digest and the server picks
53
+ * its field set by presence alone, so sending one to a server that predates it
54
+ * fails EVERY login. Only a caller that knows the server accepts statements
55
+ * (a browser reading the capability flag) should turn this on. Ignored without
56
+ * `loginAudience`, since the expected text is derived from the audience host.
57
+ */
58
+ withStatement?: boolean;
48
59
  /**
49
60
  * Fires immediately before a wallet signature is requested — i.e. only when
50
61
  * neither the cached JWT nor the persisted envelope could renew the session,
@@ -86,6 +97,8 @@ export declare class ServerSession {
86
97
  private readonly signer;
87
98
  private readonly serviceEndpoint;
88
99
  private readonly loginAudience;
100
+ /** Whether this session's login carries the human-readable statement. */
101
+ private readonly withStatement;
89
102
  private readonly chainId;
90
103
  private readonly store;
91
104
  private readonly onSignaturePrompt?;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The one error the sign-guard raises when it refuses to let bytes reach a key.
3
+ *
4
+ * Exported from exactly one SDK entry (`@gvnrdao/dh-sdk/sign-guard`) so
5
+ * `instanceof` checks in consumers cannot split across two module realms.
6
+ */
7
+ export declare class TxValidationError extends Error {
8
+ constructor(msg: string);
9
+ }
@@ -0,0 +1,15 @@
1
+ import { ethers } from "ethers";
2
+ import type { ContractRegistry } from "./tx-validator";
3
+ export interface ExtendFeeBound {
4
+ ucdDebt: bigint;
5
+ extensionFeeRateBps: bigint;
6
+ upperBoundFeeWei: bigint;
7
+ }
8
+ /**
9
+ * Best-effort: returns `null` if a required address is missing or a read
10
+ * fails, and reports WHY through `onDegraded`. Callers should surface the
11
+ * degrade and sign with the on-chain validator signature as the only fee
12
+ * binder rather than block the user — a borrower who cannot renew risks
13
+ * liquidation.
14
+ */
15
+ export declare function computeExtendFeeUpperBound(provider: ethers.Provider, contracts: ContractRegistry, positionId: string, selectedTerm: number, onDegraded?: (reason: string) => void): Promise<ExtendFeeBound | null>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Upper bounds on what a client is willing to sign.
3
+ *
4
+ * The calldata whitelist pins *what* is called; these pin what it may cost.
5
+ * The EOA path signs a server's `gasLimit` / `maxFeePerGas` verbatim, and
6
+ * neither the on-chain validator signature nor the calldata whitelist covers
7
+ * fee fields — so without this a compromised lit-ops-server could return a
8
+ * perfectly-pinned `mintUCD` carrying `gasLimit: 30_000_000` at 5000 gwei
9
+ * (~150 ETH) and the client would sign it. The calldata would be exactly what
10
+ * the user approved; the cost would not be.
11
+ *
12
+ * The worst-case total is the binding constraint — the two component rails
13
+ * are sanity checks on the individual fields. Real protocol calls land around
14
+ * 500k gas, so even a 200 gwei congestion spike costs ~0.1 ETH.
15
+ */
16
+ export declare const MAX_GAS_LIMIT = 5000000n;
17
+ export declare const MAX_FEE_PER_GAS_WEI = 2000000000000n;
18
+ export declare const DEFAULT_MAX_TX_FEE_WEI = 250000000000000000n;
19
+ export interface FeeCeiling {
20
+ maxGasLimit: bigint;
21
+ maxFeePerGasWei: bigint;
22
+ /** Worst-case `gasLimit × maxFeePerGas` a single tx may authorize. */
23
+ maxTxFeeWei: bigint;
24
+ /**
25
+ * Appended to the worst-case refusal so the user learns how THIS client lets
26
+ * them raise the ceiling (the CLI names `DH_MAX_TX_FEE_ETH`). The SDK has no
27
+ * opinion on where the override lives — that is consumer configuration.
28
+ */
29
+ raiseHint?: string;
30
+ }
31
+ export declare const DEFAULT_FEE_CEILING: FeeCeiling;
32
+ /** Worst-case fee a tx can cost: gasLimit × the highest per-gas price it authorizes. */
33
+ export declare function worstCaseFeeWei(gasLimit: bigint | null, maxFeePerGas: bigint | null): bigint | null;
34
+ /**
35
+ * Call `raise` if a transaction's fee parameters exceed the ceiling.
36
+ *
37
+ * Absent fields are not an error — ethers/the provider populates them later,
38
+ * and callers re-assert the ceiling after that happens.
39
+ */
40
+ export declare function assertFeeCeiling(gasLimit: bigint | null, maxFeePerGas: bigint | null, raise: (msg: string) => never, ceiling?: FeeCeiling): void;
@@ -0,0 +1,9 @@
1
+ export { TxValidationError } from "./errors";
2
+ export { SIGNABLE_FUNCTIONS, encodeSignable, decodeSignable, signableFunctionName, signableSelectors, } from "./signable-functions";
3
+ export { MAX_GAS_LIMIT, MAX_FEE_PER_GAS_WEI, DEFAULT_MAX_TX_FEE_WEI, DEFAULT_FEE_CEILING, worstCaseFeeWei, assertFeeCeiling, type FeeCeiling, } from "./fee-ceiling";
4
+ export { validateUnsignedTx, buildValidationContext, describeUnsignedTx, readTxFeeFields, type ExpectedAction, type ContractRegistry, type ValidationContext, } from "./tx-validator";
5
+ export { QUANTUM_SECONDS, AUTH_VALIDITY_WINDOWS, authValiditySec, nextQuantumTimestamp, isAuthFresh, isStrictCurrentQuantum, modeForChainId, randomNonceHex, OP_ENVELOPE_LAYOUT, OP_ENVELOPE_TYPES, OP_ENVELOPE_FIELD_ORDER, stampOpEnvelope, buildAuthEnvelope, type OpEnvelopeInput, type StampedOpEnvelope, type AuthEnvelope, } from "./op-envelope";
6
+ export { DELEGATED_OPS, BROADCAST_PATH, type DelegatedOp, type DelegatedOpSpec } from "./op-specs";
7
+ export { REQUEST_TIMEOUT_MS, LitOpsHttpError, LitOpsTransportError, fetchWithTimeout, postJson, asEnvelope, authorizeWithServer, broadcastSignedTx, type FetchOptions, type ExecuteEnvelope, type BroadcastEnvelope, type ExecuteOutcome, type BroadcastOutcome, } from "./lit-ops-client";
8
+ export { signAndBroadcast, type SignGuardSigner } from "./sign-and-broadcast";
9
+ export { computeExtendFeeUpperBound, type ExtendFeeBound } from "./extend-fee-bound";
@@ -0,0 +1,107 @@
1
+ import type { AuthEnvelope } from "./op-envelope";
2
+ /**
3
+ * The transport half of the server-delegated pipeline: POST to lit-ops-server
4
+ * with a timeout, turn the three classic failure modes into legible errors
5
+ * (a stalled connection, a non-2xx with an HTML body, a 200 that is not JSON),
6
+ * and narrow the untrusted envelope.
7
+ *
8
+ * `fetchImpl` is the test seam (precedent: `server-session.ts`). Login is NOT
9
+ * here — callers pass already-authenticated `headers`; how a session is
10
+ * obtained and persisted is consumer policy.
11
+ */
12
+ /** Default network timeout for lit-ops-server calls. */
13
+ export declare const REQUEST_TIMEOUT_MS = 60000;
14
+ export interface FetchOptions {
15
+ fetchImpl?: typeof fetch;
16
+ timeoutMs?: number;
17
+ }
18
+ /** A non-2xx response. Branch on `status`, never on message text. */
19
+ export declare class LitOpsHttpError extends Error {
20
+ readonly status: number;
21
+ readonly url: string;
22
+ readonly body: string;
23
+ constructor(url: string, status: number, body: string);
24
+ }
25
+ /** The connection could not be made at all (DNS, refused, reset). Distinct from a timeout. */
26
+ export declare class LitOpsTransportError extends Error {
27
+ readonly url: string;
28
+ constructor(url: string, cause: Error);
29
+ }
30
+ /**
31
+ * `fetch` with an abort-based timeout so a server that accepts the connection
32
+ * then stalls cannot hang the caller indefinitely.
33
+ */
34
+ export declare function fetchWithTimeout(url: string, init: RequestInit, timeoutMs?: number, fetchImpl?: typeof fetch): Promise<Response>;
35
+ /** Status-aware JSON POST. Throws `LitOpsHttpError` on non-2xx, `LitOpsTransportError` on no connection. */
36
+ export declare function postJson<T>(url: string, body: unknown, headers: Record<string, string>, opts?: FetchOptions): Promise<T>;
37
+ /** The per-action `execute` envelope, after runtime checking. */
38
+ export interface ExecuteEnvelope {
39
+ success: boolean;
40
+ data?: {
41
+ approved?: boolean;
42
+ unsignedTx?: Record<string, unknown>;
43
+ error?: string;
44
+ [key: string]: unknown;
45
+ };
46
+ error?: string;
47
+ }
48
+ export interface BroadcastEnvelope {
49
+ success: boolean;
50
+ data?: {
51
+ txHash?: string;
52
+ blockNumber?: number;
53
+ };
54
+ error?: string;
55
+ }
56
+ /**
57
+ * Narrow an untrusted server response to the envelope shape.
58
+ *
59
+ * Deliberately shallow: it verifies the envelope is an object with the fields
60
+ * the pipeline branches on, and leaves the security-critical values (calldata,
61
+ * target, amounts) to `validateUnsignedTx`, which pins them against what the
62
+ * user authorized. A schema library would add a dependency without moving that
63
+ * boundary.
64
+ */
65
+ export declare function asEnvelope(raw: unknown, label: string): ExecuteEnvelope & BroadcastEnvelope;
66
+ export interface ExecuteOutcome {
67
+ data: NonNullable<ExecuteEnvelope["data"]>;
68
+ unsignedTx: Record<string, unknown>;
69
+ }
70
+ /**
71
+ * POST the signed auth envelope to the op's execute route and enforce the
72
+ * approval gates. Returns a discriminated failure rather than throwing so
73
+ * callers keep their `{ success: false, error }` result shape.
74
+ */
75
+ export declare function authorizeWithServer(opts: {
76
+ serviceEndpoint: string;
77
+ executePath: string;
78
+ envelope: AuthEnvelope;
79
+ /** Already-authenticated headers (e.g. the lit-ops-server Bearer JWT). */
80
+ headers: Record<string, string>;
81
+ /** Used when the server refuses without saying why. */
82
+ refusalMessage: string;
83
+ } & FetchOptions): Promise<{
84
+ ok: true;
85
+ outcome: ExecuteOutcome;
86
+ } | {
87
+ ok: false;
88
+ error: string;
89
+ }>;
90
+ export interface BroadcastOutcome {
91
+ transactionHash?: string;
92
+ blockNumber?: number;
93
+ }
94
+ /** Relay a signed tx through lit-ops-server. */
95
+ export declare function broadcastSignedTx(opts: {
96
+ serviceEndpoint: string;
97
+ headers: Record<string, string>;
98
+ signedTx: string;
99
+ /** Merged into the broadcast body, e.g. `{ mintMeta }`. */
100
+ meta?: Record<string, unknown>;
101
+ } & FetchOptions): Promise<{
102
+ ok: true;
103
+ outcome: BroadcastOutcome;
104
+ } | {
105
+ ok: false;
106
+ error: string;
107
+ }>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Quantum-window timing, replay nonces and the borrower authorization
3
+ * envelope for the server-delegated protocol actions (mint / repay / renew).
4
+ *
5
+ * These helpers were duplicated across the CLI (`util/quantum.ts`) and the MCP
6
+ * (`boundary/upstream/shared/quantum.ts`) because the SDK owned the canonical
7
+ * quantum logic internally without exporting it. One copy now.
8
+ */
9
+ /** Quantum window width in seconds — matches the on-chain / LIT-action window. */
10
+ export declare const QUANTUM_SECONDS = 60;
11
+ /** Borrower authorizations are valid for this many windows (past, current, next). */
12
+ export declare const AUTH_VALIDITY_WINDOWS = 3;
13
+ export declare function authValiditySec(): number;
14
+ /**
15
+ * The NEXT quantum boundary, in seconds.
16
+ *
17
+ * Audit M-C: signing against the *current* boundary leaves almost no time for
18
+ * the round-trip when the call lands late in a window, so the authorization
19
+ * can expire before the tx is mined. Sitting exactly on a boundary must still
20
+ * advance, or the authorization is born at the edge of its window.
21
+ */
22
+ export declare function nextQuantumTimestamp(nowMs?: number): number;
23
+ /** General ops (mint/repay/extend): issued within the last 3 windows and never in the future. */
24
+ export declare function isAuthFresh(issuedAtSec: number, nowSec: number): boolean;
25
+ /** Withdrawal (`strictCurrentQuantum`): issued in the SAME quantum window as now. */
26
+ export declare function isStrictCurrentQuantum(issuedAtSec: number, nowSec: number): boolean;
27
+ /** lit-ops-server environment selector. Mainnet is the only `prod` chain. */
28
+ export declare function modeForChainId(chainId: number): string;
29
+ /**
30
+ * Replay nonce for an auth envelope.
31
+ *
32
+ * Width is a parameter only because the two envelopes on the wire differ: the
33
+ * protocol-action envelopes use 16 bytes and the `DhServerLogin` envelope uses
34
+ * 32. Both are far beyond any collision concern; the parameter exists to unify
35
+ * the implementation without changing either wire format.
36
+ */
37
+ export declare function randomNonceHex(byteLength?: number): string;
38
+ /**
39
+ * The packed personal-sign layout of the operation envelope. The MCP's approval
40
+ * page recomputes this hash in the browser from the displayed fields
41
+ * (`PERSONAL_SIGN_LAYOUTS["dh-op-envelope-v1"]`); the two MUST agree
42
+ * byte-for-byte, and `tests/shared/unit/sign-guard/op-envelope.test.ts` pins
43
+ * a golden vector so the wire format cannot move silently.
44
+ */
45
+ export declare const OP_ENVELOPE_LAYOUT = "dh-op-envelope-v1";
46
+ export declare const OP_ENVELOPE_TYPES: readonly ["bytes32", "uint256", "uint256", "uint256", "bytes32"];
47
+ export declare const OP_ENVELOPE_FIELD_ORDER: readonly ["positionId", "timestamp", "chainId", "amount", "action"];
48
+ export interface OpEnvelopeInput {
49
+ positionId: string;
50
+ chainId: number;
51
+ /** uint256 slot 4 of the envelope: the amount in wei for mint/repay, the term count for renew. */
52
+ value: bigint | number;
53
+ action: string;
54
+ }
55
+ export interface StampedOpEnvelope {
56
+ /** Human-readable fields in layout order — what an approval surface displays. */
57
+ fields: Record<(typeof OP_ENVELOPE_FIELD_ORDER)[number], string>;
58
+ /** `keccak256(abi.encodePacked(positionId, timestamp, chainId, amount, keccak256(action)))`. */
59
+ hashHex: string;
60
+ }
61
+ /** Stamp the envelope for one candidate quantum timestamp (re-stampable at the human's click). */
62
+ export declare function stampOpEnvelope(input: OpEnvelopeInput, quantumTs: number): StampedOpEnvelope;
63
+ export interface AuthEnvelope {
64
+ authMessage: Record<string, unknown>;
65
+ userSignature: string;
66
+ borrowerAddress: string;
67
+ }
68
+ /**
69
+ * Build and sign the off-chain authorization the LIT Action verifies.
70
+ *
71
+ * Signer-agnostic on purpose: the CLI and the MCP each have their own
72
+ * `DHSigner` shape, so this takes a `signAuthMessage(hash)` callback. In EOA
73
+ * mode the borrower signs directly; in Safe mode the agent EOA signs and the
74
+ * LIT Action accepts it via `positionDelegate(positionId)`.
75
+ *
76
+ * `payload` carries the value under whatever key that action's server endpoint
77
+ * expects (`DELEGATED_OPS[op].payloadKey`).
78
+ */
79
+ export declare function buildAuthEnvelope(opts: {
80
+ signAuthMessage: (messageHash: string) => Promise<string>;
81
+ borrowerAddress: string;
82
+ chainId: number;
83
+ positionId: string;
84
+ action: string;
85
+ value: bigint | number;
86
+ payload: Record<string, unknown>;
87
+ nowMs?: number;
88
+ }): Promise<AuthEnvelope>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The three server-delegated operations, as DATA.
3
+ *
4
+ * Audit CRIT-5: the CLI sent the wrong action string for repayment for months
5
+ * ("partial-payment" instead of "make-payment") because the string lived in
6
+ * one of three near-identical copies and only one was corrected. Every
7
+ * consumer now reads the action string, the execute path, the payload key and
8
+ * the broadcast meta key from here, so there is nothing left to correct twice.
9
+ *
10
+ * Action strings must match what the corresponding validator Lit Action pins
11
+ * (`PARTIAL_PAYMENT_ACTION_STRING = "make-payment"` in lit-actions; see also
12
+ * `sdk/src/utils/mint-authorization.utils.ts`).
13
+ */
14
+ export interface DelegatedOpSpec {
15
+ /** The `action` field of the signed envelope — what the LIT Action pins. */
16
+ readonly action: string;
17
+ /** lit-ops-server route that runs the LIT Action and returns the unsigned tx. */
18
+ readonly executePath: `/api/lit/${string}/execute`;
19
+ /** Key the server expects the value under in `authMessage`. */
20
+ readonly payloadKey: string;
21
+ /** Key the broadcast body carries the op's metadata under. */
22
+ readonly broadcastMetaKey: string;
23
+ /** Used when the server refuses without saying why. */
24
+ readonly refusalMessage: string;
25
+ }
26
+ export declare const DELEGATED_OPS: {
27
+ readonly mint: {
28
+ readonly action: "mint-ucd";
29
+ readonly executePath: "/api/lit/mint/execute";
30
+ readonly payloadKey: "amount";
31
+ readonly broadcastMetaKey: "mintMeta";
32
+ readonly refusalMessage: "Mint authorization failed";
33
+ };
34
+ readonly repay: {
35
+ readonly action: "make-payment";
36
+ readonly executePath: "/api/lit/repay/execute";
37
+ readonly payloadKey: "paymentAmount";
38
+ readonly broadcastMetaKey: "repayMeta";
39
+ readonly refusalMessage: "Repay authorization failed";
40
+ };
41
+ readonly renew: {
42
+ readonly action: "extend-position";
43
+ readonly executePath: "/api/lit/renew/execute";
44
+ readonly payloadKey: "newTerm";
45
+ readonly broadcastMetaKey: "renewMeta";
46
+ readonly refusalMessage: "Renew authorization failed";
47
+ };
48
+ };
49
+ export type DelegatedOp = keyof typeof DELEGATED_OPS;
50
+ /** lit-ops-server route every signed tx is relayed through. */
51
+ export declare const BROADCAST_PATH = "/api/lit/broadcast";
@@ -0,0 +1,56 @@
1
+ import type { ethers } from "ethers";
2
+ import { type ExpectedAction, type ValidationContext } from "./tx-validator";
3
+ import { type BroadcastOutcome, type FetchOptions } from "./lit-ops-client";
4
+ /**
5
+ * The minimum a signer must offer to take part in the pipeline. Both the CLI's
6
+ * and the MCP's `DHSigner` satisfy it structurally.
7
+ *
8
+ * `wrapInnerTx` is where Safe mode applies its `AgentModule.execute` envelope;
9
+ * EOA mode returns the tx unchanged.
10
+ */
11
+ export interface SignGuardSigner {
12
+ wrapInnerTx(inner: ethers.TransactionRequest): Promise<ethers.TransactionRequest>;
13
+ signTransaction(tx: ethers.TransactionRequest): Promise<string>;
14
+ }
15
+ /**
16
+ * The only path from a server-returned tx to a signature.
17
+ *
18
+ * Order is load-bearing and is why this lives in ONE function, in the SDK,
19
+ * consumed by every client:
20
+ * 1. `validateUnsignedTx` pins the call against what the user authorized;
21
+ * 2. `beforeSign` lets the consumer render its pre-sign banner (a second,
22
+ * independent decode gate) — it is the ONLY hook, and it runs after
23
+ * validation so it can never be used to launder an unvalidated tx;
24
+ * 3. `wrapInnerTx` applies the Safe envelope AFTER validation, so wrapping
25
+ * can never launder an unvalidated inner call;
26
+ * 4. only then is a key asked to sign;
27
+ * 5. the signed bytes are relayed through lit-ops-server.
28
+ *
29
+ * Deliberately two composable primitives (`authorizeWithServer` + this) rather
30
+ * than one configurable executor: a single executor taking eight hooks would
31
+ * relocate the divergence risk into hook implementations instead of removing
32
+ * it. Flow differences live in `DELEGATED_OPS` data and in the callers'
33
+ * `expected` objects, never in per-flow hooks.
34
+ */
35
+ export declare function signAndBroadcast(opts: {
36
+ unsignedTx: Record<string, unknown>;
37
+ expected: ExpectedAction;
38
+ vctx: ValidationContext;
39
+ signer: SignGuardSigner;
40
+ /** Presentation only — runs after validation, before any key is touched. */
41
+ beforeSign?: (unsignedTx: Record<string, unknown>) => void;
42
+ broadcast: {
43
+ serviceEndpoint: string;
44
+ headers: Record<string, string>;
45
+ /** Merged into the broadcast body, e.g. `{ mintMeta }`. */
46
+ meta?: Record<string, unknown>;
47
+ } & FetchOptions;
48
+ /** Prefix for sign/broadcast errors, e.g. "UCD approve broadcast". */
49
+ errorPrefix?: string;
50
+ }): Promise<{
51
+ ok: true;
52
+ outcome: BroadcastOutcome;
53
+ } | {
54
+ ok: false;
55
+ error: string;
56
+ }>;
@@ -0,0 +1,41 @@
1
+ import { ethers } from "ethers";
2
+ /**
3
+ * The ONE whitelist of every function a Diamond Hands client is willing to
4
+ * sign on a borrower's behalf.
5
+ *
6
+ * History that explains why this lives in the SDK: the CLI and the MCP each
7
+ * carried a private copy of this list. They drifted in both directions — the
8
+ * MCP copy kept `repayPosition` (a function on no deployed contract, removed
9
+ * from the CLI in ccbe0e3fc) and gained the PSM pair; the CLI copy alone had a
10
+ * parity test against `AgentModule.sol`. Two consumers, one on-chain module,
11
+ * one parity test. Now there is one list, one decoder and one parity test
12
+ * (`tests/shared/unit/sign-guard/agent-module-whitelist.parity.test.ts`) that
13
+ * guards every consumer.
14
+ *
15
+ * Kept as a literal ABI array rather than derived from `POSITION_MANAGER_ABI`
16
+ * so that any new signable function requires an explicit, reviewed change to
17
+ * this file. `contracts/src/safe-modules/AgentModule.sol` states that its
18
+ * hardcoded whitelist MUST stay in lock-step with this array.
19
+ */
20
+ export declare const SIGNABLE_FUNCTIONS: readonly ["function mintUCD(bytes32 positionId, uint256 mintAmount, uint256 mintFee, uint256 newDebt, uint256 newCollateral, uint256 btcPrice, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractHash, uint256 quantumTimestamp, bytes calldata mintValidatorSignature) external returns (bool)", "function makePayment(bytes32 positionId, uint256 paymentAmount, uint256 quantumTimestamp, uint256 btcPrice, bytes calldata paymentValidatorSignature) external returns (bool)", "function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, uint256 proRataRenewalFee, bytes calldata extensionValidatorSignature) external returns (bool)", "function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, bytes calldata extensionValidatorSignature) external returns (bool)", "function withdrawBTC((bytes32 positionId, bytes32 actionHash, bytes32 authorizedSpendsHash, bytes32 ucdDebtHash, bytes32 contractBundleHash, string withdrawalAddress, uint256 totalDeduction, uint256 newCollateral, uint256 quantumTimestamp, uint256 btcPrice, string utxoTxid, uint32 utxoVout) params, bytes withdrawalValidatorSignature, bytes btcSpendAuthSignature) external returns (bool)", "function approve(address spender, uint256 amount) external returns (bool)", "function setPositionDelegate(bytes32 positionId, address newDelegate) external", "function createPosition(bytes32 pkpId, bytes calldata validatorSignature, string mainnetVaultAddress, string regtestVaultAddress, uint256 selectedTermMonths, uint256 validatorVersion, bytes calldata pkpPublicKey) external returns (bytes32 positionId)", "function swap(address stablecoin, uint256 amountIn, uint256 minUcdOut) external returns (uint256)", "function redeem(address stablecoin, uint256 ucdAmount, uint256 minStablecoinOut) external returns (uint256)", "function addAddress(string btcAddress) external"];
21
+ /**
22
+ * Encode calldata through the SAME ABI surface `validateUnsignedTx` decodes
23
+ * against. Any synthesized tx (test doubles, mock upstreams) MUST use this and
24
+ * never its own Interface, so synthesized bytes cannot drift from the decode
25
+ * whitelist.
26
+ */
27
+ export declare function encodeSignable(fn: string, args: unknown[]): string;
28
+ /**
29
+ * Decode calldata against the whitelist — the ONLY decoder a human-facing
30
+ * approval surface may render from (decode-equivalence: what the human sees
31
+ * IS what the validator checks). Throws `TxValidationError` when the bytes do
32
+ * not match any signable function.
33
+ */
34
+ export declare function decodeSignable(data: string): {
35
+ name: string;
36
+ args: ethers.Result;
37
+ };
38
+ /** Function name for display, or `"<unparseable>"`. Never throws. */
39
+ export declare function signableFunctionName(data: string): string;
40
+ /** Every selector the whitelist accepts, keyed by 4-byte selector → sighash form. */
41
+ export declare function signableSelectors(): Map<string, string>;