@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.
- package/browser/dist/browser.js +1 -1
- package/dist/deployments.js +8 -8
- package/dist/deployments.mjs +8 -8
- package/dist/index.js +46 -12
- package/dist/index.mjs +46 -12
- package/dist/interfaces/chunks/config.i.d.ts +19 -0
- package/dist/safe-delegation.js +8 -8
- package/dist/safe-delegation.mjs +8 -8
- package/dist/sign-guard.d.ts +26 -0
- package/dist/sign-guard.js +1008 -0
- package/dist/sign-guard.mjs +941 -0
- package/dist/utils/borrower-authorization-712.d.ts +1 -1
- package/dist/utils/eip712-login.d.ts +34 -1
- package/dist/utils/server-session-store.d.ts +15 -0
- package/dist/utils/server-session.d.ts +13 -0
- package/dist/utils/sign-guard/errors.d.ts +9 -0
- package/dist/utils/sign-guard/extend-fee-bound.d.ts +15 -0
- package/dist/utils/sign-guard/fee-ceiling.d.ts +40 -0
- package/dist/utils/sign-guard/index.d.ts +9 -0
- package/dist/utils/sign-guard/lit-ops-client.d.ts +107 -0
- package/dist/utils/sign-guard/op-envelope.d.ts +88 -0
- package/dist/utils/sign-guard/op-specs.d.ts +51 -0
- package/dist/utils/sign-guard/sign-and-broadcast.d.ts +56 -0
- package/dist/utils/sign-guard/signable-functions.d.ts +41 -0
- package/dist/utils/sign-guard/tx-validator.d.ts +148 -0
- package/package.json +25 -3
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
// src/utils/sign-guard/errors.ts
|
|
2
|
+
var TxValidationError = class extends Error {
|
|
3
|
+
constructor(msg) {
|
|
4
|
+
super(`Refusing to sign server-returned transaction: ${msg}`);
|
|
5
|
+
this.name = "TxValidationError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/utils/sign-guard/signable-functions.ts
|
|
10
|
+
import { ethers } from "ethers";
|
|
11
|
+
var SIGNABLE_FUNCTIONS = [
|
|
12
|
+
"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)",
|
|
13
|
+
// `makePayment` is the ONLY repayment entry point. A `repayPosition` entry
|
|
14
|
+
// used to sit here; it is not a function on any deployed contract — the
|
|
15
|
+
// compiled PositionManager ABI has no `repay*` function at all, and the name
|
|
16
|
+
// survives only in `archive/` prototypes. Nothing in lit-ops-server, the
|
|
17
|
+
// lit-actions, or the SDK ever emitted it, and AgentModule never whitelisted
|
|
18
|
+
// it. A signable-function whitelist should not carry calldata shapes the
|
|
19
|
+
// protocol cannot execute.
|
|
20
|
+
"function makePayment(bytes32 positionId, uint256 paymentAmount, uint256 quantumTimestamp, uint256 btcPrice, bytes calldata paymentValidatorSignature) external returns (bool)",
|
|
21
|
+
"function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, uint256 proRataRenewalFee, bytes calldata extensionValidatorSignature) external returns (bool)",
|
|
22
|
+
"function extendPosition(bytes32 positionId, uint256 selectedTerm, uint256 quantumTimestamp, uint256 btcPrice, uint256 availableBTCBalance, bytes calldata extensionValidatorSignature) external returns (bool)",
|
|
23
|
+
// Struct-version: matches the actual on-chain function signature and the
|
|
24
|
+
// SDK's calldata builder. An earlier 3-arg declaration was stale and would
|
|
25
|
+
// silently fail to decode real `withdrawBTC` calldata. networkFee is NOT a
|
|
26
|
+
// contract param — it is an off-chain Phase-2 BTC tx fee, validated elsewhere.
|
|
27
|
+
"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)",
|
|
28
|
+
// `cancelStaleSpendByOwner` removed in audit #62210 follow-up — the on-chain
|
|
29
|
+
// function reverts permanently. The trustless replacement
|
|
30
|
+
// (`cancelStaleSpendWithProof` on BTCSpendAuthorizer) lands via a separate
|
|
31
|
+
// entry when its LIT Action ships.
|
|
32
|
+
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
33
|
+
"function setPositionDelegate(bytes32 positionId, address newDelegate) external",
|
|
34
|
+
// Position creation. Signed opaquely by the SDK (like withdrawBTC), so
|
|
35
|
+
// without an entry here it decoded as "<unparseable>" and the CLI's
|
|
36
|
+
// WithdrawGuard waved it through — the flow that locks the user's BTC
|
|
37
|
+
// collateral had no client-side pin at all.
|
|
38
|
+
"function createPosition(bytes32 pkpId, bytes calldata validatorSignature, string mainnetVaultAddress, string regtestVaultAddress, uint256 selectedTermMonths, uint256 validatorVersion, bytes calldata pkpPublicKey) external returns (bytes32 positionId)",
|
|
39
|
+
// SimplePSMV2 — the PSM exchange pair. Direct EOA calls, never routed through
|
|
40
|
+
// AgentModule; the parity test documents them as client-only.
|
|
41
|
+
"function swap(address stablecoin, uint256 amountIn, uint256 minUcdOut) external returns (uint256)",
|
|
42
|
+
"function redeem(address stablecoin, uint256 ucdAmount, uint256 minStablecoinOut) external returns (uint256)",
|
|
43
|
+
// BitcoinWithdrawalAddressRegistry — `msg.sender` must be the borrower, so
|
|
44
|
+
// this too is client-only and never module-whitelisted.
|
|
45
|
+
"function addAddress(string btcAddress) external"
|
|
46
|
+
];
|
|
47
|
+
var IFACE = new ethers.Interface(SIGNABLE_FUNCTIONS);
|
|
48
|
+
function encodeSignable(fn, args) {
|
|
49
|
+
return IFACE.encodeFunctionData(fn, args);
|
|
50
|
+
}
|
|
51
|
+
function decodeSignable(data) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = IFACE.parseTransaction({ data });
|
|
54
|
+
if (!parsed) {
|
|
55
|
+
throw new TxValidationError(
|
|
56
|
+
`unsignedTx.data does not decode against the signable-function whitelist`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return { name: parsed.name, args: parsed.args };
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e instanceof TxValidationError)
|
|
62
|
+
throw e;
|
|
63
|
+
throw new TxValidationError(
|
|
64
|
+
`unsignedTx.data does not decode against the signable-function whitelist: ${e.message}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function signableFunctionName(data) {
|
|
69
|
+
try {
|
|
70
|
+
const parsed = IFACE.parseTransaction({ data });
|
|
71
|
+
return parsed ? parsed.name : "<unparseable>";
|
|
72
|
+
} catch {
|
|
73
|
+
return "<unparseable>";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function signableSelectors() {
|
|
77
|
+
const out = /* @__PURE__ */ new Map();
|
|
78
|
+
IFACE.forEachFunction((fn) => out.set(fn.selector, fn.format("sighash")));
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/utils/sign-guard/fee-ceiling.ts
|
|
83
|
+
import { ethers as ethers2 } from "ethers";
|
|
84
|
+
var MAX_GAS_LIMIT = 5000000n;
|
|
85
|
+
var MAX_FEE_PER_GAS_WEI = 2000000000000n;
|
|
86
|
+
var DEFAULT_MAX_TX_FEE_WEI = 250000000000000000n;
|
|
87
|
+
var DEFAULT_FEE_CEILING = {
|
|
88
|
+
maxGasLimit: MAX_GAS_LIMIT,
|
|
89
|
+
maxFeePerGasWei: MAX_FEE_PER_GAS_WEI,
|
|
90
|
+
maxTxFeeWei: DEFAULT_MAX_TX_FEE_WEI
|
|
91
|
+
};
|
|
92
|
+
function fmtEth(wei) {
|
|
93
|
+
return `${ethers2.formatEther(wei)} ETH`;
|
|
94
|
+
}
|
|
95
|
+
function fmtGwei(wei) {
|
|
96
|
+
return `${ethers2.formatUnits(wei, "gwei")} gwei`;
|
|
97
|
+
}
|
|
98
|
+
function worstCaseFeeWei(gasLimit, maxFeePerGas) {
|
|
99
|
+
if (gasLimit == null || maxFeePerGas == null)
|
|
100
|
+
return null;
|
|
101
|
+
return gasLimit * maxFeePerGas;
|
|
102
|
+
}
|
|
103
|
+
function assertFeeCeiling(gasLimit, maxFeePerGas, raise, ceiling = DEFAULT_FEE_CEILING) {
|
|
104
|
+
if (gasLimit != null && gasLimit > ceiling.maxGasLimit) {
|
|
105
|
+
raise(`gasLimit ${gasLimit} exceeds the signing ceiling of ${ceiling.maxGasLimit}`);
|
|
106
|
+
}
|
|
107
|
+
if (maxFeePerGas != null && maxFeePerGas > ceiling.maxFeePerGasWei) {
|
|
108
|
+
raise(
|
|
109
|
+
`maxFeePerGas ${fmtGwei(maxFeePerGas)} exceeds the signing ceiling of ${fmtGwei(ceiling.maxFeePerGasWei)}`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const worst = worstCaseFeeWei(gasLimit, maxFeePerGas);
|
|
113
|
+
if (worst != null && worst > ceiling.maxTxFeeWei) {
|
|
114
|
+
raise(
|
|
115
|
+
`worst-case transaction fee ${fmtEth(worst)} (gasLimit ${gasLimit} \xD7 ${fmtGwei(maxFeePerGas)}) exceeds the signing ceiling of ${fmtEth(ceiling.maxTxFeeWei)}.${ceiling.raiseHint ? ` ${ceiling.raiseHint}` : ""}`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/utils/sign-guard/tx-validator.ts
|
|
121
|
+
import { ethers as ethers3 } from "ethers";
|
|
122
|
+
|
|
123
|
+
// src/constants/chunks/deployment-addresses.ts
|
|
124
|
+
var SEPOLIA_CONTRACTS = {
|
|
125
|
+
UpgradeValidator: "0x592e650edD9bA26ecc407989fdd6F9cdcDeC27A6",
|
|
126
|
+
UCDToken: "0xdd817Ff47C9b8f81335Ac579895D8EA321Fa86Df",
|
|
127
|
+
UCDController: "0x360Bde4e489ee94E26601B206932De09EE60FF04",
|
|
128
|
+
PriceFeedConsumer: "0xb3Dc64501935a6f0334779126Ee37bE463cee318",
|
|
129
|
+
PositionManagerCoreModule: "0xfFAA39a08887FeFB697eBC8691A8630CbCC33391",
|
|
130
|
+
TermManagerModule: "0x94Adc6F9558368B1482197584f1fc7133Ecf835b",
|
|
131
|
+
LoanOperationsManagerModule: "0x3e05e760a87715207b7F4e8fD35EF7559BcA72C0",
|
|
132
|
+
CollateralManagerModule: "0x967E92c976Fc0fa8268D37D4d6B7245d912aA7dB",
|
|
133
|
+
LiquidationManagerModule: "0xC4AB00f39e7ceD812F5C67058C9fa44e15d40e76",
|
|
134
|
+
CircuitBreakerModule: "0x3D0B2cAE481821E57BA9CC3807bCC0d0D7F18bd8",
|
|
135
|
+
AdminModule: "0xAcd1f07915b17CA3727e7fE89BdF618A1545a1ed",
|
|
136
|
+
PositionManagerViews: "0x3b970E41a0668508B4B2ACb83653b2A402F745AE",
|
|
137
|
+
PositionManager: "0x68e0507D982AEBF1D1C1Af8FBbd842327e2deeFF",
|
|
138
|
+
OperationAuthorizationRegistry: "0x75f36E772df9616d40b02092642744D6fd702Df8",
|
|
139
|
+
PKPValidation: "0x8a1365A974F91e92D2dF5C51216EFCD408e209A8",
|
|
140
|
+
BTCSpendAuthorizer: "0xd7E9Eb5eE53f1d9ee55Dc162bBC7486bE437B6bD",
|
|
141
|
+
LITActionValidator: "0x54eECd7C90F8A8fac27749Ba30BbE8AFBAccc856",
|
|
142
|
+
SimplePSMV2: "0x19EcF7BA26e054fd4Ff06009A03070103dbBB058",
|
|
143
|
+
BitcoinProviderRegistry: "0xbfA0c48B070D0a9A9CD27469AeacFD2d4261cEE2",
|
|
144
|
+
ContractVersionRegistry: "0x97E43AA6aFF2C0cf659d3aE501658D2Ef6777522",
|
|
145
|
+
mockUsdcToken: "0x96409A98e0D322d6566b7F44a0fAbD1BD4ce2AEc",
|
|
146
|
+
mockUsdtToken: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
|
|
147
|
+
PositionDelegateRegistry: "0x482369De283622E7b05731875ec137d4E3D208F9",
|
|
148
|
+
FeeRecipientRegistry: "0x95795F8403DDb629E8527B2934C4e71f5fC0C374",
|
|
149
|
+
BitcoinWithdrawalAddressRegistry: "0x6376D392097F0A81e51BD83960A97EA20eaf1dbe",
|
|
150
|
+
CarryAgentRegistry: "0x5595CB538D44E6Cf179D20364fD631D0477e69f7",
|
|
151
|
+
AgentDelegationRegistry: "0x6AE7fc6bE0925126b9D25b1B7Ce5AD5d127ec025",
|
|
152
|
+
DHAgentDelegate: "0xa5A14489ae883960df6E1463A41a47aBE9Ecc5AF",
|
|
153
|
+
AgentModuleFactory: "0x1Fd3aCa2C030Cf77ce28Af9694D3Bb3034ACa2E5"
|
|
154
|
+
};
|
|
155
|
+
var MAINNET_CONTRACTS = {
|
|
156
|
+
MessageHashBuilder: "0xaDE074023F9e87f6AEaF679e36B5d7B93FA22269",
|
|
157
|
+
LITActionValidator: "0xD2c5d59c2CC5c409AC65ce0A817236aeC6869512",
|
|
158
|
+
UpgradeValidator: "0xC46Ae71F22e3741A6CF46E399aE914C4868fEbed",
|
|
159
|
+
UCDToken: "0xaAaB6B05E5EecB015898483d4e6703d7F218850b",
|
|
160
|
+
UCDController: "0x4b4f8063048bCD59D469219E3f07C6aaDF4f6157",
|
|
161
|
+
PositionManagerCoreModule: "0xd895B685914c1BE4e1c7aab9f89a90c203DE8d22",
|
|
162
|
+
TermManagerModule: "0xDB99CdF883ffa38acD099bC39F8d1f4508c9239E",
|
|
163
|
+
LoanOperationsManagerModule: "0x2561974A40F1AEed1251d0A090A83cF8536FB9E9",
|
|
164
|
+
BTCSpendAuthorizer: "0xfBbF108486fa07F1B06a811C5b66ed68DD9c508a",
|
|
165
|
+
CollateralManagerModule: "0x2Eb74FEc478218f5C260aAc78Bb8eC297F9d7135",
|
|
166
|
+
LiquidationManagerModule: "0x74B28695352E7bf736D6c9F76ccb7B4439477De9",
|
|
167
|
+
CircuitBreakerModule: "0xF7b545d3a4Ee056D06909AB367E2F7bFFE3cB1BC",
|
|
168
|
+
AdminModule: "0x0023B71B586795eE5fa4E83E384345C1EC8Bb946",
|
|
169
|
+
PositionManagerViews: "0x8B57954eA338c6A6bfF1457f71080e25baFF3937",
|
|
170
|
+
PositionManager: "0x12A1836e56139ccaD8722B89519d84233efc224B",
|
|
171
|
+
OperationAuthorizationRegistry: "0xD4505645b724754f5170a2027804a4265e860d44",
|
|
172
|
+
ContractVersionRegistry: "0x5e18f77d01940dA5B1D986D373D9A6ad0767008d",
|
|
173
|
+
FeeRecipientRegistry: "0x8fbCAAdA198E1C246ecEb2fB021C5A3d8050c6B3",
|
|
174
|
+
PositionDelegateRegistry: "0xf0aa2678aD4F60f49e8F7725089B5E86B7F8FE4C",
|
|
175
|
+
BitcoinProviderRegistry: "0x4ab3Fb96e02893Ed6FAa61bad1918390c8DbbFFC",
|
|
176
|
+
BitcoinWithdrawalAddressRegistry: "0xf667f77FDba7458345f227eB6Bfe4148243E7270",
|
|
177
|
+
SimplePSMV2: "0xfE0C14B4E614f40Bf58845b4B04CeD17fd116532",
|
|
178
|
+
PKPValidation: "0x29922d3ccb86516f6C24F89f1eb3068655ca7Fa0",
|
|
179
|
+
AgentDelegationRegistry: "0x54853b7E91ce2Be34ce65655D80c4447e67187Fa",
|
|
180
|
+
DHAgentDelegate: "0x70232d0caBe529e29a01A442423d7e03BBd414c8"
|
|
181
|
+
};
|
|
182
|
+
var LOCALHOST_CONTRACTS = {
|
|
183
|
+
MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
|
|
184
|
+
LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
|
|
185
|
+
UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
|
|
186
|
+
UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
187
|
+
UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
188
|
+
PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
|
|
189
|
+
TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
|
|
190
|
+
LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
|
|
191
|
+
BTCSpendAuthorizer: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d",
|
|
192
|
+
CollateralManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
|
|
193
|
+
LiquidationManagerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
|
|
194
|
+
CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
|
|
195
|
+
AdminModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
|
196
|
+
PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
|
197
|
+
PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
|
|
198
|
+
OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
|
|
199
|
+
PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
|
|
200
|
+
};
|
|
201
|
+
var ALL_CONTRACTS = {
|
|
202
|
+
sepolia: SEPOLIA_CONTRACTS,
|
|
203
|
+
mainnet: MAINNET_CONTRACTS,
|
|
204
|
+
localhost: LOCALHOST_CONTRACTS
|
|
205
|
+
};
|
|
206
|
+
function getContractsByNetwork(network) {
|
|
207
|
+
return ALL_CONTRACTS[network] || null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/utils/sign-guard/tx-validator.ts
|
|
211
|
+
function buildValidationContext(network, chainId, feeCeiling) {
|
|
212
|
+
const sdkKey = chainId === 31337 ? "localhost" : chainId === 11155111 ? "sepolia" : network.toLowerCase();
|
|
213
|
+
const contracts = getContractsByNetwork(sdkKey);
|
|
214
|
+
if (!contracts) {
|
|
215
|
+
throw new TxValidationError(
|
|
216
|
+
`no SDK contract registry for network='${network}' chainId=${chainId} (sdkKey='${sdkKey}')`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return feeCeiling ? { chainId, network, contracts, feeCeiling } : { chainId, network, contracts };
|
|
220
|
+
}
|
|
221
|
+
function eqAddr(a, b) {
|
|
222
|
+
if (!a || !b)
|
|
223
|
+
return false;
|
|
224
|
+
try {
|
|
225
|
+
return ethers3.getAddress(a) === ethers3.getAddress(b);
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function readToAddress(tx) {
|
|
231
|
+
const to = tx["to"];
|
|
232
|
+
if (typeof to !== "string") {
|
|
233
|
+
throw new TxValidationError(`unsignedTx.to is missing or not a string`);
|
|
234
|
+
}
|
|
235
|
+
return to;
|
|
236
|
+
}
|
|
237
|
+
function readData(tx) {
|
|
238
|
+
const data = tx["data"];
|
|
239
|
+
if (typeof data !== "string" || !data.startsWith("0x")) {
|
|
240
|
+
throw new TxValidationError(`unsignedTx.data missing or malformed`);
|
|
241
|
+
}
|
|
242
|
+
return data;
|
|
243
|
+
}
|
|
244
|
+
function readChainId(tx) {
|
|
245
|
+
const c = tx["chainId"];
|
|
246
|
+
if (c == null)
|
|
247
|
+
return null;
|
|
248
|
+
if (typeof c === "number")
|
|
249
|
+
return c;
|
|
250
|
+
if (typeof c === "string")
|
|
251
|
+
return parseInt(c.startsWith("0x") ? c.slice(2) : c, c.startsWith("0x") ? 16 : 10);
|
|
252
|
+
if (typeof c === "bigint")
|
|
253
|
+
return Number(c);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function readOptionalBigInt(tx, key) {
|
|
257
|
+
const v = tx[key];
|
|
258
|
+
if (v == null)
|
|
259
|
+
return null;
|
|
260
|
+
try {
|
|
261
|
+
if (typeof v === "bigint")
|
|
262
|
+
return v;
|
|
263
|
+
if (typeof v === "number")
|
|
264
|
+
return BigInt(v);
|
|
265
|
+
if (typeof v === "string")
|
|
266
|
+
return BigInt(v);
|
|
267
|
+
return BigInt(v.toString());
|
|
268
|
+
} catch {
|
|
269
|
+
throw new TxValidationError(`unsignedTx.${key} is present but not a valid integer`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function readValue(tx) {
|
|
273
|
+
const v = tx["value"];
|
|
274
|
+
if (v == null)
|
|
275
|
+
return 0n;
|
|
276
|
+
if (typeof v === "string")
|
|
277
|
+
return BigInt(v);
|
|
278
|
+
if (typeof v === "number")
|
|
279
|
+
return BigInt(v);
|
|
280
|
+
if (typeof v === "bigint")
|
|
281
|
+
return v;
|
|
282
|
+
const maybe = v;
|
|
283
|
+
if (typeof maybe.toBigInt === "function")
|
|
284
|
+
return maybe.toBigInt();
|
|
285
|
+
if (typeof maybe.toString === "function")
|
|
286
|
+
return BigInt(maybe.toString());
|
|
287
|
+
throw new TxValidationError(`unsignedTx.value has unrecognized type`);
|
|
288
|
+
}
|
|
289
|
+
function normalizePositionId(id) {
|
|
290
|
+
const hex = id.startsWith("0x") ? id : `0x${id}`;
|
|
291
|
+
return ethers3.zeroPadValue(hex, 32).toLowerCase();
|
|
292
|
+
}
|
|
293
|
+
function requirePositionId(actual, expected) {
|
|
294
|
+
const txPositionId = String(actual).toLowerCase();
|
|
295
|
+
if (txPositionId !== normalizePositionId(expected)) {
|
|
296
|
+
throw new TxValidationError(
|
|
297
|
+
`positionId mismatch \u2014 tx ${txPositionId}, expected ${normalizePositionId(expected)}`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function validateUnsignedTx(unsignedTx, expected, vctx) {
|
|
302
|
+
const txChainId = readChainId(unsignedTx);
|
|
303
|
+
if (txChainId == null) {
|
|
304
|
+
throw new TxValidationError(
|
|
305
|
+
`unsignedTx.chainId is missing \u2014 refusing to sign a transaction with no chain binding (client is on ${vctx.chainId})`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
if (txChainId !== vctx.chainId) {
|
|
309
|
+
throw new TxValidationError(
|
|
310
|
+
`chainId mismatch \u2014 tx says ${txChainId}, client is on ${vctx.chainId}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
const value = readValue(unsignedTx);
|
|
314
|
+
if (value !== 0n) {
|
|
315
|
+
throw new TxValidationError(
|
|
316
|
+
`unsignedTx.value is non-zero (${value}); none of the protocol functions accept ETH`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
assertFeeCeiling(
|
|
320
|
+
readOptionalBigInt(unsignedTx, "gasLimit"),
|
|
321
|
+
readOptionalBigInt(unsignedTx, "maxFeePerGas") ?? readOptionalBigInt(unsignedTx, "gasPrice"),
|
|
322
|
+
(msg) => {
|
|
323
|
+
throw new TxValidationError(msg);
|
|
324
|
+
},
|
|
325
|
+
vctx.feeCeiling
|
|
326
|
+
);
|
|
327
|
+
const to = readToAddress(unsignedTx);
|
|
328
|
+
const data = readData(unsignedTx);
|
|
329
|
+
const { name, args } = decodeSignable(data);
|
|
330
|
+
const pm = vctx.contracts.PositionManager;
|
|
331
|
+
switch (expected.kind) {
|
|
332
|
+
case "mint": {
|
|
333
|
+
if (name !== "mintUCD") {
|
|
334
|
+
throw new TxValidationError(`expected mintUCD, server returned ${name}`);
|
|
335
|
+
}
|
|
336
|
+
if (!eqAddr(to, pm)) {
|
|
337
|
+
throw new TxValidationError(`mintUCD must target PositionManager (${pm}), got ${to}`);
|
|
338
|
+
}
|
|
339
|
+
requirePositionId(args[0], expected.positionId);
|
|
340
|
+
const txMintAmount = BigInt(String(args[1]));
|
|
341
|
+
if (txMintAmount.toString() !== expected.amountWei) {
|
|
342
|
+
throw new TxValidationError(`mintAmount mismatch \u2014 tx ${txMintAmount}, expected ${expected.amountWei}`);
|
|
343
|
+
}
|
|
344
|
+
const txMintFee = BigInt(String(args[2]));
|
|
345
|
+
if (txMintFee.toString() !== expected.mintFeeWei) {
|
|
346
|
+
throw new TxValidationError(`mintFee mismatch \u2014 tx ${txMintFee}, expected ${expected.mintFeeWei}`);
|
|
347
|
+
}
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "repay": {
|
|
351
|
+
if (name !== "makePayment") {
|
|
352
|
+
throw new TxValidationError(`expected makePayment, server returned ${name}`);
|
|
353
|
+
}
|
|
354
|
+
if (!eqAddr(to, pm)) {
|
|
355
|
+
throw new TxValidationError(`${name} must target PositionManager (${pm}), got ${to}`);
|
|
356
|
+
}
|
|
357
|
+
requirePositionId(args[0], expected.positionId);
|
|
358
|
+
const txAmount = BigInt(String(args[1]));
|
|
359
|
+
if (txAmount.toString() !== expected.amountWei) {
|
|
360
|
+
throw new TxValidationError(`paymentAmount mismatch \u2014 tx ${txAmount}, expected ${expected.amountWei}`);
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "extend": {
|
|
365
|
+
if (name !== "extendPosition") {
|
|
366
|
+
throw new TxValidationError(`expected extendPosition, server returned ${name}`);
|
|
367
|
+
}
|
|
368
|
+
if (!eqAddr(to, pm)) {
|
|
369
|
+
throw new TxValidationError(`extendPosition must target PositionManager (${pm}), got ${to}`);
|
|
370
|
+
}
|
|
371
|
+
requirePositionId(args[0], expected.positionId);
|
|
372
|
+
const txTerm = Number(args[1]);
|
|
373
|
+
if (txTerm !== expected.selectedTerm) {
|
|
374
|
+
throw new TxValidationError(`selectedTerm mismatch \u2014 tx ${txTerm}, expected ${expected.selectedTerm}`);
|
|
375
|
+
}
|
|
376
|
+
if (expected.upperBoundProRataFeeWei != null && args.length >= 7) {
|
|
377
|
+
const txFee = BigInt(String(args[5]));
|
|
378
|
+
const upper = BigInt(expected.upperBoundProRataFeeWei);
|
|
379
|
+
if (txFee > upper) {
|
|
380
|
+
throw new TxValidationError(
|
|
381
|
+
`proRataRenewalFee=${txFee} exceeds upper bound=${upper}. A compromised validator could otherwise inflate the fee to drain UCD.`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "withdraw-btc": {
|
|
388
|
+
if (name !== "withdrawBTC") {
|
|
389
|
+
throw new TxValidationError(`expected withdrawBTC, server returned ${name}`);
|
|
390
|
+
}
|
|
391
|
+
if (!eqAddr(to, pm)) {
|
|
392
|
+
throw new TxValidationError(`withdrawBTC must target PositionManager (${pm}), got ${to}`);
|
|
393
|
+
}
|
|
394
|
+
const paramsTuple = args[0];
|
|
395
|
+
requirePositionId(paramsTuple[0], expected.positionId);
|
|
396
|
+
const txWithdrawalAddress = String(paramsTuple[5]);
|
|
397
|
+
if (txWithdrawalAddress !== expected.btcAddress) {
|
|
398
|
+
throw new TxValidationError(`btcAddress mismatch \u2014 tx '${txWithdrawalAddress}', expected '${expected.btcAddress}'`);
|
|
399
|
+
}
|
|
400
|
+
if (expected.upperBoundTotalDeduction != null) {
|
|
401
|
+
const txDeduction = BigInt(String(paramsTuple[6]));
|
|
402
|
+
const upper = BigInt(expected.upperBoundTotalDeduction);
|
|
403
|
+
if (txDeduction > upper) {
|
|
404
|
+
throw new TxValidationError(
|
|
405
|
+
`totalDeduction=${txDeduction} exceeds approved upper bound=${upper}. A compromised validator could otherwise drain collateral beyond the approved amount.`
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "ucd-approve": {
|
|
412
|
+
if (name !== "approve") {
|
|
413
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
414
|
+
}
|
|
415
|
+
const ucd = vctx.contracts.UCDToken;
|
|
416
|
+
if (!eqAddr(to, ucd)) {
|
|
417
|
+
throw new TxValidationError(`approve must target UCDToken (${ucd}), got ${to}`);
|
|
418
|
+
}
|
|
419
|
+
const spender = String(args[0]);
|
|
420
|
+
const ok = expected.spenderCandidates.some((s) => eqAddr(spender, s));
|
|
421
|
+
if (!ok) {
|
|
422
|
+
throw new TxValidationError(
|
|
423
|
+
`approve spender ${spender} not in expected set ${expected.spenderCandidates.join(", ")}`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
const amt = BigInt(String(args[1]));
|
|
427
|
+
const min = BigInt(expected.minAmountWei);
|
|
428
|
+
if (amt < min) {
|
|
429
|
+
throw new TxValidationError(`approve amount ${amt} is less than required ${min}`);
|
|
430
|
+
}
|
|
431
|
+
const SANITY_MAX = min * 1000n;
|
|
432
|
+
if (amt > SANITY_MAX) {
|
|
433
|
+
throw new TxValidationError(`approve amount ${amt} exceeds sanity cap ${SANITY_MAX} (1000x requested)`);
|
|
434
|
+
}
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
case "create-position": {
|
|
438
|
+
if (name !== "createPosition") {
|
|
439
|
+
throw new TxValidationError(`expected createPosition, server returned ${name}`);
|
|
440
|
+
}
|
|
441
|
+
if (!eqAddr(to, pm)) {
|
|
442
|
+
throw new TxValidationError(`createPosition must target PositionManager (${pm}), got ${to}`);
|
|
443
|
+
}
|
|
444
|
+
const txTerm = BigInt(String(args[4]));
|
|
445
|
+
if (txTerm !== BigInt(expected.selectedTerm)) {
|
|
446
|
+
throw new TxValidationError(`selectedTerm mismatch \u2014 tx ${txTerm}, expected ${expected.selectedTerm}`);
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
case "set-position-delegate": {
|
|
451
|
+
if (name !== "setPositionDelegate") {
|
|
452
|
+
throw new TxValidationError(`expected setPositionDelegate, server returned ${name}`);
|
|
453
|
+
}
|
|
454
|
+
if (!eqAddr(to, expected.registryAddress)) {
|
|
455
|
+
throw new TxValidationError(
|
|
456
|
+
`setPositionDelegate must target PositionDelegateRegistry (${expected.registryAddress}), got ${to}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
requirePositionId(args[0], expected.positionId);
|
|
460
|
+
const txDelegate = String(args[1]);
|
|
461
|
+
if (!eqAddr(txDelegate, expected.delegate)) {
|
|
462
|
+
throw new TxValidationError(`delegate mismatch \u2014 tx ${txDelegate}, expected ${expected.delegate}`);
|
|
463
|
+
}
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
case "bwar-add-address": {
|
|
467
|
+
if (name !== "addAddress") {
|
|
468
|
+
throw new TxValidationError(`expected addAddress, decoded ${name}`);
|
|
469
|
+
}
|
|
470
|
+
if (!eqAddr(to, expected.registryAddress)) {
|
|
471
|
+
throw new TxValidationError(
|
|
472
|
+
`addAddress must target the BitcoinWithdrawalAddressRegistry (${expected.registryAddress}), got ${to}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (String(args[0]) !== expected.btcAddress) {
|
|
476
|
+
throw new TxValidationError(
|
|
477
|
+
`btcAddress mismatch \u2014 tx says '${String(args[0])}', user asked for '${expected.btcAddress}'`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case "stablecoin-approve": {
|
|
483
|
+
if (name !== "approve") {
|
|
484
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
485
|
+
}
|
|
486
|
+
if (!eqAddr(to, expected.tokenAddress)) {
|
|
487
|
+
throw new TxValidationError(`approve must target the stablecoin token (${expected.tokenAddress}), got ${to}`);
|
|
488
|
+
}
|
|
489
|
+
if (!eqAddr(String(args[0]), expected.spender)) {
|
|
490
|
+
throw new TxValidationError(`approve spender must be the PSM (${expected.spender}), got ${String(args[0])}`);
|
|
491
|
+
}
|
|
492
|
+
if (BigInt(String(args[1])).toString() !== expected.amountUnits) {
|
|
493
|
+
throw new TxValidationError(
|
|
494
|
+
`approve amount must be EXACTLY ${expected.amountUnits} (exact-amount rule), got ${BigInt(String(args[1]))}`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
case "ucd-approve-controller": {
|
|
500
|
+
if (name !== "approve") {
|
|
501
|
+
throw new TxValidationError(`expected ERC-20 approve, server returned ${name}`);
|
|
502
|
+
}
|
|
503
|
+
if (!eqAddr(to, expected.ucdTokenAddress)) {
|
|
504
|
+
throw new TxValidationError(`approve must target UCDToken (${expected.ucdTokenAddress}), got ${to}`);
|
|
505
|
+
}
|
|
506
|
+
if (!eqAddr(String(args[0]), expected.spender)) {
|
|
507
|
+
throw new TxValidationError(`approve spender must be the UCDController (${expected.spender}), got ${String(args[0])}`);
|
|
508
|
+
}
|
|
509
|
+
if (BigInt(String(args[1])).toString() !== expected.amountWei) {
|
|
510
|
+
throw new TxValidationError(
|
|
511
|
+
`approve amount must be EXACTLY ${expected.amountWei} (exact-amount rule), got ${BigInt(String(args[1]))}`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "psm-swap": {
|
|
517
|
+
if (name !== "swap")
|
|
518
|
+
throw new TxValidationError(`expected swap, server returned ${name}`);
|
|
519
|
+
if (!eqAddr(to, expected.psmAddress)) {
|
|
520
|
+
throw new TxValidationError(`swap must target the PSM (${expected.psmAddress}), got ${to}`);
|
|
521
|
+
}
|
|
522
|
+
if (!eqAddr(String(args[0]), expected.stablecoin))
|
|
523
|
+
throw new TxValidationError(`swap stablecoin mismatch`);
|
|
524
|
+
if (BigInt(String(args[1])).toString() !== expected.amountIn)
|
|
525
|
+
throw new TxValidationError(`swap amountIn mismatch`);
|
|
526
|
+
if (BigInt(String(args[2])).toString() !== expected.minOut)
|
|
527
|
+
throw new TxValidationError(`swap minUcdOut mismatch`);
|
|
528
|
+
if (BigInt(expected.minOut) === 0n) {
|
|
529
|
+
throw new TxValidationError(`swap minUcdOut is 0 \u2014 a slippage floor is mandatory (SlippageProtectionRequired)`);
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "psm-redeem": {
|
|
534
|
+
if (name !== "redeem")
|
|
535
|
+
throw new TxValidationError(`expected redeem, server returned ${name}`);
|
|
536
|
+
if (!eqAddr(to, expected.psmAddress)) {
|
|
537
|
+
throw new TxValidationError(`redeem must target the PSM (${expected.psmAddress}), got ${to}`);
|
|
538
|
+
}
|
|
539
|
+
if (!eqAddr(String(args[0]), expected.stablecoin))
|
|
540
|
+
throw new TxValidationError(`redeem stablecoin mismatch`);
|
|
541
|
+
if (BigInt(String(args[1])).toString() !== expected.amountUcdWei)
|
|
542
|
+
throw new TxValidationError(`redeem ucdAmount mismatch`);
|
|
543
|
+
if (BigInt(String(args[2])).toString() !== expected.minOut)
|
|
544
|
+
throw new TxValidationError(`redeem minStablecoinOut mismatch`);
|
|
545
|
+
if (BigInt(expected.minOut) === 0n) {
|
|
546
|
+
throw new TxValidationError(`redeem minStablecoinOut is 0 \u2014 a slippage floor is mandatory (SlippageProtectionRequired)`);
|
|
547
|
+
}
|
|
548
|
+
break;
|
|
549
|
+
}
|
|
550
|
+
default: {
|
|
551
|
+
const _exhaustive = expected;
|
|
552
|
+
throw new TxValidationError(`unhandled expected action kind: ${JSON.stringify(_exhaustive)}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function readTxFeeFields(unsignedTx) {
|
|
557
|
+
try {
|
|
558
|
+
return {
|
|
559
|
+
gasLimit: readOptionalBigInt(unsignedTx, "gasLimit"),
|
|
560
|
+
maxFeePerGas: readOptionalBigInt(unsignedTx, "maxFeePerGas") ?? readOptionalBigInt(unsignedTx, "gasPrice")
|
|
561
|
+
};
|
|
562
|
+
} catch {
|
|
563
|
+
return { gasLimit: null, maxFeePerGas: null };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function describeUnsignedTx(unsignedTx) {
|
|
567
|
+
const to = readToAddress(unsignedTx);
|
|
568
|
+
const data = readData(unsignedTx);
|
|
569
|
+
return { to, fn: signableFunctionName(data), chainId: readChainId(unsignedTx) };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/utils/sign-guard/op-envelope.ts
|
|
573
|
+
import { ethers as ethers4 } from "ethers";
|
|
574
|
+
|
|
575
|
+
// src/utils/quantum-timing.ts
|
|
576
|
+
var QUANTUM_WINDOW_SECONDS = 60;
|
|
577
|
+
var SLOT_SECONDS = 12;
|
|
578
|
+
var INCLUSION_SLOT_TOLERANCE = 3;
|
|
579
|
+
var INCLUSION_LATENCY_BUDGET = SLOT_SECONDS * INCLUSION_SLOT_TOLERANCE;
|
|
580
|
+
|
|
581
|
+
// src/utils/sign-guard/op-envelope.ts
|
|
582
|
+
var QUANTUM_SECONDS = QUANTUM_WINDOW_SECONDS;
|
|
583
|
+
var AUTH_VALIDITY_WINDOWS = 3;
|
|
584
|
+
function authValiditySec() {
|
|
585
|
+
return QUANTUM_SECONDS * AUTH_VALIDITY_WINDOWS;
|
|
586
|
+
}
|
|
587
|
+
function nextQuantumTimestamp(nowMs = Date.now()) {
|
|
588
|
+
const now = Math.floor(nowMs / 1e3);
|
|
589
|
+
const currentQuantum = Math.floor(now / QUANTUM_SECONDS) * QUANTUM_SECONDS;
|
|
590
|
+
return currentQuantum + QUANTUM_SECONDS;
|
|
591
|
+
}
|
|
592
|
+
function isAuthFresh(issuedAtSec, nowSec) {
|
|
593
|
+
return issuedAtSec <= nowSec && nowSec - issuedAtSec < authValiditySec();
|
|
594
|
+
}
|
|
595
|
+
function isStrictCurrentQuantum(issuedAtSec, nowSec) {
|
|
596
|
+
return issuedAtSec <= nowSec && Math.floor(issuedAtSec / QUANTUM_SECONDS) === Math.floor(nowSec / QUANTUM_SECONDS);
|
|
597
|
+
}
|
|
598
|
+
function modeForChainId(chainId) {
|
|
599
|
+
return chainId === 1 ? "prod" : "dev";
|
|
600
|
+
}
|
|
601
|
+
function randomNonceHex(byteLength = 16) {
|
|
602
|
+
return ethers4.hexlify(ethers4.randomBytes(byteLength));
|
|
603
|
+
}
|
|
604
|
+
var OP_ENVELOPE_LAYOUT = "dh-op-envelope-v1";
|
|
605
|
+
var OP_ENVELOPE_TYPES = ["bytes32", "uint256", "uint256", "uint256", "bytes32"];
|
|
606
|
+
var OP_ENVELOPE_FIELD_ORDER = ["positionId", "timestamp", "chainId", "amount", "action"];
|
|
607
|
+
function stampOpEnvelope(input, quantumTs) {
|
|
608
|
+
const actionHash = ethers4.keccak256(ethers4.toUtf8Bytes(input.action));
|
|
609
|
+
const hashHex = ethers4.solidityPackedKeccak256(
|
|
610
|
+
[...OP_ENVELOPE_TYPES],
|
|
611
|
+
[input.positionId, quantumTs, input.chainId, input.value, actionHash]
|
|
612
|
+
);
|
|
613
|
+
return {
|
|
614
|
+
fields: {
|
|
615
|
+
positionId: input.positionId,
|
|
616
|
+
timestamp: String(quantumTs),
|
|
617
|
+
chainId: String(input.chainId),
|
|
618
|
+
amount: input.value.toString(),
|
|
619
|
+
action: input.action
|
|
620
|
+
},
|
|
621
|
+
hashHex
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
async function buildAuthEnvelope(opts) {
|
|
625
|
+
const timestamp = nextQuantumTimestamp(opts.nowMs);
|
|
626
|
+
const { hashHex } = stampOpEnvelope(
|
|
627
|
+
{ positionId: opts.positionId, chainId: opts.chainId, value: opts.value, action: opts.action },
|
|
628
|
+
timestamp
|
|
629
|
+
);
|
|
630
|
+
const userSignature = await opts.signAuthMessage(hashHex);
|
|
631
|
+
return {
|
|
632
|
+
borrowerAddress: opts.borrowerAddress,
|
|
633
|
+
userSignature,
|
|
634
|
+
authMessage: {
|
|
635
|
+
positionId: opts.positionId,
|
|
636
|
+
timestamp,
|
|
637
|
+
chainId: opts.chainId,
|
|
638
|
+
...opts.payload,
|
|
639
|
+
action: opts.action,
|
|
640
|
+
mode: modeForChainId(opts.chainId),
|
|
641
|
+
nonce: randomNonceHex()
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/utils/sign-guard/op-specs.ts
|
|
647
|
+
var DELEGATED_OPS = {
|
|
648
|
+
mint: {
|
|
649
|
+
action: "mint-ucd",
|
|
650
|
+
executePath: "/api/lit/mint/execute",
|
|
651
|
+
payloadKey: "amount",
|
|
652
|
+
broadcastMetaKey: "mintMeta",
|
|
653
|
+
refusalMessage: "Mint authorization failed"
|
|
654
|
+
},
|
|
655
|
+
repay: {
|
|
656
|
+
action: "make-payment",
|
|
657
|
+
executePath: "/api/lit/repay/execute",
|
|
658
|
+
payloadKey: "paymentAmount",
|
|
659
|
+
broadcastMetaKey: "repayMeta",
|
|
660
|
+
refusalMessage: "Repay authorization failed"
|
|
661
|
+
},
|
|
662
|
+
renew: {
|
|
663
|
+
action: "extend-position",
|
|
664
|
+
executePath: "/api/lit/renew/execute",
|
|
665
|
+
payloadKey: "newTerm",
|
|
666
|
+
broadcastMetaKey: "renewMeta",
|
|
667
|
+
refusalMessage: "Renew authorization failed"
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
var BROADCAST_PATH = "/api/lit/broadcast";
|
|
671
|
+
|
|
672
|
+
// src/utils/service-endpoint-policy.ts
|
|
673
|
+
function assertSafeServiceEndpoint(endpoint) {
|
|
674
|
+
let url;
|
|
675
|
+
try {
|
|
676
|
+
url = new URL(endpoint);
|
|
677
|
+
} catch {
|
|
678
|
+
throw new Error(`Invalid serviceEndpoint: ${endpoint}`);
|
|
679
|
+
}
|
|
680
|
+
if (url.protocol === "https:")
|
|
681
|
+
return;
|
|
682
|
+
if (url.protocol === "http:") {
|
|
683
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
684
|
+
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || // IPv4 loopback range 127.0.0.0/8 (numeric — not resolvable/hijackable).
|
|
685
|
+
// Each of the trailing three octets must be a valid 0–255 value.
|
|
686
|
+
/^127\.(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){2}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/.test(host);
|
|
687
|
+
if (isLoopback)
|
|
688
|
+
return;
|
|
689
|
+
throw new Error(
|
|
690
|
+
`Refusing to use plaintext http:// for non-loopback endpoint: ${endpoint}. Bearer tokens and signed transactions would travel in cleartext. Use https://\u2026 or run the lit-ops-server on localhost.`
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Unsupported serviceEndpoint protocol '${url.protocol}': ${endpoint}. Use https:// (or http://localhost for development).`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/utils/sign-guard/lit-ops-client.ts
|
|
699
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
700
|
+
var LitOpsHttpError = class extends Error {
|
|
701
|
+
status;
|
|
702
|
+
url;
|
|
703
|
+
body;
|
|
704
|
+
constructor(url, status, body) {
|
|
705
|
+
super(`POST ${url} \u2192 HTTP ${status}: ${body.slice(0, 500)}`);
|
|
706
|
+
this.name = "LitOpsHttpError";
|
|
707
|
+
this.status = status;
|
|
708
|
+
this.url = url;
|
|
709
|
+
this.body = body;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
var LitOpsTransportError = class extends Error {
|
|
713
|
+
url;
|
|
714
|
+
constructor(url, cause) {
|
|
715
|
+
super(`lit-ops-server unreachable at ${url}: ${cause.message}`);
|
|
716
|
+
this.name = "LitOpsTransportError";
|
|
717
|
+
this.url = url;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
async function fetchWithTimeout(url, init, timeoutMs = REQUEST_TIMEOUT_MS, fetchImpl = (...a) => fetch(...a)) {
|
|
721
|
+
const controller = new AbortController();
|
|
722
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
723
|
+
try {
|
|
724
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
725
|
+
} catch (e) {
|
|
726
|
+
if (e.name === "AbortError") {
|
|
727
|
+
throw new Error(`Request to ${url} timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
728
|
+
}
|
|
729
|
+
throw e;
|
|
730
|
+
} finally {
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function postJson(url, body, headers, opts = {}) {
|
|
735
|
+
let resp;
|
|
736
|
+
try {
|
|
737
|
+
resp = await fetchWithTimeout(
|
|
738
|
+
url,
|
|
739
|
+
{
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
742
|
+
body: JSON.stringify(body)
|
|
743
|
+
},
|
|
744
|
+
opts.timeoutMs,
|
|
745
|
+
opts.fetchImpl
|
|
746
|
+
);
|
|
747
|
+
} catch (e) {
|
|
748
|
+
if (/timed out/.test(e.message))
|
|
749
|
+
throw e;
|
|
750
|
+
throw new LitOpsTransportError(url, e);
|
|
751
|
+
}
|
|
752
|
+
if (!resp.ok) {
|
|
753
|
+
const text = await resp.text().catch(() => "");
|
|
754
|
+
throw new LitOpsHttpError(url, resp.status, text);
|
|
755
|
+
}
|
|
756
|
+
try {
|
|
757
|
+
return await resp.json();
|
|
758
|
+
} catch (e) {
|
|
759
|
+
throw new Error(`POST ${url} returned non-JSON body: ${e.message}`);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function asEnvelope(raw, label) {
|
|
763
|
+
if (typeof raw !== "object" || raw === null) {
|
|
764
|
+
throw new Error(`${label}: server returned ${raw === null ? "null" : typeof raw}, expected an object`);
|
|
765
|
+
}
|
|
766
|
+
const env = raw;
|
|
767
|
+
if (typeof env["success"] !== "boolean") {
|
|
768
|
+
throw new Error(`${label}: server response is missing a boolean 'success' field`);
|
|
769
|
+
}
|
|
770
|
+
if (env["data"] !== void 0 && (typeof env["data"] !== "object" || env["data"] === null)) {
|
|
771
|
+
throw new Error(`${label}: server response 'data' is not an object`);
|
|
772
|
+
}
|
|
773
|
+
const out = { success: env["success"] };
|
|
774
|
+
if (env["data"] !== void 0)
|
|
775
|
+
out.data = env["data"];
|
|
776
|
+
if (typeof env["error"] === "string")
|
|
777
|
+
out.error = env["error"];
|
|
778
|
+
return out;
|
|
779
|
+
}
|
|
780
|
+
async function authorizeWithServer(opts) {
|
|
781
|
+
assertSafeServiceEndpoint(opts.serviceEndpoint);
|
|
782
|
+
let execJson;
|
|
783
|
+
try {
|
|
784
|
+
const raw = await postJson(
|
|
785
|
+
`${opts.serviceEndpoint}${opts.executePath}`,
|
|
786
|
+
{
|
|
787
|
+
authMessage: opts.envelope.authMessage,
|
|
788
|
+
userSignature: opts.envelope.userSignature,
|
|
789
|
+
borrowerAddress: opts.envelope.borrowerAddress
|
|
790
|
+
},
|
|
791
|
+
opts.headers,
|
|
792
|
+
opts
|
|
793
|
+
);
|
|
794
|
+
execJson = asEnvelope(raw, "authorization");
|
|
795
|
+
} catch (e) {
|
|
796
|
+
return { ok: false, error: e.message };
|
|
797
|
+
}
|
|
798
|
+
if (!execJson.success || !execJson.data?.approved) {
|
|
799
|
+
return { ok: false, error: execJson.data?.error ?? execJson.error ?? opts.refusalMessage };
|
|
800
|
+
}
|
|
801
|
+
const unsignedTx = execJson.data.unsignedTx;
|
|
802
|
+
if (!unsignedTx) {
|
|
803
|
+
return { ok: false, error: "Server did not return unsigned transaction" };
|
|
804
|
+
}
|
|
805
|
+
return { ok: true, outcome: { data: execJson.data, unsignedTx } };
|
|
806
|
+
}
|
|
807
|
+
async function broadcastSignedTx(opts) {
|
|
808
|
+
assertSafeServiceEndpoint(opts.serviceEndpoint);
|
|
809
|
+
let broadcastJson;
|
|
810
|
+
try {
|
|
811
|
+
const raw = await postJson(
|
|
812
|
+
`${opts.serviceEndpoint}${BROADCAST_PATH}`,
|
|
813
|
+
{ signedTx: opts.signedTx, ...opts.meta ?? {} },
|
|
814
|
+
opts.headers,
|
|
815
|
+
opts
|
|
816
|
+
);
|
|
817
|
+
broadcastJson = asEnvelope(raw, "broadcast");
|
|
818
|
+
} catch (e) {
|
|
819
|
+
return { ok: false, error: e.message };
|
|
820
|
+
}
|
|
821
|
+
if (!broadcastJson.success) {
|
|
822
|
+
return { ok: false, error: broadcastJson.error ?? "Broadcast failed" };
|
|
823
|
+
}
|
|
824
|
+
const outcome = {};
|
|
825
|
+
if (broadcastJson.data?.txHash !== void 0)
|
|
826
|
+
outcome.transactionHash = broadcastJson.data.txHash;
|
|
827
|
+
if (broadcastJson.data?.blockNumber !== void 0)
|
|
828
|
+
outcome.blockNumber = broadcastJson.data.blockNumber;
|
|
829
|
+
return { ok: true, outcome };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/utils/sign-guard/sign-and-broadcast.ts
|
|
833
|
+
async function signAndBroadcast(opts) {
|
|
834
|
+
const prefix = opts.errorPrefix ? `${opts.errorPrefix}: ` : "";
|
|
835
|
+
try {
|
|
836
|
+
validateUnsignedTx(opts.unsignedTx, opts.expected, opts.vctx);
|
|
837
|
+
} catch (e) {
|
|
838
|
+
return { ok: false, error: e.message };
|
|
839
|
+
}
|
|
840
|
+
opts.beforeSign?.(opts.unsignedTx);
|
|
841
|
+
let signedTx;
|
|
842
|
+
try {
|
|
843
|
+
const outerTx = await opts.signer.wrapInnerTx(opts.unsignedTx);
|
|
844
|
+
signedTx = await opts.signer.signTransaction(outerTx);
|
|
845
|
+
} catch (e) {
|
|
846
|
+
return { ok: false, error: `${prefix}${e.message}` };
|
|
847
|
+
}
|
|
848
|
+
const sent = await broadcastSignedTx({ ...opts.broadcast, signedTx });
|
|
849
|
+
if (!sent.ok)
|
|
850
|
+
return { ok: false, error: `${prefix}${sent.error}` };
|
|
851
|
+
return sent;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/utils/sign-guard/extend-fee-bound.ts
|
|
855
|
+
import { ethers as ethers5 } from "ethers";
|
|
856
|
+
var POSITION_DETAILS_ABI = [
|
|
857
|
+
"function getPositionDetails(bytes32) view returns (tuple(bytes32 positionId, bytes32 pkpId, uint256 ucdDebt, string vaultAddress, address borrower, uint40 createdAt, uint40 lastUpdated, uint16 selectedTerm, uint40 expiryAt, uint8 status, uint40 previousExpiryAt, uint16 totalTerm))"
|
|
858
|
+
];
|
|
859
|
+
var TERM_MANAGER_ABI = [
|
|
860
|
+
"function getTermFees(uint256 termMonths) view returns (uint88 originationFee, uint88 extensionFee)"
|
|
861
|
+
];
|
|
862
|
+
async function computeExtendFeeUpperBound(provider, contracts, positionId, selectedTerm, onDegraded) {
|
|
863
|
+
const degrade = (reason) => {
|
|
864
|
+
onDegraded?.(reason);
|
|
865
|
+
return null;
|
|
866
|
+
};
|
|
867
|
+
const corePm = contracts["PositionManagerCoreModule"];
|
|
868
|
+
const termMgr = contracts["TermManagerModule"];
|
|
869
|
+
if (!corePm)
|
|
870
|
+
return degrade("PositionManagerCoreModule is not in the contract registry");
|
|
871
|
+
if (!termMgr)
|
|
872
|
+
return degrade("TermManagerModule is not in the contract registry");
|
|
873
|
+
const positionIdBytes32 = ethers5.zeroPadValue(
|
|
874
|
+
positionId.startsWith("0x") ? positionId : `0x${positionId}`,
|
|
875
|
+
32
|
|
876
|
+
);
|
|
877
|
+
let ucdDebt;
|
|
878
|
+
try {
|
|
879
|
+
const pm = new ethers5.Contract(corePm, POSITION_DETAILS_ABI, provider);
|
|
880
|
+
const details = await pm.getFunction("getPositionDetails")(positionIdBytes32);
|
|
881
|
+
ucdDebt = BigInt(details.ucdDebt?.toString() ?? "0");
|
|
882
|
+
} catch (e) {
|
|
883
|
+
return degrade(`getPositionDetails failed on ${corePm}: ${e.message.slice(0, 160)}`);
|
|
884
|
+
}
|
|
885
|
+
let extensionFeeRateBps;
|
|
886
|
+
try {
|
|
887
|
+
const tm = new ethers5.Contract(termMgr, TERM_MANAGER_ABI, provider);
|
|
888
|
+
const fees = await tm.getFunction("getTermFees")(selectedTerm);
|
|
889
|
+
const extensionFee = fees[1];
|
|
890
|
+
if (extensionFee === void 0)
|
|
891
|
+
return degrade(`getTermFees(${selectedTerm}) returned no extensionFee`);
|
|
892
|
+
extensionFeeRateBps = BigInt(extensionFee.toString());
|
|
893
|
+
} catch (e) {
|
|
894
|
+
return degrade(`getTermFees(${selectedTerm}) failed on ${termMgr}: ${e.message.slice(0, 160)}`);
|
|
895
|
+
}
|
|
896
|
+
const upperBoundFeeWei = ucdDebt * extensionFeeRateBps / 10000n;
|
|
897
|
+
return { ucdDebt, extensionFeeRateBps, upperBoundFeeWei };
|
|
898
|
+
}
|
|
899
|
+
export {
|
|
900
|
+
AUTH_VALIDITY_WINDOWS,
|
|
901
|
+
BROADCAST_PATH,
|
|
902
|
+
DEFAULT_FEE_CEILING,
|
|
903
|
+
DEFAULT_MAX_TX_FEE_WEI,
|
|
904
|
+
DELEGATED_OPS,
|
|
905
|
+
LitOpsHttpError,
|
|
906
|
+
LitOpsTransportError,
|
|
907
|
+
MAX_FEE_PER_GAS_WEI,
|
|
908
|
+
MAX_GAS_LIMIT,
|
|
909
|
+
OP_ENVELOPE_FIELD_ORDER,
|
|
910
|
+
OP_ENVELOPE_LAYOUT,
|
|
911
|
+
OP_ENVELOPE_TYPES,
|
|
912
|
+
QUANTUM_SECONDS,
|
|
913
|
+
REQUEST_TIMEOUT_MS,
|
|
914
|
+
SIGNABLE_FUNCTIONS,
|
|
915
|
+
TxValidationError,
|
|
916
|
+
asEnvelope,
|
|
917
|
+
assertFeeCeiling,
|
|
918
|
+
authValiditySec,
|
|
919
|
+
authorizeWithServer,
|
|
920
|
+
broadcastSignedTx,
|
|
921
|
+
buildAuthEnvelope,
|
|
922
|
+
buildValidationContext,
|
|
923
|
+
computeExtendFeeUpperBound,
|
|
924
|
+
decodeSignable,
|
|
925
|
+
describeUnsignedTx,
|
|
926
|
+
encodeSignable,
|
|
927
|
+
fetchWithTimeout,
|
|
928
|
+
isAuthFresh,
|
|
929
|
+
isStrictCurrentQuantum,
|
|
930
|
+
modeForChainId,
|
|
931
|
+
nextQuantumTimestamp,
|
|
932
|
+
postJson,
|
|
933
|
+
randomNonceHex,
|
|
934
|
+
readTxFeeFields,
|
|
935
|
+
signAndBroadcast,
|
|
936
|
+
signableFunctionName,
|
|
937
|
+
signableSelectors,
|
|
938
|
+
stampOpEnvelope,
|
|
939
|
+
validateUnsignedTx,
|
|
940
|
+
worstCaseFeeWei
|
|
941
|
+
};
|