@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.
- package/browser/dist/browser.js +1 -1
- package/dist/graphs/diamond-hands.d.ts +14 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1138 -182
- package/dist/index.mjs +1073 -117
- package/dist/interfaces/chunks/loan-operations.i.d.ts +34 -0
- package/dist/modules/diamond-hands-sdk.d.ts +22 -5
- package/dist/modules/loan/loan-query.module.d.ts +12 -1
- package/dist/sign-guard.js +141 -2
- package/dist/sign-guard.mjs +135 -2
- package/dist/utils/address-conversion.utils.d.ts +15 -6
- package/dist/utils/agent-delegation.utils.d.ts +39 -0
- package/dist/utils/assert-provider-chain.d.ts +16 -0
- package/dist/utils/borrower-ucd-debt-summary.d.ts +44 -0
- package/dist/utils/concurrency-limiter.utils.d.ts +59 -0
- package/dist/utils/lit-action-chain-name.d.ts +14 -0
- package/dist/utils/loan-helpers.utils.d.ts +115 -0
- package/dist/utils/sign-guard/errors.d.ts +5 -1
- package/dist/utils/sign-guard/index.d.ts +1 -0
- package/dist/utils/sign-guard/psm-exchange.d.ts +122 -0
- package/package.json +3 -4
|
@@ -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
|
-
*
|
|
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;
|
|
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
|
-
*
|
|
996
|
-
* UCDToken.burn(from, amount) calls
|
|
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
|
|
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
|
*
|
package/dist/sign-guard.js
CHANGED
|
@@ -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
|
-
|
|
70
|
-
|
|
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,
|
package/dist/sign-guard.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// src/utils/sign-guard/errors.ts
|
|
2
2
|
var TxValidationError = class extends Error {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
|
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;
|