@gvnrdao/dh-sdk 0.0.339 → 0.0.341

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,115 @@
1
+ /**
2
+ * Loan Helper Utilities
3
+ * Extracted common patterns for loan data processing and enrichment
4
+ */
5
+ import type { LoanData, LoanDataDetail, BitcoinAddresses } from '../interfaces';
6
+ import type { BitcoinProvider } from './bitcoin-provider.utils';
7
+ import type { ConcurrencyLimiter } from './concurrency-limiter.utils';
8
+ /**
9
+ * Derive Bitcoin addresses for a loan if needed
10
+ * @param loan Loan data that may need address derivation
11
+ * @param bitcoinNetwork Target Bitcoin network ("mainnet" | "testnet" | "regtest")
12
+ * @param getBitcoinAddressesFromPkp Function to derive addresses from PKP
13
+ * @returns Updated loan with derived addresses
14
+ */
15
+ export declare function deriveAddressesIfNeeded(loan: LoanData | LoanDataDetail, bitcoinNetwork: "mainnet" | "testnet" | "regtest" | undefined, getBitcoinAddressesFromPkp: (pkpPublicKey: string) => Promise<BitcoinAddresses>): Promise<LoanData | LoanDataDetail>;
16
+ /**
17
+ * Enrich loans with Bitcoin balance information
18
+ * Handles the common pattern of fetching and attaching balance data
19
+ * @param loans Array of loans to enrich
20
+ * @param bitcoinProvider Bitcoin provider instance
21
+ * @param concurrencyLimiter Optional concurrency limiter for controlled parallel execution
22
+ * @param getBalanceWithCache Function to get balance with caching
23
+ * @returns Enriched loans with balance information
24
+ */
25
+ export declare function enrichWithBalance(loans: (LoanData | LoanDataDetail)[], bitcoinProvider: BitcoinProvider | null, concurrencyLimiter: ConcurrencyLimiter | null, getBalanceWithCache: (vaultAddress: string) => Promise<any>): Promise<(LoanData | LoanDataDetail)[]>;
26
+ /**
27
+ * Normalize loan data transformation
28
+ * Single mapper to avoid subtle divergence across methods
29
+ * @param rawLoan Raw loan data from contract or graph
30
+ * @param transformType Type of transformation to apply
31
+ * @returns Normalized loan data
32
+ */
33
+ export declare function normalizeLoanTransform(rawLoan: any, transformType?: 'contract' | 'graph'): LoanData | LoanDataDetail;
34
+ /**
35
+ * Extract vault address from loan collateral
36
+ * Handles both string and BitcoinAddresses object types
37
+ * @param loan Loan data
38
+ * @param preferredNetwork Preferred network to use if multiple addresses available
39
+ * @returns Vault address string
40
+ */
41
+ export declare function extractVaultAddress(loan: LoanData | LoanDataDetail, preferredNetwork?: 'mainnet' | 'testnet' | 'regtest'): string;
42
+ /**
43
+ * Select Bitcoin address for the given network.
44
+ *
45
+ * @param addresses Bitcoin addresses object (mainnet/testnet/regtest variants)
46
+ * @param bitcoinNetwork Target network ("mainnet" | "testnet" | "regtest" | undefined → mainnet)
47
+ */
48
+ export declare function selectBitcoinAddress(addresses: BitcoinAddresses, bitcoinNetwork: "mainnet" | "testnet" | "regtest" | undefined): string;
49
+ /**
50
+ * Calculate the expected mint fee for a UCD mint, including the Scenario B tail fee.
51
+ *
52
+ * Scenario B applies when:
53
+ * - The loan has been renewed (previousExpiryAt > 0), AND
54
+ * - The mint occurs before the original term expiry (mintTimestamp < previousExpiryAt)
55
+ *
56
+ * In that case, an additional pro-rata tail fee is charged for the remaining time
57
+ * in the original term, on top of the standard origination fee for the renewal term.
58
+ *
59
+ * Formula:
60
+ * standardFee = originationFeeBps * mintAmount / 10000
61
+ * tailFee = (tailSeconds / originalTermSeconds) * originationFeeBps * mintAmount / 10000
62
+ * totalFee = standardFee + tailFee (Scenario B only)
63
+ *
64
+ * This function is purely local (no RPC calls) and mirrors ProtocolConstants logic:
65
+ * 12 months = 365 days; other terms = 30 days/month.
66
+ *
67
+ * @param mintAmount - Amount of UCD to mint (in wei, bigint)
68
+ * @param mintTimestamp - Unix timestamp of the mint (seconds)
69
+ * @param originationFeeBps - Origination fee in basis points (e.g. 50 = 0.5%)
70
+ * @param expiryAt - Current loan expiry timestamp after renewal (seconds)
71
+ * @param previousExpiryAt - Expiry before last renewal; 0 if never renewed (seconds)
72
+ * @param selectedTerm - Current term segment in months (e.g. 12)
73
+ * @param totalTerm - Total accumulated loan months (e.g. 24 after one renewal); 0 before first renewal
74
+ */
75
+ export declare function calculateMintFee(params: {
76
+ mintAmount: bigint;
77
+ mintTimestamp: number;
78
+ originationFeeBps: number;
79
+ expiryAt: number;
80
+ previousExpiryAt: number;
81
+ selectedTerm: number;
82
+ totalTerm: number;
83
+ }): bigint;
84
+ /**
85
+ * Base origination fee on a principal (wei), `floor(amount * bps / 10000)` —
86
+ * the same integer arithmetic the Lit validator and TermManager use. Excludes
87
+ * the post-renewal tail fee, so it is a LOWER bound on the fee the validator
88
+ * will charge; a pre-check built on it can only be more permissive, never
89
+ * stricter, than the validator's own fee-inclusive check.
90
+ */
91
+ export declare function baseMintFeeWei(mintAmountWei: bigint, originationFeeBps: number): bigint;
92
+ /**
93
+ * The loan cap is fee-INCLUSIVE on-chain: `UCDController._enforceLoanPathPerMintCap`
94
+ * rejects when `currentDebt + mintAmount + mintFee > maximumLoanValueUcd`
95
+ * (audit V12 #62230). Mainnet 2026-09-15: a 98 UCD mint + 2.94 fee against a
96
+ * 100 UCD cap passed every principal-only pre-check, paid for the Lit round-trip
97
+ * and reverted `MintGuardFailed(1)`. Mirrors the contract exactly — equal to the
98
+ * cap is allowed.
99
+ */
100
+ export declare function debtAfterMintExceedsLoanCap(params: {
101
+ currentDebtWei: bigint;
102
+ mintAmountWei: bigint;
103
+ mintFeeWei: bigint;
104
+ maxLoanWei: bigint;
105
+ }): boolean;
106
+ /**
107
+ * Largest principal (wei) that still fits under the cap once its base fee is
108
+ * added: `floor((maxLoan - currentDebt) * 10000 / (10000 + bps))`; zero when
109
+ * the position is already at or above the cap.
110
+ */
111
+ export declare function maxPrincipalWithinLoanCap(params: {
112
+ maxLoanWei: bigint;
113
+ currentDebtWei: bigint;
114
+ originationFeeBps: number;
115
+ }): bigint;
@@ -15,8 +15,20 @@ export declare const ERROR_STRING_SELECTOR = "0x08c379a0";
15
15
  /** Solidity `Panic(uint256)` (assert failures, overflow, div-by-zero, …). */
16
16
  export declare const PANIC_SELECTOR = "0x4e487b71";
17
17
  /**
18
- * Union of the custom-error selectors the mint/payment/quantum paths decode. Keep in
19
- * sync with OperationAuthorizationRegistry / PositionManager / LoanOperationsManager.
18
+ * The custom-error selectors the mint/payment/quantum paths decode.
19
+ *
20
+ * EVERY row is an error some contract declares, and
21
+ * `tests/shared/unit/contract-error-decoder.test.ts` proves it against the committed ABIs —
22
+ * because six rows here were fiction, and fiction on this path is read by a borrower:
23
+ *
24
+ * - `InDeadZone()`, `QuantumExpired()`, `UnauthorizedSigner()` and a no-argument
25
+ * `InvalidSignature()` are declared by NO contract (the real ones are
26
+ * `DeadZoneViolation()`, and `InvalidSignature(string reason)`);
27
+ * - `0x48f5c3ed` was labelled `Unauthorized()`, which is really `0x82b42900`;
28
+ * - `0x3ee5aeb5` was labelled `OperationNotAuthorized()` — it is
29
+ * `ReentrancyGuardReentrantCall()`, and `0xd92e233d`, labelled `ValidationFailed()`, is
30
+ * really `ZeroAddress()` (`ValidationFailed()` is `0x0a0b0d79`). Both are reachable
31
+ * reverts that were being reported to users under another error's name.
20
32
  */
21
33
  export declare const QUANTUM_REVERT_NAMES: Record<string, string>;
22
34
  export interface DecodedRevert {
@@ -5,5 +5,9 @@
5
5
  * `instanceof` checks in consumers cannot split across two module realms.
6
6
  */
7
7
  export declare class TxValidationError extends Error {
8
- constructor(msg: string);
8
+ /**
9
+ * @param subject What is being refused. Defaults to the server-returned transaction the
10
+ * validator exists for; client-built flows (the PSM exchange) name themselves instead.
11
+ */
12
+ constructor(msg: string, subject?: string);
9
13
  }
@@ -16,6 +16,22 @@
16
16
  export declare const MAX_GAS_LIMIT = 5000000n;
17
17
  export declare const MAX_FEE_PER_GAS_WEI = 2000000000000n;
18
18
  export declare const DEFAULT_MAX_TX_FEE_WEI = 250000000000000000n;
19
+ /**
20
+ * The priority-fee (tip) band for a quantum-bounded send, in wei.
21
+ *
22
+ * These live HERE, beside the ceilings, because they are the same kind of fact — what a
23
+ * client may pay — and every client needs them: the SDK's EIP-1559 broadcaster clamps the
24
+ * market tip into this band, and the CLI and MCP price their own sends. They were
25
+ * previously reachable only from a deep SDK path (`utils/chunks/eip1559-broadcast.utils`),
26
+ * which a consumer taking only `./sign-guard` cannot import, so each client wrote its own
27
+ * numbers instead.
28
+ *
29
+ * Floor: builders skip a tip that is too low, and a missed quantum wastes the Lit
30
+ * signatures, the Chipotle spend and the reverted transaction's base fee — all far more
31
+ * than the tip. Cap: a spiking `eth_feeHistory` must not turn into an overpaid send.
32
+ */
33
+ export declare const PRIORITY_FEE_FLOOR_WEI = 100000000n;
34
+ export declare const PRIORITY_FEE_CAP_WEI = 3000000000n;
19
35
  export interface FeeCeiling {
20
36
  maxGasLimit: bigint;
21
37
  maxFeePerGasWei: bigint;
@@ -1,9 +1,10 @@
1
1
  export { TxValidationError } from "./errors";
2
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";
3
+ export { MAX_GAS_LIMIT, MAX_FEE_PER_GAS_WEI, DEFAULT_MAX_TX_FEE_WEI, PRIORITY_FEE_FLOOR_WEI, PRIORITY_FEE_CAP_WEI, DEFAULT_FEE_CEILING, worstCaseFeeWei, assertFeeCeiling, type FeeCeiling, } from "./fee-ceiling";
4
4
  export { validateUnsignedTx, buildValidationContext, describeUnsignedTx, readTxFeeFields, type ExpectedAction, type ContractRegistry, type ValidationContext, } from "./tx-validator";
5
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
6
  export { DELEGATED_OPS, BROADCAST_PATH, type DelegatedOp, type DelegatedOpSpec } from "./op-specs";
7
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
8
  export { signAndBroadcast, type SignGuardSigner } from "./sign-and-broadcast";
9
9
  export { computeExtendFeeUpperBound, type ExtendFeeBound } from "./extend-fee-bound";
10
+ export { buildPsmApproveTx, buildPsmSwapTx, buildPsmRedeemTx, planPsmExchange, executePsmPlan, psmExchangeReadsFromProvider, type PsmUnsignedTx, type PsmLeg, type PsmExchangeAddresses, type PsmExchangeReads, type PsmExchangePlan, } from "./psm-exchange";
@@ -0,0 +1,122 @@
1
+ import { ethers } from "ethers";
2
+ import { type ExpectedAction, type ValidationContext } from "./tx-validator";
3
+ /**
4
+ * The PSM exchange pair — stablecoin → UCD (`swap`) and UCD → stablecoin (`redeem`) — built and
5
+ * validated in ONE place, for every client.
6
+ *
7
+ * Exact-amount approvals only. The SDK's old `psmSwap` / `psmRedeem` approved `MaxUint256`, so a
8
+ * single swap left an unlimited allowance behind; the MCP refused to use them and built its own
9
+ * exact-amount flow. This module is that flow, lifted into the SDK.
10
+ *
11
+ * Deliberately split in two:
12
+ * - `planPsmExchange` builds every leg and runs `validateUnsignedTx` on each before returning;
13
+ * - `executePsmPlan` sends the legs through an ethers `Signer` — the path a browser wallet can
14
+ * take (`sendTransaction`). Clients with their own signing pipeline (the MCP's keystore and
15
+ * approval cards) take the validated legs from the plan and submit them their own way.
16
+ */
17
+ /** An unsigned tx exactly as `validateUnsignedTx` consumes it. */
18
+ export interface PsmUnsignedTx {
19
+ to: string;
20
+ data: string;
21
+ value: "0x0";
22
+ chainId: number;
23
+ }
24
+ export interface PsmLeg {
25
+ /** What this leg is — for progress reporting, cards and error messages. */
26
+ step: "reset-approve" | "approve" | "swap" | "redeem";
27
+ tx: PsmUnsignedTx;
28
+ /** The sign-guard expectation this leg was validated against. Re-check any populated form with it. */
29
+ expected: ExpectedAction;
30
+ }
31
+ export interface PsmExchangeAddresses {
32
+ /** SimplePSMV2 proxy. */
33
+ psm: string;
34
+ /** The stablecoin swapped in (swap) or received (redeem). */
35
+ stablecoin: string;
36
+ /** Required for redeem: UCD is the token approved. */
37
+ ucdToken?: string;
38
+ /** Required for redeem: UCDToken.burn spends the allowance as the UCDController (audit M-4). */
39
+ ucdController?: string;
40
+ }
41
+ /** The two on-chain facts the plan depends on. Injected so the planner stays pure. */
42
+ export interface PsmExchangeReads {
43
+ isStablecoinSupported(stablecoin: string): Promise<boolean>;
44
+ allowance(token: string, owner: string, spender: string): Promise<bigint>;
45
+ }
46
+ export interface PsmExchangePlan {
47
+ direction: "swap" | "redeem";
48
+ owner: string;
49
+ /** The token approved: the stablecoin for swap, UCD for redeem. */
50
+ token: string;
51
+ /** Its spender: the PSM for swap, the UCDController for redeem. */
52
+ spender: string;
53
+ amountIn: bigint;
54
+ minOut: bigint;
55
+ /** The allowance read when the plan was made. */
56
+ allowanceBefore: bigint;
57
+ /** Zero, one (approve) or two (reset to zero, then approve) legs — in send order. */
58
+ approvals: PsmLeg[];
59
+ exec: PsmLeg;
60
+ }
61
+ export declare function buildPsmApproveTx(p: {
62
+ token: string;
63
+ spender: string;
64
+ amount: bigint;
65
+ chainId: number;
66
+ }): PsmUnsignedTx;
67
+ export declare function buildPsmSwapTx(p: {
68
+ psm: string;
69
+ stablecoin: string;
70
+ amountIn: bigint;
71
+ minOut: bigint;
72
+ chainId: number;
73
+ }): PsmUnsignedTx;
74
+ export declare function buildPsmRedeemTx(p: {
75
+ psm: string;
76
+ stablecoin: string;
77
+ ucdAmount: bigint;
78
+ minOut: bigint;
79
+ chainId: number;
80
+ }): PsmUnsignedTx;
81
+ /** Provider-backed reads for the planner. */
82
+ export declare function psmExchangeReadsFromProvider(provider: ethers.Provider, psmAddress: string): PsmExchangeReads;
83
+ /**
84
+ * Build and validate every leg of one PSM exchange. Throws before anything is signed when:
85
+ * the amount or the minimum-out floor is zero (the contract refuses a zero floor); an address
86
+ * is missing — a redeem refuses rather than guess its spender; or the stablecoin is not
87
+ * supported by the PSM ON-CHAIN (an index or a config can lag a governance removal).
88
+ *
89
+ * Approvals, against the allowance read now:
90
+ * - `allowance >= amountIn` → no approve leg;
91
+ * - `allowance == 0` → approve exactly `amountIn`;
92
+ * - otherwise → approve 0, then approve exactly `amountIn`. Some tokens (USDT)
93
+ * revert a non-zero → non-zero approve, and a failed exchange can leave exactly this partial
94
+ * residue. Resetting first is correct for every ERC-20, so it is not token-sniffed.
95
+ */
96
+ export declare function planPsmExchange(params: {
97
+ direction: "swap" | "redeem";
98
+ owner: string;
99
+ addresses: PsmExchangeAddresses;
100
+ amountIn: bigint;
101
+ minOut: bigint;
102
+ vctx: ValidationContext;
103
+ reads: PsmExchangeReads;
104
+ }): Promise<PsmExchangePlan>;
105
+ /**
106
+ * Send a plan's legs through an ethers `Signer`, in order, each confirmed before the next.
107
+ *
108
+ * Each leg's request — exactly `{to, data, value: 0, chainId}` — is validated again immediately
109
+ * before it is handed over. Nonce, gas and fees are NOT populated here: the signer populates them
110
+ * when the leg's turn comes (a swap cannot be gas-estimated until its approve is mined), so a
111
+ * browser wallet shows the user its own fee and a Safe signer applies its own nonce, exactly as
112
+ * before. Blind signers that need the fee ceiling on the signed form (CLI, MCP) take the
113
+ * validated legs from the plan into their own pipeline instead.
114
+ *
115
+ * A leg that reverts or fails throws, naming the step; when an approve already landed, the error
116
+ * says so, because an exact allowance then stands.
117
+ */
118
+ export declare function executePsmPlan(plan: PsmExchangePlan, signer: ethers.Signer, vctx: ValidationContext): Promise<{
119
+ hash: string;
120
+ blockNumber: number;
121
+ approvalHashes: string[];
122
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gvnrdao/dh-sdk",
3
- "version": "0.0.339",
3
+ "version": "0.0.341",
4
4
  "description": "TypeScript SDK for Diamond Hands Protocol",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -39,6 +39,11 @@
39
39
  "types": "./dist/sign-guard.d.ts",
40
40
  "import": "./dist/sign-guard.mjs",
41
41
  "require": "./dist/sign-guard.js"
42
+ },
43
+ "./contract-errors": {
44
+ "types": "./dist/contract-errors.d.ts",
45
+ "import": "./dist/contract-errors.mjs",
46
+ "require": "./dist/contract-errors.js"
42
47
  }
43
48
  },
44
49
  "files": [
@@ -50,8 +55,9 @@
50
55
  "scripts": {
51
56
  "build": "npm run sync:deployments && npm run validate:contracts && npm run build:node",
52
57
  "build:all": "npm run sync:deployments && npm run validate:contracts && npm run build:node && npm run build:browser",
53
- "build:node": "npm run sync:deployments && tsup && npm run build:types",
58
+ "build:node": "npm run sync:deployments && tsup && npm run build:types && npm run check:bundle-externals",
54
59
  "build:types": "tsc -p tsconfig.build.json",
60
+ "check:bundle-externals": "node scripts/check-bundle-externals.mjs",
55
61
  "build:browser": "cd browser && npm install && npm run build",
56
62
  "sync:deployments": "node scripts/sync-deployments.js",
57
63
  "sync:typechain": "cd ../contracts && npm run compile",
@@ -74,7 +80,8 @@
74
80
  "lint": "eslint src --ext .ts && npm run lint:server-boundary && npm run lint:sign-guard",
75
81
  "lint:server-boundary": "node scripts/check-pkp-mint-server-imports.mjs",
76
82
  "lint:sign-guard": "node scripts/check-sign-guard-imports.mjs",
77
- "clean": "rm -rf dist && rm -rf browser/dist"
83
+ "clean": "rm -rf dist && rm -rf browser/dist",
84
+ "gen:contract-errors": "node scripts/gen-contract-errors.mjs"
78
85
  },
79
86
  "keywords": [
80
87
  "bitcoin",
@@ -93,18 +100,22 @@
93
100
  },
94
101
  "sideEffects": false,
95
102
  "dependencies": {
96
- "@gvnrdao/dh-lit-actions": "^0.0.322",
97
- "@gvnrdao/dh-lit-ops": "^0.0.316",
98
103
  "@noble/hashes": "^1.5.0",
99
104
  "axios": "^1.17.0",
100
105
  "bech32": "^2.0.0",
101
106
  "bip66": "^2.0.0",
102
107
  "bitcoinjs-lib": "^6.1.0",
108
+ "bn.js": "^5.2.3",
103
109
  "bs58check": "^3.0.1",
104
110
  "crypto-js": "^4.2.0",
105
111
  "dotenv": "^17.4.2",
112
+ "elliptic": "^6.6.1",
106
113
  "ethers": "6.16.0",
107
- "valibot": "^1.1.0"
114
+ "uuid": "^9.0.1"
115
+ },
116
+ "optionalDependencies": {
117
+ "@gvnrdao/dh-lit-actions": "^0.0.322",
118
+ "@gvnrdao/dh-lit-ops": "^0.0.316"
108
119
  },
109
120
  "devDependencies": {
110
121
  "@babel/preset-env": "7.29.7",
@@ -151,6 +162,9 @@
151
162
  ],
152
163
  "sign-guard": [
153
164
  "./dist/sign-guard.d.ts"
165
+ ],
166
+ "contract-errors": [
167
+ "./dist/contract-errors.d.ts"
154
168
  ]
155
169
  }
156
170
  }