@gvnrdao/dh-sdk 0.0.338 → 0.0.340

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.
@@ -78,6 +78,40 @@ export interface PaginatedLoansResponse {
78
78
  maxRows: number;
79
79
  totalLoans: number;
80
80
  }
81
+ /**
82
+ * Per-status row in {@link BorrowerUcdDebtSummary.byStatus}.
83
+ * Only statuses with count > 0 appear; order follows {@link LoanStatus} ordinals.
84
+ */
85
+ export interface BorrowerUcdDebtStatusCount {
86
+ status: LoanStatus;
87
+ /** Enum key, e.g. "ACTIVE", "PENDING_DEPOSIT". */
88
+ statusLabel: string;
89
+ count: number;
90
+ }
91
+ /**
92
+ * Aggregated UCD debt + status mix for one borrower, from ONE pass over the
93
+ * subgraph's raw position rows — shared by Butler, MCP, CLI.
94
+ *
95
+ * This is the INDEXED view (the subgraph lags writes). It is informational and
96
+ * must never size a transaction; repayments read the chain
97
+ * (`getPositionDetailsView`).
98
+ */
99
+ export interface BorrowerUcdDebtSummary {
100
+ /** Lower-cased. */
101
+ borrower: string;
102
+ /** Positions included in the sum — every status, all pages. */
103
+ loanCount: number;
104
+ /** Canonical total debt in wei (18 decimals), as a decimal string (BigInt sum). */
105
+ totalUcdDebt: string;
106
+ /** Display only: 18-decimal rendering with trailing zeros trimmed (e.g. "21.93"). */
107
+ totalUcdDebtHuman: string;
108
+ /** Only statuses with count > 0, ordered by {@link LoanStatus} ordinal. */
109
+ byStatus: BorrowerUcdDebtStatusCount[];
110
+ /** Where the figures came from. Reserved for a future on-chain cross-check ("chain"). */
111
+ source: "subgraph";
112
+ /** ms epoch, set by the SDK when the last page landed. */
113
+ fetchedAt: number;
114
+ }
81
115
  /**
82
116
  * Detailed Loan Data Interface (used by getLoanById / PKP)
83
117
  */
@@ -893,6 +893,16 @@ export declare class DiamondHandsSDK {
893
893
  page: number;
894
894
  pageSize: number;
895
895
  }, orderBy?: "createdAt" | "lastUpdatedAt" | "ucdDebt", orderDirection?: "asc" | "desc"): Promise<Result<import("../interfaces/chunks/loan-operations.i").PaginatedLoansResponse, SDKError>>;
896
+ /**
897
+ * Combined UCD debt (canonical wei string + display string) and by-status loan
898
+ * counts for a borrower, from one pass over the subgraph. `source: "subgraph"` —
899
+ * an INDEXED, informational figure that lags writes; never size a transaction
900
+ * from it (repayments read the chain via `getPositionDetailsView`).
901
+ *
902
+ * Fails loud with a SUBGRAPH-category `SDKError` (cause attached) on any page
903
+ * failure or past 10,000 positions; never returns a partial total.
904
+ */
905
+ getBorrowerUcdDebtSummary(borrower: string): Promise<Result<import("../interfaces/chunks/loan-operations.i").BorrowerUcdDebtSummary, SDKError>>;
896
906
  /**
897
907
  * Get all active loans
898
908
  *
@@ -974,11 +984,12 @@ export declare class DiamondHandsSDK {
974
984
  getPSMAvailableReserves(stablecoinAddress: string): Promise<bigint>;
975
985
  /**
976
986
  * Execute a PSM stablecoin → UCD swap.
977
- * Handles stablecoin approval to the PSM contract if the current allowance is insufficient.
987
+ * Approves EXACTLY `amountWei` to the PSM when the current allowance is short (resetting a
988
+ * partial allowance to zero first); every leg is validated by the sign-guard before it is sent.
978
989
  *
979
990
  * @param params.stablecoinAddress - ERC-20 address of the stablecoin to swap in
980
991
  * @param params.amountWei - Stablecoin amount in native decimals (bigint)
981
- * @param params.minUcdOutWei - Minimum UCD to receive; reverts if below this (1% slippage guard)
992
+ * @param params.minUcdOutWei - Minimum UCD to receive; must be > 0 (reverts on-chain if below)
982
993
  * @param params.signer - Connected signer for the approval and swap transactions
983
994
  */
984
995
  psmSwap(params: {
@@ -992,12 +1003,13 @@ export declare class DiamondHandsSDK {
992
1003
  }>;
993
1004
  /**
994
1005
  * Execute a PSM UCD → stablecoin redeem.
995
- * Handles UCD approval to UCDController (not PSM) to satisfy the M-4 burn allowance guard:
996
- * UCDToken.burn(from, amount) calls _spendAllowance(from, msg.sender=ucdController, amount).
1006
+ * Approves EXACTLY `ucdAmountWei` of UCD to the UCDController (not the PSM) to satisfy the
1007
+ * M-4 burn allowance guard: UCDToken.burn(from, amount) calls
1008
+ * _spendAllowance(from, msg.sender=ucdController, amount).
997
1009
  *
998
1010
  * @param params.stablecoinAddress - ERC-20 address of the stablecoin to receive
999
1011
  * @param params.ucdAmountWei - UCD amount to redeem (18 decimals, bigint)
1000
- * @param params.minStablecoinOutWei - Minimum stablecoin to receive (slippage guard)
1012
+ * @param params.minStablecoinOutWei - Minimum stablecoin to receive; must be > 0
1001
1013
  * @param params.signer - Connected signer
1002
1014
  */
1003
1015
  psmRedeem(params: {
@@ -1009,6 +1021,11 @@ export declare class DiamondHandsSDK {
1009
1021
  hash: string;
1010
1022
  blockNumber: number;
1011
1023
  }>;
1024
+ /**
1025
+ * Shared PSM path: plan + validate every leg against the chain the SIGNER is on, then send.
1026
+ * The signer's chain must match the SDK's configured chain — the addresses came from it.
1027
+ */
1028
+ private runPsmExchange;
1012
1029
  /**
1013
1030
  * Wait for the subgraph to index up to (and including) the given block number.
1014
1031
  * Call after on-chain actions (createLoan, mintUCD, etc.) before querying the subgraph.
@@ -13,7 +13,7 @@ import { Result } from "../../types/result";
13
13
  import { SDKError } from "../../utils/error-handler";
14
14
  import type { BitcoinOperations } from "../bitcoin/bitcoin-operations.module";
15
15
  import type { Cache } from "../cache/cache-manager.module";
16
- import type { LoanData, LoanDataDetail, PaginatedLoansResponse } from "../../interfaces/chunks/loan-operations.i";
16
+ import type { BorrowerUcdDebtSummary, LoanData, LoanDataDetail, PaginatedLoansResponse } from "../../interfaces/chunks/loan-operations.i";
17
17
  import type { LoanEvents, LoanEventsFilter } from "../../types/event-types";
18
18
  import { DiamondHandsGraphClient } from "@graphs/diamond-hands";
19
19
  import { type Provider } from "ethers";
@@ -141,6 +141,17 @@ export declare class LoanQuery {
141
141
  * @returns Paginated loans for borrower
142
142
  */
143
143
  getLoansByBorrower(borrower: string, pagination?: PaginationParams, orderBy?: "createdAt" | "lastUpdatedAt" | "ucdDebt", orderDirection?: "asc" | "desc"): Promise<Result<PaginatedLoansResponse, SDKError>>;
144
+ /**
145
+ * Combined UCD debt + by-status counts for a borrower from ONE pass over the
146
+ * subgraph's raw rows (`graphClient.getBorrowerDebtRows`): BigInt wei sum, strict
147
+ * status labels, `source: "subgraph"`.
148
+ *
149
+ * INDEXED figure, not a transaction input — the subgraph lags writes. Anything that
150
+ * sizes a repayment reads the chain (`getPositionDetailsView`). Fails loud: a page
151
+ * failure or the page cap is a SUBGRAPH-category failure carrying the cause; there
152
+ * is no partial total.
153
+ */
154
+ getBorrowerUcdDebtSummary(borrower: string): Promise<Result<BorrowerUcdDebtSummary, SDKError>>;
144
155
  /**
145
156
  * Get active loans (status = ACTIVE)
146
157
  *
@@ -42,17 +42,23 @@ __export(sign_guard_exports, {
42
42
  authorizeWithServer: () => authorizeWithServer,
43
43
  broadcastSignedTx: () => broadcastSignedTx,
44
44
  buildAuthEnvelope: () => buildAuthEnvelope,
45
+ buildPsmApproveTx: () => buildPsmApproveTx,
46
+ buildPsmRedeemTx: () => buildPsmRedeemTx,
47
+ buildPsmSwapTx: () => buildPsmSwapTx,
45
48
  buildValidationContext: () => buildValidationContext,
46
49
  computeExtendFeeUpperBound: () => computeExtendFeeUpperBound,
47
50
  decodeSignable: () => decodeSignable,
48
51
  describeUnsignedTx: () => describeUnsignedTx,
49
52
  encodeSignable: () => encodeSignable,
53
+ executePsmPlan: () => executePsmPlan,
50
54
  fetchWithTimeout: () => fetchWithTimeout,
51
55
  isAuthFresh: () => isAuthFresh,
52
56
  isStrictCurrentQuantum: () => isStrictCurrentQuantum,
53
57
  modeForChainId: () => modeForChainId,
54
58
  nextQuantumTimestamp: () => nextQuantumTimestamp,
59
+ planPsmExchange: () => planPsmExchange,
55
60
  postJson: () => postJson,
61
+ psmExchangeReadsFromProvider: () => psmExchangeReadsFromProvider,
56
62
  randomNonceHex: () => randomNonceHex,
57
63
  readTxFeeFields: () => readTxFeeFields,
58
64
  signAndBroadcast: () => signAndBroadcast,
@@ -66,8 +72,12 @@ module.exports = __toCommonJS(sign_guard_exports);
66
72
 
67
73
  // src/utils/sign-guard/errors.ts
68
74
  var TxValidationError = class extends Error {
69
- constructor(msg) {
70
- super(`Refusing to sign server-returned transaction: ${msg}`);
75
+ /**
76
+ * @param subject What is being refused. Defaults to the server-returned transaction the
77
+ * validator exists for; client-built flows (the PSM exchange) name themselves instead.
78
+ */
79
+ constructor(msg, subject = "server-returned transaction") {
80
+ super(`Refusing to sign ${subject}: ${msg}`);
71
81
  this.name = "TxValidationError";
72
82
  }
73
83
  };
@@ -962,6 +972,129 @@ async function computeExtendFeeUpperBound(provider, contracts, positionId, selec
962
972
  const upperBoundFeeWei = ucdDebt * extensionFeeRateBps / 10000n;
963
973
  return { ucdDebt, extensionFeeRateBps, upperBoundFeeWei };
964
974
  }
975
+
976
+ // src/utils/sign-guard/psm-exchange.ts
977
+ var import_ethers6 = require("ethers");
978
+ function buildPsmApproveTx(p) {
979
+ return {
980
+ to: p.token,
981
+ data: encodeSignable("approve", [p.spender, p.amount]),
982
+ value: "0x0",
983
+ chainId: p.chainId
984
+ };
985
+ }
986
+ function buildPsmSwapTx(p) {
987
+ return {
988
+ to: p.psm,
989
+ data: encodeSignable("swap", [p.stablecoin, p.amountIn, p.minOut]),
990
+ value: "0x0",
991
+ chainId: p.chainId
992
+ };
993
+ }
994
+ function buildPsmRedeemTx(p) {
995
+ return {
996
+ to: p.psm,
997
+ data: encodeSignable("redeem", [p.stablecoin, p.ucdAmount, p.minOut]),
998
+ value: "0x0",
999
+ chainId: p.chainId
1000
+ };
1001
+ }
1002
+ var PSM_READS_ABI = ["function supportedStablecoins(address) view returns (bool)"];
1003
+ var ERC20_READS_ABI = ["function allowance(address owner, address spender) view returns (uint256)"];
1004
+ function psmExchangeReadsFromProvider(provider, psmAddress) {
1005
+ const psm = new import_ethers6.ethers.Contract(psmAddress, PSM_READS_ABI, provider);
1006
+ return {
1007
+ isStablecoinSupported: async (stablecoin) => await psm.getFunction("supportedStablecoins")(stablecoin),
1008
+ allowance: async (token, owner, spender) => await new import_ethers6.ethers.Contract(token, ERC20_READS_ABI, provider).getFunction("allowance")(owner, spender)
1009
+ };
1010
+ }
1011
+ var refuse = (direction, msg) => new TxValidationError(msg, `PSM ${direction}`);
1012
+ function requireAddress(direction, label, value) {
1013
+ if (!value || !import_ethers6.ethers.isAddress(value)) {
1014
+ throw refuse(direction, `${label} address is missing or invalid (${String(value)})`);
1015
+ }
1016
+ return import_ethers6.ethers.getAddress(value);
1017
+ }
1018
+ async function planPsmExchange(params) {
1019
+ const { direction, amountIn, minOut, vctx, reads } = params;
1020
+ const chainId = vctx.chainId;
1021
+ if (amountIn <= 0n) {
1022
+ throw refuse(direction, `amount must be greater than zero (got ${amountIn})`);
1023
+ }
1024
+ if (minOut <= 0n) {
1025
+ throw refuse(direction, `minimum-out floor must be greater than zero (got ${minOut}) \u2014 a zero floor is refused on-chain`);
1026
+ }
1027
+ const owner = requireAddress(direction, "owner", params.owner);
1028
+ const psm = requireAddress(direction, "SimplePSMV2", params.addresses.psm);
1029
+ const stablecoin = requireAddress(direction, "stablecoin", params.addresses.stablecoin);
1030
+ const isSwap = direction === "swap";
1031
+ const token = isSwap ? stablecoin : requireAddress(direction, "UCDToken", params.addresses.ucdToken);
1032
+ const spender = isSwap ? psm : requireAddress(direction, "UCDController", params.addresses.ucdController);
1033
+ if (!await reads.isStablecoinSupported(stablecoin)) {
1034
+ throw refuse(direction, `stablecoin ${stablecoin} is not supported by the PSM at ${psm}`);
1035
+ }
1036
+ const approveExpected = (amount) => isSwap ? { kind: "stablecoin-approve", tokenAddress: token, spender, amountUnits: amount.toString() } : { kind: "ucd-approve-controller", ucdTokenAddress: token, spender, amountWei: amount.toString() };
1037
+ const allowanceBefore = await reads.allowance(token, owner, spender);
1038
+ const approvals = [];
1039
+ if (allowanceBefore < amountIn) {
1040
+ if (allowanceBefore > 0n) {
1041
+ approvals.push({
1042
+ step: "reset-approve",
1043
+ tx: buildPsmApproveTx({ token, spender, amount: 0n, chainId }),
1044
+ expected: approveExpected(0n)
1045
+ });
1046
+ }
1047
+ approvals.push({
1048
+ step: "approve",
1049
+ tx: buildPsmApproveTx({ token, spender, amount: amountIn, chainId }),
1050
+ expected: approveExpected(amountIn)
1051
+ });
1052
+ }
1053
+ const exec = isSwap ? {
1054
+ step: "swap",
1055
+ tx: buildPsmSwapTx({ psm, stablecoin, amountIn, minOut, chainId }),
1056
+ expected: { kind: "psm-swap", psmAddress: psm, stablecoin, amountIn: amountIn.toString(), minOut: minOut.toString() }
1057
+ } : {
1058
+ step: "redeem",
1059
+ tx: buildPsmRedeemTx({ psm, stablecoin, ucdAmount: amountIn, minOut, chainId }),
1060
+ expected: {
1061
+ kind: "psm-redeem",
1062
+ psmAddress: psm,
1063
+ stablecoin,
1064
+ amountUcdWei: amountIn.toString(),
1065
+ minOut: minOut.toString()
1066
+ }
1067
+ };
1068
+ for (const leg of [...approvals, exec]) {
1069
+ validateUnsignedTx({ ...leg.tx }, leg.expected, vctx);
1070
+ }
1071
+ return { direction, owner, token, spender, amountIn, minOut, allowanceBefore, approvals, exec };
1072
+ }
1073
+ async function executePsmPlan(plan, signer, vctx) {
1074
+ const approvalHashes = [];
1075
+ const residual = () => approvalHashes.length === 0 ? "" : ` An approval already landed (tx ${approvalHashes[approvalHashes.length - 1]}): an allowance of exactly ${plan.amountIn} to ${plan.spender} stands. It authorizes only that amount; the next exchange uses or replaces it.`;
1076
+ const sendLeg = async (leg) => {
1077
+ const request = { to: leg.tx.to, data: leg.tx.data, value: 0n, chainId: leg.tx.chainId };
1078
+ validateUnsignedTx({ ...request }, leg.expected, vctx);
1079
+ const sent = await signer.sendTransaction(request);
1080
+ const receipt = await sent.wait();
1081
+ if (!receipt)
1082
+ throw new Error(`PSM ${leg.step} tx ${sent.hash} returned no receipt`);
1083
+ if (receipt.status !== 1)
1084
+ throw new Error(`PSM ${leg.step} tx ${sent.hash} reverted`);
1085
+ return { hash: sent.hash, blockNumber: receipt.blockNumber };
1086
+ };
1087
+ for (const leg of plan.approvals) {
1088
+ const landed = await sendLeg(leg).catch((e) => {
1089
+ throw new Error(`PSM ${leg.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
1090
+ });
1091
+ approvalHashes.push(landed.hash);
1092
+ }
1093
+ const exec = await sendLeg(plan.exec).catch((e) => {
1094
+ throw new Error(`PSM ${plan.exec.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
1095
+ });
1096
+ return { ...exec, approvalHashes };
1097
+ }
965
1098
  // Annotate the CommonJS export names for ESM import in node:
966
1099
  0 && (module.exports = {
967
1100
  AUTH_VALIDITY_WINDOWS,
@@ -986,17 +1119,23 @@ async function computeExtendFeeUpperBound(provider, contracts, positionId, selec
986
1119
  authorizeWithServer,
987
1120
  broadcastSignedTx,
988
1121
  buildAuthEnvelope,
1122
+ buildPsmApproveTx,
1123
+ buildPsmRedeemTx,
1124
+ buildPsmSwapTx,
989
1125
  buildValidationContext,
990
1126
  computeExtendFeeUpperBound,
991
1127
  decodeSignable,
992
1128
  describeUnsignedTx,
993
1129
  encodeSignable,
1130
+ executePsmPlan,
994
1131
  fetchWithTimeout,
995
1132
  isAuthFresh,
996
1133
  isStrictCurrentQuantum,
997
1134
  modeForChainId,
998
1135
  nextQuantumTimestamp,
1136
+ planPsmExchange,
999
1137
  postJson,
1138
+ psmExchangeReadsFromProvider,
1000
1139
  randomNonceHex,
1001
1140
  readTxFeeFields,
1002
1141
  signAndBroadcast,
@@ -1,7 +1,11 @@
1
1
  // src/utils/sign-guard/errors.ts
2
2
  var TxValidationError = class extends Error {
3
- constructor(msg) {
4
- super(`Refusing to sign server-returned transaction: ${msg}`);
3
+ /**
4
+ * @param subject What is being refused. Defaults to the server-returned transaction the
5
+ * validator exists for; client-built flows (the PSM exchange) name themselves instead.
6
+ */
7
+ constructor(msg, subject = "server-returned transaction") {
8
+ super(`Refusing to sign ${subject}: ${msg}`);
5
9
  this.name = "TxValidationError";
6
10
  }
7
11
  };
@@ -896,6 +900,129 @@ async function computeExtendFeeUpperBound(provider, contracts, positionId, selec
896
900
  const upperBoundFeeWei = ucdDebt * extensionFeeRateBps / 10000n;
897
901
  return { ucdDebt, extensionFeeRateBps, upperBoundFeeWei };
898
902
  }
903
+
904
+ // src/utils/sign-guard/psm-exchange.ts
905
+ import { ethers as ethers6 } from "ethers";
906
+ function buildPsmApproveTx(p) {
907
+ return {
908
+ to: p.token,
909
+ data: encodeSignable("approve", [p.spender, p.amount]),
910
+ value: "0x0",
911
+ chainId: p.chainId
912
+ };
913
+ }
914
+ function buildPsmSwapTx(p) {
915
+ return {
916
+ to: p.psm,
917
+ data: encodeSignable("swap", [p.stablecoin, p.amountIn, p.minOut]),
918
+ value: "0x0",
919
+ chainId: p.chainId
920
+ };
921
+ }
922
+ function buildPsmRedeemTx(p) {
923
+ return {
924
+ to: p.psm,
925
+ data: encodeSignable("redeem", [p.stablecoin, p.ucdAmount, p.minOut]),
926
+ value: "0x0",
927
+ chainId: p.chainId
928
+ };
929
+ }
930
+ var PSM_READS_ABI = ["function supportedStablecoins(address) view returns (bool)"];
931
+ var ERC20_READS_ABI = ["function allowance(address owner, address spender) view returns (uint256)"];
932
+ function psmExchangeReadsFromProvider(provider, psmAddress) {
933
+ const psm = new ethers6.Contract(psmAddress, PSM_READS_ABI, provider);
934
+ return {
935
+ isStablecoinSupported: async (stablecoin) => await psm.getFunction("supportedStablecoins")(stablecoin),
936
+ allowance: async (token, owner, spender) => await new ethers6.Contract(token, ERC20_READS_ABI, provider).getFunction("allowance")(owner, spender)
937
+ };
938
+ }
939
+ var refuse = (direction, msg) => new TxValidationError(msg, `PSM ${direction}`);
940
+ function requireAddress(direction, label, value) {
941
+ if (!value || !ethers6.isAddress(value)) {
942
+ throw refuse(direction, `${label} address is missing or invalid (${String(value)})`);
943
+ }
944
+ return ethers6.getAddress(value);
945
+ }
946
+ async function planPsmExchange(params) {
947
+ const { direction, amountIn, minOut, vctx, reads } = params;
948
+ const chainId = vctx.chainId;
949
+ if (amountIn <= 0n) {
950
+ throw refuse(direction, `amount must be greater than zero (got ${amountIn})`);
951
+ }
952
+ if (minOut <= 0n) {
953
+ throw refuse(direction, `minimum-out floor must be greater than zero (got ${minOut}) \u2014 a zero floor is refused on-chain`);
954
+ }
955
+ const owner = requireAddress(direction, "owner", params.owner);
956
+ const psm = requireAddress(direction, "SimplePSMV2", params.addresses.psm);
957
+ const stablecoin = requireAddress(direction, "stablecoin", params.addresses.stablecoin);
958
+ const isSwap = direction === "swap";
959
+ const token = isSwap ? stablecoin : requireAddress(direction, "UCDToken", params.addresses.ucdToken);
960
+ const spender = isSwap ? psm : requireAddress(direction, "UCDController", params.addresses.ucdController);
961
+ if (!await reads.isStablecoinSupported(stablecoin)) {
962
+ throw refuse(direction, `stablecoin ${stablecoin} is not supported by the PSM at ${psm}`);
963
+ }
964
+ const approveExpected = (amount) => isSwap ? { kind: "stablecoin-approve", tokenAddress: token, spender, amountUnits: amount.toString() } : { kind: "ucd-approve-controller", ucdTokenAddress: token, spender, amountWei: amount.toString() };
965
+ const allowanceBefore = await reads.allowance(token, owner, spender);
966
+ const approvals = [];
967
+ if (allowanceBefore < amountIn) {
968
+ if (allowanceBefore > 0n) {
969
+ approvals.push({
970
+ step: "reset-approve",
971
+ tx: buildPsmApproveTx({ token, spender, amount: 0n, chainId }),
972
+ expected: approveExpected(0n)
973
+ });
974
+ }
975
+ approvals.push({
976
+ step: "approve",
977
+ tx: buildPsmApproveTx({ token, spender, amount: amountIn, chainId }),
978
+ expected: approveExpected(amountIn)
979
+ });
980
+ }
981
+ const exec = isSwap ? {
982
+ step: "swap",
983
+ tx: buildPsmSwapTx({ psm, stablecoin, amountIn, minOut, chainId }),
984
+ expected: { kind: "psm-swap", psmAddress: psm, stablecoin, amountIn: amountIn.toString(), minOut: minOut.toString() }
985
+ } : {
986
+ step: "redeem",
987
+ tx: buildPsmRedeemTx({ psm, stablecoin, ucdAmount: amountIn, minOut, chainId }),
988
+ expected: {
989
+ kind: "psm-redeem",
990
+ psmAddress: psm,
991
+ stablecoin,
992
+ amountUcdWei: amountIn.toString(),
993
+ minOut: minOut.toString()
994
+ }
995
+ };
996
+ for (const leg of [...approvals, exec]) {
997
+ validateUnsignedTx({ ...leg.tx }, leg.expected, vctx);
998
+ }
999
+ return { direction, owner, token, spender, amountIn, minOut, allowanceBefore, approvals, exec };
1000
+ }
1001
+ async function executePsmPlan(plan, signer, vctx) {
1002
+ const approvalHashes = [];
1003
+ const residual = () => approvalHashes.length === 0 ? "" : ` An approval already landed (tx ${approvalHashes[approvalHashes.length - 1]}): an allowance of exactly ${plan.amountIn} to ${plan.spender} stands. It authorizes only that amount; the next exchange uses or replaces it.`;
1004
+ const sendLeg = async (leg) => {
1005
+ const request = { to: leg.tx.to, data: leg.tx.data, value: 0n, chainId: leg.tx.chainId };
1006
+ validateUnsignedTx({ ...request }, leg.expected, vctx);
1007
+ const sent = await signer.sendTransaction(request);
1008
+ const receipt = await sent.wait();
1009
+ if (!receipt)
1010
+ throw new Error(`PSM ${leg.step} tx ${sent.hash} returned no receipt`);
1011
+ if (receipt.status !== 1)
1012
+ throw new Error(`PSM ${leg.step} tx ${sent.hash} reverted`);
1013
+ return { hash: sent.hash, blockNumber: receipt.blockNumber };
1014
+ };
1015
+ for (const leg of plan.approvals) {
1016
+ const landed = await sendLeg(leg).catch((e) => {
1017
+ throw new Error(`PSM ${leg.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
1018
+ });
1019
+ approvalHashes.push(landed.hash);
1020
+ }
1021
+ const exec = await sendLeg(plan.exec).catch((e) => {
1022
+ throw new Error(`PSM ${plan.exec.step} failed: ${e instanceof Error ? e.message : String(e)}.${residual()}`);
1023
+ });
1024
+ return { ...exec, approvalHashes };
1025
+ }
899
1026
  export {
900
1027
  AUTH_VALIDITY_WINDOWS,
901
1028
  BROADCAST_PATH,
@@ -919,17 +1046,23 @@ export {
919
1046
  authorizeWithServer,
920
1047
  broadcastSignedTx,
921
1048
  buildAuthEnvelope,
1049
+ buildPsmApproveTx,
1050
+ buildPsmRedeemTx,
1051
+ buildPsmSwapTx,
922
1052
  buildValidationContext,
923
1053
  computeExtendFeeUpperBound,
924
1054
  decodeSignable,
925
1055
  describeUnsignedTx,
926
1056
  encodeSignable,
1057
+ executePsmPlan,
927
1058
  fetchWithTimeout,
928
1059
  isAuthFresh,
929
1060
  isStrictCurrentQuantum,
930
1061
  modeForChainId,
931
1062
  nextQuantumTimestamp,
1063
+ planPsmExchange,
932
1064
  postJson,
1065
+ psmExchangeReadsFromProvider,
933
1066
  randomNonceHex,
934
1067
  readTxFeeFields,
935
1068
  signAndBroadcast,
@@ -22,14 +22,22 @@ export declare function safeGetBitcoinAddressesFromPkp(pkpId: string, maxRetries
22
22
  testnet: string;
23
23
  regtest: string;
24
24
  }>;
25
+ type BitcoinAddressNetwork = 'mainnet' | 'testnet' | 'regtest';
25
26
  /**
26
- * Safely validate and format a Bitcoin address
27
- * @param address Bitcoin address to validate
28
- * @param network Network type for validation
29
- * @returns Validated address
30
- * @throws Clear error message if validation fails
27
+ * Validate a Bitcoin address for the network the caller is operating on.
28
+ *
29
+ * Runs on the BTC withdrawal path before any Lit Action, authorization or signature, so
30
+ * it verifies the CHECKSUM (base58check / bech32 / bech32m, via `BitcoinUtils`) and then
31
+ * the network. BTC sent to a mistyped or wrong-network address is unrecoverable; the
32
+ * previous implementation checked the prefix only, so a one-character typo passed.
33
+ *
34
+ * `network` is required. It used to default to 'regtest', which would have validated a
35
+ * mainnet caller's address against the wrong chain without saying so.
36
+ *
37
+ * @returns the address unchanged
38
+ * @throws if the address is malformed, fails its checksum, or is not on `network`
31
39
  */
32
- export declare function safeValidateBitcoinAddress(address: string, network?: 'mainnet' | 'testnet' | 'regtest'): string;
40
+ export declare function safeValidateBitcoinAddress(address: string, network: BitcoinAddressNetwork): string;
33
41
  /**
34
42
  * Canonicalize a Bitcoin address so that the SAME string is used on both the
35
43
  * approve path (registry.addAddress) and the withdraw path. The on-chain
@@ -49,3 +57,4 @@ export declare function normalizeBitcoinAddress(address: string): string;
49
57
  * @throws Clear error message if validation fails
50
58
  */
51
59
  export declare function safeValidatePositionId(positionId: string): string;
60
+ export {};
@@ -18,3 +18,42 @@ export declare function mintAgentPkp(params: {
18
18
  authHeader?: () => Promise<Record<string, string>> | Record<string, string>;
19
19
  timeoutMs?: number;
20
20
  }): Promise<string>;
21
+ /** `AgentStatus` in `IAgentDelegateRegistryBase.sol`. */
22
+ export declare const AGENT_STATUS: {
23
+ readonly None: 0;
24
+ readonly Active: 1;
25
+ readonly Revoked: 2;
26
+ };
27
+ /** How the enable-path binds the borrower's agent. `reuse` needs no mint and no tx. */
28
+ export type AgentBindingPlan = {
29
+ kind: "reuse";
30
+ agent: string;
31
+ } | {
32
+ kind: "register";
33
+ } | {
34
+ kind: "rotate";
35
+ };
36
+ /**
37
+ * Decide which registry call binds a borrower's agent — BEFORE a PKP is minted.
38
+ *
39
+ * The registry does not change `status` on expiry: an expired agent is still
40
+ * `status == Active`, only `validUntil` has passed. So `isAgentActive()` is false, yet
41
+ * `registerAgent` reverts `AgentIsActiveUseRotate()` — rotation is the renewal path, and it
42
+ * needs a NEW agent address (AR-1 burns an address on first bind). Treating "not active" as
43
+ * "register" minted a billed PKP and then reverted, on every attempt, once an agent expired.
44
+ *
45
+ * `isAgentActive` also folds in the registry's pause. An UNEXPIRED `Active` record that reads
46
+ * inactive means paused: both `registerAgent` and `rotateAgent` are `whenNotPaused`, so this
47
+ * throws rather than let the caller mint a PKP for a transaction that cannot land.
48
+ *
49
+ * @param nowSeconds chain time (latest block timestamp), not the local clock
50
+ */
51
+ export declare function planAgentBinding(params: {
52
+ isActive: boolean;
53
+ record: {
54
+ agent: string;
55
+ validUntil: bigint | number;
56
+ status: bigint | number;
57
+ };
58
+ nowSeconds: number;
59
+ }): AgentBindingPlan;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * An explicit `chainId` is a claim until the provider confirms it.
3
+ *
4
+ * The SDK resolves its contract addresses, validators and subgraph from `chainId`. If the
5
+ * provider (the wallet) is actually on another chain, every address it is about to call or
6
+ * sign for belongs to the wrong network. So the claim is checked once, at initialisation.
7
+ *
8
+ * Returns the mismatch as a message (for the caller's `Result`), or `null` when the provider
9
+ * agrees. A provider that cannot report its network is NOT waved through: its error
10
+ * propagates unchanged, so the caller sees the real cause and no SDK exists to sign with.
11
+ */
12
+ export declare function describeProviderChainMismatch(provider: {
13
+ getNetwork(): Promise<{
14
+ chainId: bigint | number;
15
+ }>;
16
+ }, chainId: number): Promise<string | null>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pure helpers behind `getBorrowerUcdDebtSummary`.
3
+ *
4
+ * Kept free of the graph client so the arithmetic and the pagination loop are
5
+ * unit-testable without a subgraph: the loop takes a page fetcher, the builder
6
+ * takes rows. Nothing here reads the chain — every figure is the INDEXED view
7
+ * (the subgraph lags writes), which is why the summary carries `source: "subgraph"`
8
+ * and must never size a transaction.
9
+ */
10
+ import { LoanStatus } from "../types/loanStatus";
11
+ import type { BorrowerUcdDebtSummary } from "../interfaces/chunks/loan-operations.i";
12
+ /** The subgraph fields a debt summary needs, exactly as the indexer serialises them. */
13
+ export interface BorrowerDebtRow {
14
+ id: string;
15
+ /** Subgraph enum label, e.g. "ACTIVE" — NOT the numeric LoanStatus. */
16
+ status: string;
17
+ /** Wei (18 decimals) as the subgraph's BigInt decimal string. */
18
+ ucdDebt: string;
19
+ }
20
+ /** One page per round trip at The Graph's ceiling; nothing to tune. */
21
+ export declare const BORROWER_DEBT_ROWS_PAGE_SIZE = 1000;
22
+ /** 10 × 1000 = 10_000 positions. Past this the loop THROWS — never a partial total. */
23
+ export declare const BORROWER_DEBT_ROWS_MAX_PAGES = 10;
24
+ export type BorrowerDebtPageFetcher = (skip: number, first: number) => Promise<BorrowerDebtRow[]>;
25
+ /**
26
+ * Page `fetchPage` from skip 0 in `BORROWER_DEBT_ROWS_PAGE_SIZE` steps until a
27
+ * short page. A page failure propagates untouched (the caller wraps it with the
28
+ * cause); the page cap throws rather than returning what was collected so far.
29
+ */
30
+ export declare function collectBorrowerDebtRows(fetchPage: BorrowerDebtPageFetcher): Promise<BorrowerDebtRow[]>;
31
+ /**
32
+ * Subgraph `Position.status` is the enum LABEL ("ACTIVE"). Convert it explicitly
33
+ * and throw on anything else — the `as LoanStatus` cast elsewhere in the SDK only
34
+ * pretends the string is a number.
35
+ */
36
+ export declare function parseSubgraphLoanStatus(label: unknown): LoanStatus;
37
+ /** Exact 18-decimal rendering with trailing zeros trimmed: "5", "21.93", "0". */
38
+ export declare function weiToHumanUcdString(wei: bigint): string;
39
+ /**
40
+ * Sum every row regardless of status (terminal rows are expected to carry 0; a
41
+ * non-zero terminal row is an indexer defect that should show up in the total,
42
+ * not be filtered away) and histogram the statuses.
43
+ */
44
+ export declare function buildBorrowerUcdDebtSummary(borrower: string, rows: ReadonlyArray<BorrowerDebtRow>, fetchedAt?: number): BorrowerUcdDebtSummary;