@gvnrdao/dh-lit-actions 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2153 @@
1
+ (() => {
2
+ // src/constants/chunks/liquidation.ts
3
+ var ACTIVE_LOAN_LIQUIDATION_THRESHOLD_BPS = 13e3;
4
+ var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
5
+ var EXPIRED_LOAN_MAX_LIQUIDATION_THRESHOLD_BPS = 2e4;
6
+ var GRACE_PERIOD_DAYS = 30;
7
+
8
+ // src/constants/chunks/decimals.ts
9
+ var SATOSHIS_PER_BITCOIN = 100000000n;
10
+ var PRICE_ORACLE_DECIMALS = 100000000n;
11
+ var UCD_TOKEN_DECIMALS = 1000000000000000000n;
12
+
13
+ // src/constants/chunks/price-oracle.ts
14
+ var PRICE_ORACLE_OUTLIER_DEVIATION_THRESHOLD = 0.02;
15
+ var PRICE_ORACLE_DISPERSION_THRESHOLD = 0.05;
16
+
17
+ // src/constants/chunks/bitcoin.ts
18
+ var BITCOIN_DEFAULT_MIN_CONFIRMATIONS = 6;
19
+
20
+ // src/constants/chunks/quantum-time.ts
21
+ var QUANTUM_WINDOW_SECONDS = 100;
22
+ var DEAD_ZONE_SECONDS = 16;
23
+ var SAFE_EXECUTION_WINDOW_SECONDS = QUANTUM_WINDOW_SECONDS - DEAD_ZONE_SECONDS;
24
+
25
+ // deployments/deployment.sepolia.json
26
+ var deployment_sepolia_default = {
27
+ network: "sepolia",
28
+ chainId: 11155111,
29
+ timestamp: "2025-10-24T23:22:01.396Z",
30
+ deployer: "0x2767441E044aCd9bbC21a759fB0517494875092d",
31
+ contracts: {
32
+ UpgradeValidator: "0x4246830c4bF03a6f4460ecEB9e6470562a5Cd0bB",
33
+ UCDToken: "0x86060Fa5B3E01E003b8aFd9C1295F1F444a7dC60",
34
+ UCDController: "0xEE24c338e394610b503768Ba7D8A8c26efdD3Aeb",
35
+ PriceFeedConsumer: "0xA285198196B8f4f658DC81bf4e99A5DC6c191694",
36
+ BTCProofValidator: "0x79469590b41AF8b21276914da7f18FF136B59afB",
37
+ UCDMintingRewards: "0x914740bA9b454d1B3C116A9F55C0929487b7E052",
38
+ PositionManagerCoreModule: "0x0f7E84538026Efdd118168b27f6da00035c75697",
39
+ TermManagerModule: "0x73f0e9151bbEa3cB8b21628E2aAd54C3f6b3968A",
40
+ LoanOperationsManagerModule: "0x71128A4e728ca4B6976E05aC77E50Ad7fe98Ae37",
41
+ CollateralManagerModule: "0xAb9205f4b559F945Ae45c0857fc5c30d699509E1",
42
+ LiquidationManagerModule: "0xe5A72334ED5e0F962be02229b36685d5Bb331cCF",
43
+ CircuitBreakerModule: "0x55eE4Bb05008b73AebcAfa5FbC745348E645d8c6",
44
+ CommunityManagerModule: "0x4E4481B0B3B08804C1d32A19Af66130426D82e89",
45
+ AdminModule: "0xF96F14605f870A0cC2589A5d2b1D77E379762Af2",
46
+ PositionManagerViews: "0x7bc05DBC27BBdab374E3FEBB4Efaa6CDE082fc27",
47
+ PositionManager: "0xAC8d7b714865D4F304b80cd037b517796D56E4Ce",
48
+ MockUSDC: "0x43670cD40c3c4a7acBA5B06B84b8Dd97D7E08f82",
49
+ MockUSDT: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
50
+ SimplePSMV2: "0x8C2D7C833b40eD23Bf1EABAf8B9105C80FD686A2",
51
+ OperationAuthorizationRegistry: "0x400B85A6A901fdbF08f048cD5f3e37B8DD6c0D66"
52
+ },
53
+ upgraded: {
54
+ LoanOperationsManagerModule: {
55
+ previousImplementation: "0xFa3E83992d964E46d82Bb1F92844a8aC909130B2",
56
+ newImplementation: "0xFa3E83992d964E46d82Bb1F92844a8aC909130B2",
57
+ upgradedAt: "2025-10-24T23:22:01.396Z",
58
+ upgradedBy: "0x2767441E044aCd9bbC21a759fB0517494875092d"
59
+ }
60
+ }
61
+ };
62
+
63
+ // deployments/deployment.localhost.json
64
+ var deployment_localhost_default = {
65
+ network: "localhost",
66
+ chainId: 1337,
67
+ timestamp: "2025-10-19T17:53:35.461Z",
68
+ deployer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
69
+ contracts: {
70
+ UpgradeValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
71
+ UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
72
+ UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
73
+ PriceFeedConsumer: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
74
+ BTCProofValidator: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
75
+ UCDMintingRewards: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
76
+ PositionManagerCoreModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
77
+ TermManagerModule: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
78
+ LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
79
+ CollateralManagerModule: "0x59b670e9fA9D0A427751Af201D676719a970857b",
80
+ LiquidationManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
81
+ CircuitBreakerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
82
+ CommunityManagerModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
83
+ AdminModule: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933",
84
+ PositionManagerViews: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
85
+ PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB"
86
+ }
87
+ };
88
+
89
+ // src/config/network-config.ts
90
+ var NETWORK_DEPLOYMENTS = {
91
+ sepolia: deployment_sepolia_default,
92
+ localhost: deployment_localhost_default
93
+ };
94
+ function getNetworkConfig(network) {
95
+ const normalizedNetwork = network.toLowerCase();
96
+ if (!NETWORK_DEPLOYMENTS[normalizedNetwork]) {
97
+ console.warn(
98
+ `Warning: Network "${network}" not found, defaulting to sepolia`
99
+ );
100
+ return NETWORK_DEPLOYMENTS.sepolia;
101
+ }
102
+ return NETWORK_DEPLOYMENTS[normalizedNetwork];
103
+ }
104
+
105
+ // src/constants/chunks/loan-status.ts
106
+ function loanStatusToString(status) {
107
+ switch (status) {
108
+ case 0 /* PENDING_DEPOSIT */:
109
+ return "PENDING_DEPOSIT";
110
+ case 1 /* PENDING_MINT */:
111
+ return "PENDING_MINT";
112
+ case 2 /* ACTIVE */:
113
+ return "ACTIVE";
114
+ case 3 /* EXPIRED */:
115
+ return "EXPIRED";
116
+ case 4 /* LIQUIDATABLE */:
117
+ return "LIQUIDATABLE";
118
+ case 5 /* LIQUIDATED */:
119
+ return "LIQUIDATED";
120
+ case 6 /* REPAID */:
121
+ return "REPAID";
122
+ case 7 /* CLOSED */:
123
+ return "CLOSED";
124
+ default:
125
+ return `UNKNOWN(${status})`;
126
+ }
127
+ }
128
+ function getValidMintingStatuses() {
129
+ return [
130
+ 0 /* PENDING_DEPOSIT */,
131
+ 1 /* PENDING_MINT */,
132
+ 2 /* ACTIVE */
133
+ ];
134
+ }
135
+
136
+ // src/constants/chunks/bitcoin-network-config.ts
137
+ var EVMChain = /* @__PURE__ */ ((EVMChain2) => {
138
+ EVMChain2["SEPOLIA"] = "sepolia";
139
+ EVMChain2["ETHEREUM"] = "ethereum";
140
+ return EVMChain2;
141
+ })(EVMChain || {});
142
+ var CHAIN_TO_BITCOIN_NETWORK = {
143
+ ["sepolia" /* SEPOLIA */]: "testnet" /* TESTNET */,
144
+ ["ethereum" /* ETHEREUM */]: "mainnet" /* MAINNET */
145
+ };
146
+ var APPROVED_BITCOIN_PROVIDERS = {
147
+ ["testnet" /* TESTNET */]: [
148
+ {
149
+ url: "https://diamond-hands-btc-faucet-6b39a1072059.herokuapp.com/api",
150
+ network: "testnet" /* TESTNET */,
151
+ minConfirmations: 1,
152
+ name: "Diamond Hands Faucet"
153
+ }
154
+ ],
155
+ ["mainnet" /* MAINNET */]: [
156
+ {
157
+ url: "https://blockstream.info/api",
158
+ network: "mainnet" /* MAINNET */,
159
+ minConfirmations: 6,
160
+ name: "Blockstream Mainnet"
161
+ },
162
+ {
163
+ url: "https://mempool.space/api",
164
+ network: "mainnet" /* MAINNET */,
165
+ minConfirmations: 6,
166
+ name: "Mempool.space Mainnet"
167
+ }
168
+ ]
169
+ };
170
+ function validateChain(chain) {
171
+ const normalized = chain.toLowerCase().trim();
172
+ if (normalized === "sepolia") {
173
+ return "sepolia" /* SEPOLIA */;
174
+ }
175
+ if (normalized === "ethereum" || normalized === "mainnet") {
176
+ return "ethereum" /* ETHEREUM */;
177
+ }
178
+ throw new Error(
179
+ `Unsupported EVM chain: "${chain}". Supported chains: ${Object.values(EVMChain).join(", ")}`
180
+ );
181
+ }
182
+ function getBitcoinNetworkForChain(chain) {
183
+ return CHAIN_TO_BITCOIN_NETWORK[chain];
184
+ }
185
+ function validateBitcoinProvider(providerUrl, bitcoinNetwork) {
186
+ const providers = APPROVED_BITCOIN_PROVIDERS[bitcoinNetwork];
187
+ const config = providers.find((p) => p.url === providerUrl);
188
+ if (!config) {
189
+ const approvedUrls = providers.map((p) => `${p.name} (${p.url})`).join(", ");
190
+ throw new Error(
191
+ `Bitcoin provider not approved for ${bitcoinNetwork}. Provided URL: ${providerUrl}. Approved providers: ${approvedUrls}`
192
+ );
193
+ }
194
+ return config;
195
+ }
196
+ function getBitcoinProviderConfig(chain, providerUrl) {
197
+ const validatedChain = validateChain(chain);
198
+ const bitcoinNetwork = getBitcoinNetworkForChain(validatedChain);
199
+ const providerConfig = validateBitcoinProvider(providerUrl, bitcoinNetwork);
200
+ if (providerConfig.network !== bitcoinNetwork) {
201
+ throw new Error(
202
+ `Bitcoin provider network mismatch. Provider ${providerConfig.name} is for ${providerConfig.network}, but chain ${chain} requires ${bitcoinNetwork}`
203
+ );
204
+ }
205
+ return providerConfig;
206
+ }
207
+
208
+ // src/modules/quantum-time.module.ts
209
+ function getQuantumStart(timestamp) {
210
+ return Math.floor(timestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
211
+ }
212
+ function getCurrentQuantumTimestamp() {
213
+ const now = Math.floor(Date.now() / 1e3);
214
+ return getQuantumStart(now);
215
+ }
216
+ function isInDeadZone(now) {
217
+ const currentTime = now ?? Math.floor(Date.now() / 1e3);
218
+ const timeInQuantum = currentTime % QUANTUM_WINDOW_SECONDS;
219
+ return timeInQuantum >= 92 || timeInQuantum < 8;
220
+ }
221
+ async function waitForSafeQuantumMoment() {
222
+ const now = Math.floor(Date.now() / 1e3);
223
+ const timeInQuantum = now % QUANTUM_WINDOW_SECONDS;
224
+ if (!isInDeadZone(now)) {
225
+ return;
226
+ }
227
+ let waitSeconds;
228
+ if (timeInQuantum >= 92) {
229
+ waitSeconds = 100 - timeInQuantum + 8;
230
+ } else {
231
+ waitSeconds = 8 - timeInQuantum;
232
+ }
233
+ console.log(
234
+ `[Quantum] Waiting ${waitSeconds}s for safe moment (current time in quantum: ${timeInQuantum}s)`
235
+ );
236
+ await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1e3));
237
+ console.log(`[Quantum] Safe quantum ready: ${getCurrentQuantumTimestamp()}`);
238
+ }
239
+ function validateQuantumTimestamp(timestampOnSignature, now) {
240
+ const currentTime = now ?? Math.floor(Date.now() / 1e3);
241
+ const currentQuantum = getQuantumStart(currentTime);
242
+ const signatureQuantum = getQuantumStart(timestampOnSignature);
243
+ if (signatureQuantum !== currentQuantum) {
244
+ throw new Error(
245
+ `[Quantum] Invalid timestamp: signature from quantum ${signatureQuantum}, current quantum is ${currentQuantum}`
246
+ );
247
+ }
248
+ }
249
+
250
+ // src/modules/authorization.module.ts
251
+ var AuthorizationModule = class {
252
+ /**
253
+ * Verify owner signature for position action
254
+ *
255
+ * Validates that:
256
+ * 1. Waits for safe quantum moment (if in dead zone)
257
+ * 2. Timestamp is within valid quantum window
258
+ * 3. Message includes: LIT action name, caller address, position, timestamp, params
259
+ * 4. Recovered signer matches claimed caller address
260
+ * 5. Caller address matches expected owner
261
+ *
262
+ * @param litActionName - Name of the LIT action (e.g., "ucd-mint-validator")
263
+ * @param auth - Authorization parameters
264
+ * @param expectedOwner - Expected owner address from contract
265
+ * @param actionParams - Action-specific parameters to include in message
266
+ * @returns True if authorized
267
+ */
268
+ static async verifyOwnerSignature(litActionName, auth, expectedOwner, actionParams) {
269
+ await waitForSafeQuantumMoment();
270
+ try {
271
+ validateQuantumTimestamp(auth.timestamp);
272
+ } catch (error) {
273
+ console.error(
274
+ "[Authorization] Quantum timestamp validation failed:",
275
+ error
276
+ );
277
+ return false;
278
+ }
279
+ const message = this.buildAuthorizationMessage(
280
+ litActionName,
281
+ auth.callerAddress,
282
+ auth.positionId,
283
+ auth.timestamp,
284
+ actionParams
285
+ );
286
+ let recoveredAddress;
287
+ try {
288
+ recoveredAddress = await this.recoverSigner(message, auth.signature);
289
+ } catch (error) {
290
+ console.error("[Authorization] Signature recovery failed:", error);
291
+ return false;
292
+ }
293
+ if (recoveredAddress.toLowerCase() !== auth.callerAddress.toLowerCase()) {
294
+ console.error(
295
+ `[Authorization] Caller address mismatch:`,
296
+ `
297
+ Claimed: ${auth.callerAddress}`,
298
+ `
299
+ Recovered: ${recoveredAddress}`
300
+ );
301
+ return false;
302
+ }
303
+ const isAuthorized = auth.callerAddress.toLowerCase() === expectedOwner.toLowerCase();
304
+ if (!isAuthorized) {
305
+ console.error(
306
+ `[Authorization] Not authorized:`,
307
+ `
308
+ Caller: ${auth.callerAddress}`,
309
+ `
310
+ Owner: ${expectedOwner}`
311
+ );
312
+ }
313
+ return isAuthorized;
314
+ }
315
+ /**
316
+ * Build authorization message for signing
317
+ *
318
+ * Creates deterministic message from parameters for signature verification.
319
+ *
320
+ * Message structure:
321
+ * keccak256(litActionName, callerAddress, positionId, timestamp, actionParams...)
322
+ *
323
+ * @param litActionName - Name of the LIT action
324
+ * @param callerAddress - Ethereum address of caller
325
+ * @param positionId - Position identifier
326
+ * @param timestamp - Quantum timestamp
327
+ * @param actionParams - Action-specific parameters
328
+ * @returns Message hash for signing
329
+ */
330
+ static buildAuthorizationMessage(litActionName, callerAddress, positionId, timestamp, actionParams) {
331
+ const sortedKeys = Object.keys(actionParams).sort();
332
+ const values = sortedKeys.map((key) => actionParams[key]);
333
+ const types = [
334
+ "string",
335
+ // litActionName
336
+ "address",
337
+ // callerAddress
338
+ "bytes32",
339
+ // positionId
340
+ "uint256",
341
+ // timestamp
342
+ ...sortedKeys.map(() => "bytes32")
343
+ // action params
344
+ ];
345
+ const allValues = [
346
+ litActionName,
347
+ callerAddress,
348
+ positionId,
349
+ timestamp,
350
+ ...values
351
+ ];
352
+ const messageHash = ethers.utils.solidityKeccak256(types, allValues);
353
+ return messageHash;
354
+ }
355
+ /**
356
+ * Recover signer address from signature
357
+ *
358
+ * Uses EIP-191 standard (Ethereum Signed Message)
359
+ *
360
+ * @param messageHash - Keccak256 hash of the message
361
+ * @param signature - Signature to verify
362
+ * @returns Recovered signer address
363
+ */
364
+ static async recoverSigner(messageHash, signature) {
365
+ try {
366
+ const messageHashBytes = ethers.utils.arrayify(messageHash);
367
+ const prefixedHash = ethers.utils.hashMessage(messageHashBytes);
368
+ const recoveredAddress = ethers.utils.recoverAddress(
369
+ prefixedHash,
370
+ signature
371
+ );
372
+ return recoveredAddress;
373
+ } catch (error) {
374
+ console.error("[Authorization] Signature recovery failed:", error);
375
+ throw new Error(
376
+ `Failed to recover signer: ${error instanceof Error ? error.message : String(error)}`
377
+ );
378
+ }
379
+ }
380
+ /**
381
+ * Verify position owner authorization for mintUCD action
382
+ *
383
+ * @param auth - Authorization parameters
384
+ * @param positionOwner - Position owner address from contract
385
+ * @param amount - UCD amount to mint (in wei)
386
+ * @returns True if authorized
387
+ */
388
+ static async verifyMintAuthorization(auth, positionOwner, amount) {
389
+ return this.verifyOwnerSignature(
390
+ "ucd-mint-validator",
391
+ auth,
392
+ positionOwner,
393
+ {
394
+ action: ethers.utils.keccak256(ethers.utils.toUtf8Bytes("mintUCD")),
395
+ amount: ethers.utils.hexZeroPad(ethers.utils.hexlify(amount), 32)
396
+ }
397
+ );
398
+ }
399
+ /**
400
+ * Verify position owner authorization for withdrawal action
401
+ *
402
+ * @param auth - Authorization parameters
403
+ * @param positionOwner - Position owner address from contract
404
+ * @param btcAddress - Bitcoin address to withdraw to
405
+ * @param satoshis - Amount in satoshis
406
+ * @returns True if authorized
407
+ */
408
+ static async verifyWithdrawalAuthorization(auth, positionOwner, btcAddress, satoshis) {
409
+ return this.verifyOwnerSignature(
410
+ "btc-withdrawal-validator",
411
+ auth,
412
+ positionOwner,
413
+ {
414
+ action: ethers.utils.keccak256(ethers.utils.toUtf8Bytes("withdraw")),
415
+ btcAddress: ethers.utils.keccak256(
416
+ ethers.utils.toUtf8Bytes(btcAddress)
417
+ ),
418
+ satoshis: ethers.utils.hexZeroPad(ethers.utils.hexlify(satoshis), 32)
419
+ }
420
+ );
421
+ }
422
+ /**
423
+ * Verify position owner authorization for repayment action
424
+ *
425
+ * @param auth - Authorization parameters
426
+ * @param positionOwner - Position owner address from contract
427
+ * @param amount - UCD amount to repay (in wei)
428
+ * @returns True if authorized
429
+ */
430
+ static async verifyRepaymentAuthorization(auth, positionOwner, amount) {
431
+ return this.verifyOwnerSignature(
432
+ "ucd-repayment-validator",
433
+ auth,
434
+ positionOwner,
435
+ {
436
+ action: ethers.utils.keccak256(ethers.utils.toUtf8Bytes("repay")),
437
+ amount: ethers.utils.hexZeroPad(ethers.utils.hexlify(amount), 32)
438
+ }
439
+ );
440
+ }
441
+ };
442
+
443
+ // src/modules/bitcoin-data-provider.module.ts
444
+ var DEFAULT_MIN_CONFIRMATIONS = BITCOIN_DEFAULT_MIN_CONFIRMATIONS;
445
+ var BitcoinDataProvider = class {
446
+ constructor(config) {
447
+ this.config = config;
448
+ this.timeout = config.timeout || 1e4;
449
+ }
450
+ /**
451
+ * Fetch all UTXOs for a Bitcoin address
452
+ *
453
+ * @param address - Bitcoin address (legacy, segwit, or taproot)
454
+ * @returns Array of UTXOs
455
+ */
456
+ async getUTXOs(address) {
457
+ if (this.config.rpcHelper) {
458
+ return await this.fetchUTXOsFromRPC(address);
459
+ }
460
+ try {
461
+ return await this.fetchUTXOsFromProvider(
462
+ this.config.providerUrl,
463
+ address
464
+ );
465
+ } catch (error) {
466
+ console.warn(`[Bitcoin Data] Primary provider failed: ${error.message}`);
467
+ if (this.config.fallbackProviders && this.config.fallbackProviders.length > 0) {
468
+ const sortedFallbacks = [...this.config.fallbackProviders].sort(
469
+ (a, b) => a.priority - b.priority
470
+ );
471
+ for (const fallback of sortedFallbacks) {
472
+ try {
473
+ console.log(
474
+ `[Bitcoin Data] Trying fallback: ${fallback.name} (${fallback.url})`
475
+ );
476
+ return await this.fetchUTXOsFromProvider(fallback.url, address);
477
+ } catch (fallbackError) {
478
+ console.warn(
479
+ `[Bitcoin Data] Fallback ${fallback.name} failed: ${fallbackError.message}`
480
+ );
481
+ continue;
482
+ }
483
+ }
484
+ }
485
+ throw new Error(`Failed to fetch UTXOs: ${error.message}`);
486
+ }
487
+ }
488
+ /**
489
+ * Fetch UTXOs using Bitcoin RPC (for regtest/testing)
490
+ */
491
+ async fetchUTXOsFromRPC(address) {
492
+ if (!this.config.rpcHelper) {
493
+ throw new Error("RPC helper not configured");
494
+ }
495
+ const wallet = this.config.rpcWallet || "";
496
+ const unspentOutputs = await this.config.rpcHelper.listUnspent(
497
+ wallet,
498
+ address
499
+ );
500
+ const utxos = unspentOutputs.map((utxo) => ({
501
+ txid: utxo.txid,
502
+ vout: utxo.vout,
503
+ satoshis: BigInt(Math.round(utxo.amount * 1e8)),
504
+ // BTC to satoshis as bigint
505
+ confirmations: utxo.confirmations
506
+ }));
507
+ return utxos;
508
+ }
509
+ /**
510
+ * Detect provider format from URL
511
+ *
512
+ * Determines which API format a provider uses based on its URL.
513
+ *
514
+ * @param providerUrl Provider base URL
515
+ * @returns Provider format type
516
+ */
517
+ detectProviderFormat(providerUrl) {
518
+ if (providerUrl.includes("diamond-hands-btc-faucet")) {
519
+ return "diamond-hands-faucet" /* DIAMOND_HANDS_FAUCET */;
520
+ }
521
+ return "blockstream" /* BLOCKSTREAM */;
522
+ }
523
+ /**
524
+ * Parse Diamond Hands Faucet UTXO response
525
+ *
526
+ * Expected format:
527
+ * {
528
+ * "success": true,
529
+ * "address": "...",
530
+ * "utxos": [{txid, vout, amount, confirmations, scriptPubKey}],
531
+ * "totalBalance": 33000000,
532
+ * "utxoCount": 6
533
+ * }
534
+ *
535
+ * @param data Raw response from faucet API
536
+ * @returns Array of normalized UTXOs
537
+ */
538
+ parseDiamondHandsFaucetUTXOs(data) {
539
+ if (!data.success) {
540
+ throw new Error(
541
+ `Diamond Hands Faucet error: ${data.error || data.message || "Unknown error"}`
542
+ );
543
+ }
544
+ if (!Array.isArray(data.utxos)) {
545
+ throw new Error(
546
+ "Invalid Diamond Hands Faucet response: utxos must be an array"
547
+ );
548
+ }
549
+ const utxos = data.utxos.map((utxo) => ({
550
+ txid: utxo.txid,
551
+ vout: utxo.vout,
552
+ satoshis: BigInt(utxo.amount),
553
+ // Faucet uses "amount" (satoshis)
554
+ confirmations: utxo.confirmations || 0
555
+ }));
556
+ return utxos;
557
+ }
558
+ /**
559
+ * Parse Blockstream/Mempool.space UTXO response
560
+ *
561
+ * Expected format: Array of {txid, vout, value, confirmations}
562
+ *
563
+ * @param data Raw response from provider API
564
+ * @returns Array of normalized UTXOs
565
+ */
566
+ parseBlockstreamUTXOs(data) {
567
+ if (!Array.isArray(data)) {
568
+ throw new Error(
569
+ "Invalid Blockstream response format: expected an array"
570
+ );
571
+ }
572
+ const utxos = data.map((utxo) => ({
573
+ txid: utxo.txid,
574
+ vout: utxo.vout !== void 0 ? utxo.vout : utxo.n,
575
+ satoshis: BigInt(utxo.value !== void 0 ? utxo.value : utxo.satoshis),
576
+ confirmations: utxo.confirmations || 0
577
+ }));
578
+ return utxos;
579
+ }
580
+ /**
581
+ * Internal method to fetch UTXOs from a specific provider
582
+ *
583
+ * Supports multiple provider API formats:
584
+ * - Blockstream/Mempool.space: /api/address/{address}/utxos
585
+ * - Diamond Hands Faucet: /api/faucet/utxos/{address}
586
+ */
587
+ async fetchUTXOsFromProvider(providerUrl, address) {
588
+ const format = this.detectProviderFormat(providerUrl);
589
+ let endpoint;
590
+ if (format === "diamond-hands-faucet" /* DIAMOND_HANDS_FAUCET */) {
591
+ endpoint = `${providerUrl}/api/faucet/utxos/${address}`;
592
+ } else {
593
+ endpoint = `${providerUrl}/api/address/${address}/utxos`;
594
+ }
595
+ const controller = new AbortController();
596
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
597
+ try {
598
+ const response = await fetch(endpoint, { signal: controller.signal });
599
+ clearTimeout(timeoutId);
600
+ if (!response.ok) {
601
+ throw new Error(
602
+ `Bitcoin provider error: ${response.status} ${response.statusText}`
603
+ );
604
+ }
605
+ const data = await response.json();
606
+ if (format === "diamond-hands-faucet" /* DIAMOND_HANDS_FAUCET */) {
607
+ return this.parseDiamondHandsFaucetUTXOs(data);
608
+ } else {
609
+ return this.parseBlockstreamUTXOs(data);
610
+ }
611
+ } catch (error) {
612
+ clearTimeout(timeoutId);
613
+ if (error.name === "AbortError") {
614
+ throw new Error(`Request timeout after ${this.timeout}ms`);
615
+ }
616
+ throw error;
617
+ }
618
+ }
619
+ /**
620
+ * Get complete UTXO set with balance summary
621
+ *
622
+ * This is the SINGLE method that vault-balance module should call.
623
+ * Returns everything needed to know about Bitcoin network state for an address.
624
+ *
625
+ * @param address - Bitcoin address
626
+ * @param minConfirmations - Minimum confirmations for "confirmed" status (default: 6)
627
+ * @returns Complete UTXO set with balance breakdown
628
+ */
629
+ async getUTXOSet(address, minConfirmations = DEFAULT_MIN_CONFIRMATIONS) {
630
+ const utxos = await this.getUTXOs(address);
631
+ const totalBalance = utxos.reduce((sum, utxo) => sum + utxo.satoshis, 0n);
632
+ const confirmedBalance = utxos.filter((utxo) => utxo.confirmations >= minConfirmations).reduce((sum, utxo) => sum + utxo.satoshis, 0n);
633
+ const unconfirmedBalance = totalBalance - confirmedBalance;
634
+ return {
635
+ utxos,
636
+ totalBalance,
637
+ totalUTXOs: utxos.length,
638
+ confirmedBalance,
639
+ unconfirmedBalance
640
+ };
641
+ }
642
+ /**
643
+ * Get transaction with confirmation count
644
+ *
645
+ * Checks if transaction exists on Bitcoin network and returns confirmation count.
646
+ * Used to validate authorized spends have been broadcasted.
647
+ *
648
+ * @param txid - Transaction ID to check
649
+ * @returns Transaction with confirmation count, or null if not found
650
+ */
651
+ async getTransaction(txid) {
652
+ if (this.config.rpcHelper) {
653
+ try {
654
+ const wallet = this.config.rpcWallet || "";
655
+ const tx = await this.config.rpcHelper.getTransaction(wallet, txid);
656
+ return {
657
+ txid: tx.txid,
658
+ confirmations: tx.confirmations || 0
659
+ };
660
+ } catch (error) {
661
+ if (error.message?.includes("Invalid or non-wallet transaction") || error.code === -5) {
662
+ return null;
663
+ }
664
+ throw error;
665
+ }
666
+ }
667
+ const controller = new AbortController();
668
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
669
+ try {
670
+ const response = await fetch(
671
+ `${this.config.providerUrl}/api/tx/${txid}`,
672
+ {
673
+ signal: controller.signal
674
+ }
675
+ );
676
+ clearTimeout(timeoutId);
677
+ if (!response.ok) {
678
+ if (response.status === 404) {
679
+ return null;
680
+ }
681
+ throw new Error(
682
+ `Bitcoin provider error: ${response.status} ${response.statusText}`
683
+ );
684
+ }
685
+ const data = await response.json();
686
+ if (data.status) {
687
+ return {
688
+ txid: data.txid,
689
+ confirmations: data.status.confirmed ? data.status.block_height ? 1 : 0 : 0
690
+ };
691
+ }
692
+ return null;
693
+ } catch (error) {
694
+ clearTimeout(timeoutId);
695
+ if (error.name === "AbortError") {
696
+ throw new Error(`Request timeout after ${this.timeout}ms`);
697
+ }
698
+ if (error.message?.includes("404")) {
699
+ return null;
700
+ }
701
+ throw error;
702
+ }
703
+ }
704
+ // FUTURE: Multi-provider consensus methods will be added here
705
+ // async getUTXOsWithConsensus(address: string): Promise<UTXO[]> {
706
+ // // Fetch from all providers
707
+ // // Return UTXOs that appear in 2+ sources
708
+ // // Use median value if amounts differ
709
+ // }
710
+ };
711
+
712
+ // src/modules/price-oracle.module.ts
713
+ var PriceOracleModule = class {
714
+ constructor(sources) {
715
+ this.sources = sources || this.getDefaultSources();
716
+ this.sources.sort((a, b) => a.priority - b.priority);
717
+ }
718
+ getDefaultSources() {
719
+ return [
720
+ {
721
+ name: "CoinGecko",
722
+ fetchPrice: async () => {
723
+ const url = new URL("https://api.coingecko.com/api/v3/simple/price");
724
+ url.searchParams.set("ids", "bitcoin");
725
+ url.searchParams.set("vs_currencies", "usd");
726
+ const response = await fetch(url.toString());
727
+ if (!response.ok) {
728
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
729
+ }
730
+ const data = await response.json();
731
+ const price = data.bitcoin.usd;
732
+ const allPrices = await Lit.Actions.broadcastAndCollect({
733
+ name: "coinGeckoPrice",
734
+ value: price.toString()
735
+ });
736
+ const prices = allPrices.map((p) => parseFloat(p));
737
+ prices.sort((a, b) => a - b);
738
+ return prices[Math.floor(prices.length / 2)];
739
+ },
740
+ priority: 1
741
+ },
742
+ {
743
+ name: "Binance",
744
+ fetchPrice: async () => {
745
+ const url = new URL("https://api.binance.com/api/v3/ticker/price");
746
+ url.searchParams.set("symbol", "BTCUSDT");
747
+ const response = await fetch(url.toString());
748
+ if (!response.ok) {
749
+ throw new Error(`Binance API error: ${response.status} ${response.statusText}`);
750
+ }
751
+ const data = await response.json();
752
+ const price = parseFloat(data.price);
753
+ const allPrices = await Lit.Actions.broadcastAndCollect({
754
+ name: "binancePrice",
755
+ value: price.toString()
756
+ });
757
+ const prices = allPrices.map((p) => parseFloat(p));
758
+ prices.sort((a, b) => a - b);
759
+ return prices[Math.floor(prices.length / 2)];
760
+ },
761
+ priority: 2
762
+ },
763
+ {
764
+ name: "Coinbase",
765
+ fetchPrice: async () => {
766
+ const response = await fetch("https://api.coinbase.com/v2/prices/BTC-USD/spot");
767
+ if (!response.ok) {
768
+ throw new Error(`Coinbase API error: ${response.status} ${response.statusText}`);
769
+ }
770
+ const data = await response.json();
771
+ const price = parseFloat(data.data.amount);
772
+ const allPrices = await Lit.Actions.broadcastAndCollect({
773
+ name: "coinbasePrice",
774
+ value: price.toString()
775
+ });
776
+ const prices = allPrices.map((p) => parseFloat(p));
777
+ prices.sort((a, b) => a - b);
778
+ return prices[Math.floor(prices.length / 2)];
779
+ },
780
+ priority: 3
781
+ }
782
+ ];
783
+ }
784
+ /**
785
+ * Get BTC price in USD with 8 decimals
786
+ * Fetches from real price sources in priority order
787
+ */
788
+ async getBTCPrice() {
789
+ console.log(`[Price Oracle] Fetching BTC price from external sources...`);
790
+ for (const source of this.sources) {
791
+ try {
792
+ console.log(`[Price Oracle] Trying ${source.name}...`);
793
+ const priceUSD = await source.fetchPrice();
794
+ console.log(
795
+ `[Price Oracle] \u2705 ${source.name}: $${priceUSD.toLocaleString()}`
796
+ );
797
+ const priceWith8Decimals = BigInt(Math.floor(priceUSD * 1e8));
798
+ console.log(
799
+ `[Price Oracle] Price with 8 decimals: ${priceWith8Decimals}`
800
+ );
801
+ return priceWith8Decimals;
802
+ } catch (error) {
803
+ console.warn(
804
+ `[Price Oracle] \u26A0\uFE0F ${source.name} failed: ${error.message}`
805
+ );
806
+ continue;
807
+ }
808
+ }
809
+ throw new Error("All price sources failed");
810
+ }
811
+ /**
812
+ * Consensus across multiple sources with outlier detection
813
+ * Returns median price from valid sources after filtering outliers
814
+ *
815
+ * Master's Wisdom: Two-Path Validation
816
+ * 1. If ONE source is outlier (>2% from median) → FILTER it, continue with remaining
817
+ * 2. If ALL sources too dispersed (>5% spread) → REJECT all, throw error
818
+ *
819
+ * Logic Flow:
820
+ * 1. Calculate initial median from all sources
821
+ * 2. Filter individual outliers (>2% from median)
822
+ * 3. If outliers found: remove them, require minimum 2 sources remaining
823
+ * 4. Recalculate median from valid sources only
824
+ * 5. Check if remaining sources are too dispersed (max/min ratio > 1.05)
825
+ * 6. Return final median or throw appropriate error
826
+ */
827
+ async getBTCPriceConsensus() {
828
+ console.log(`[Price Oracle] Fetching BTC price with consensus...`);
829
+ const results = await Promise.allSettled(
830
+ this.sources.map(async (source) => {
831
+ const price = await source.fetchPrice();
832
+ return { source: source.name, price };
833
+ })
834
+ );
835
+ const successful = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
836
+ if (successful.length === 0) {
837
+ throw new Error("No price sources returned data");
838
+ }
839
+ console.log(
840
+ `[Price Oracle] Got prices from ${successful.length}/${this.sources.length} sources:`
841
+ );
842
+ successful.forEach((s) => {
843
+ console.log(` ${s.source}: $${s.price.toLocaleString()}`);
844
+ });
845
+ const prices = successful.map((s) => s.price);
846
+ prices.sort((a, b) => a - b);
847
+ const initialMedian = prices[Math.floor(prices.length / 2)];
848
+ const minPrice = prices[0];
849
+ const maxPrice = prices[prices.length - 1];
850
+ const dispersionRatio = maxPrice / minPrice;
851
+ const validPrices = successful.filter((s) => {
852
+ const deviation = Math.abs(s.price - initialMedian) / initialMedian;
853
+ return deviation <= PRICE_ORACLE_OUTLIER_DEVIATION_THRESHOLD;
854
+ });
855
+ if (dispersionRatio > 1 + PRICE_ORACLE_DISPERSION_THRESHOLD && validPrices.length === successful.length) {
856
+ throw new Error(
857
+ `Price consensus failed: sources too dispersed (${((dispersionRatio - 1) * 100).toFixed(1)}% spread)`
858
+ );
859
+ }
860
+ if (validPrices.length < successful.length) {
861
+ const outliers = successful.filter(
862
+ (s) => !validPrices.find((v) => v.source === s.source)
863
+ );
864
+ console.log(`[Price Oracle] \u26A0\uFE0F Detected ${outliers.length} outlier(s):`);
865
+ outliers.forEach((o) => {
866
+ const deviation = Math.abs(o.price - initialMedian) / initialMedian;
867
+ console.log(
868
+ ` ${o.source}: $${o.price.toLocaleString()} (${(deviation * 100).toFixed(1)}% deviation)`
869
+ );
870
+ });
871
+ if (validPrices.length < 2) {
872
+ throw new Error(
873
+ "Price consensus failed: insufficient valid sources after outlier removal"
874
+ );
875
+ }
876
+ console.log(
877
+ `[Price Oracle] \u2705 Outliers filtered, continuing with ${validPrices.length} valid sources`
878
+ );
879
+ const validPricesOnly = validPrices.map((v) => v.price);
880
+ validPricesOnly.sort((a, b) => a - b);
881
+ const finalMedian = validPricesOnly[Math.floor(validPricesOnly.length / 2)];
882
+ console.log(
883
+ `[Price Oracle] \u2705 Consensus price (median): $${finalMedian.toLocaleString()}`
884
+ );
885
+ return BigInt(Math.floor(finalMedian * 1e8));
886
+ }
887
+ console.log(
888
+ `[Price Oracle] \u2705 Consensus price (median): $${initialMedian.toLocaleString()}`
889
+ );
890
+ return BigInt(Math.floor(initialMedian * 1e8));
891
+ }
892
+ };
893
+
894
+ // src/modules/vault-balance.module.ts
895
+ var VaultBalanceModule = class {
896
+ constructor(config) {
897
+ this.config = config;
898
+ this.bitcoinProvider = config.bitcoinProvider;
899
+ }
900
+ /**
901
+ * Calculate trusted balance for a position
902
+ *
903
+ * This is the main entry point for determining available balance.
904
+ *
905
+ * CRITICAL: Validates that all authorized spends have been properly broadcasted
906
+ * to Bitcoin network with sufficient confirmations. Throws errors if:
907
+ * 1. Authorized transaction not found on network (not broadcasted)
908
+ * 2. Authorized transaction has insufficient confirmations (network updating)
909
+ *
910
+ * @param positionId - The position ID to check
911
+ * @param vaultAddress - Bitcoin address of the vault
912
+ * @returns Complete breakdown of vault balance
913
+ * @throws Error if authorized spends are in invalid state
914
+ */
915
+ async calculateTrustedBalance(positionId, vaultAddress) {
916
+ const minConfirmations = this.config.minConfirmations || 6;
917
+ const utxoSet = await this.bitcoinProvider.getUTXOSet(
918
+ vaultAddress,
919
+ minConfirmations
920
+ );
921
+ const authorizedSpends = await this.getAuthorizedSpendsFromContract(positionId);
922
+ for (const spend of authorizedSpends) {
923
+ const utxoStillExists = utxoSet.utxos.some(
924
+ (utxo) => utxo.txid === spend.txid && utxo.vout === spend.vout
925
+ );
926
+ if (utxoStillExists) {
927
+ throw new Error(
928
+ `Authorized UTXO ${spend.txid}:${spend.vout} not yet spent. Transaction was authorized in smart contract but not signed and broadcasted to Bitcoin network. Complete the transaction by signing and broadcasting it.`
929
+ );
930
+ }
931
+ }
932
+ const authorizedBalance = authorizedSpends.reduce(
933
+ (sum, spend) => sum + spend.satoshis,
934
+ 0n
935
+ );
936
+ const availableUTXOs = utxoSet.utxos.filter(
937
+ (utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
938
+ );
939
+ const availableBalance = availableUTXOs.reduce(
940
+ (sum, utxo) => sum + utxo.satoshis,
941
+ 0n
942
+ );
943
+ return {
944
+ // Total from Bitcoin
945
+ totalUTXOs: utxoSet.utxos,
946
+ totalBalance: utxoSet.totalBalance,
947
+ // Authorized from Contract
948
+ authorizedUTXOs: authorizedSpends,
949
+ authorizedBalance,
950
+ // Available = Total - Authorized (THIS IS THE TRUSTED BALANCE)
951
+ availableUTXOs,
952
+ availableBalance,
953
+ // Metadata
954
+ vaultAddress,
955
+ positionId,
956
+ timestamp: Date.now()
957
+ };
958
+ }
959
+ /**
960
+ * Get just the trusted balance amount
961
+ *
962
+ * Convenience method when you only need the number.
963
+ *
964
+ * @param positionId - The position ID to check
965
+ * @param vaultAddress - Bitcoin address of the vault
966
+ * @returns Available balance in satoshis
967
+ */
968
+ async getTrustedBalance(positionId, vaultAddress) {
969
+ const result = await this.calculateTrustedBalance(positionId, vaultAddress);
970
+ return result.availableBalance;
971
+ }
972
+ /**
973
+ * Get available UTXOs that can be used for new authorizations
974
+ *
975
+ * This is useful when you need to select specific UTXOs for authorization.
976
+ *
977
+ * @param positionId - The position ID to check
978
+ * @param vaultAddress - Bitcoin address of the vault
979
+ * @returns Array of available UTXOs
980
+ */
981
+ async getAvailableUTXOs(positionId, vaultAddress) {
982
+ const result = await this.calculateTrustedBalance(positionId, vaultAddress);
983
+ return result.availableUTXOs;
984
+ }
985
+ /**
986
+ * Check if a specific UTXO is available for authorization
987
+ *
988
+ * @param positionId - The position ID to check
989
+ * @param vaultAddress - Bitcoin address of the vault
990
+ * @param txid - Transaction ID of the UTXO
991
+ * @param vout - Output index of the UTXO
992
+ * @returns True if UTXO is available, false otherwise
993
+ */
994
+ async isUTXOAvailable(positionId, vaultAddress, txid, vout) {
995
+ const availableUTXOs = await this.getAvailableUTXOs(
996
+ positionId,
997
+ vaultAddress
998
+ );
999
+ return availableUTXOs.some(
1000
+ (utxo) => utxo.txid === txid && utxo.vout === vout
1001
+ );
1002
+ }
1003
+ /**
1004
+ * Check if a UTXO is authorized (appears in authorized spends)
1005
+ *
1006
+ * @param utxo - The UTXO to check
1007
+ * @param authorizedSpends - List of authorized spends from contract
1008
+ * @returns True if UTXO is authorized, false otherwise
1009
+ */
1010
+ isUTXOAuthorized(utxo, authorizedSpends) {
1011
+ return authorizedSpends.some(
1012
+ (spend) => spend.txid === utxo.txid && spend.vout === utxo.vout
1013
+ );
1014
+ }
1015
+ /**
1016
+ * Query smart contract for authorized spends
1017
+ *
1018
+ * Calls the contract's getAuthorizedSpends(positionId) view function
1019
+ * to retrieve all authorized Bitcoin spends for this position.
1020
+ *
1021
+ * Contract struct AuthorizedSpend:
1022
+ * - txid: string
1023
+ * - vout: uint32
1024
+ * - satoshis: uint256
1025
+ * - authorizedAt: uint256 (timestamp)
1026
+ *
1027
+ * @param positionId - The position ID to query
1028
+ * @returns Array of authorized spends for this position
1029
+ */
1030
+ async getAuthorizedSpendsFromContract(positionId) {
1031
+ const positionIdBytes32 = positionId.startsWith("0x") ? positionId : `0x${positionId.padStart(64, "0")}`;
1032
+ const getAuthorizedSpendsABI = [
1033
+ {
1034
+ inputs: [
1035
+ { internalType: "bytes32", name: "positionId", type: "bytes32" }
1036
+ ],
1037
+ name: "getAuthorizedSpends",
1038
+ outputs: [
1039
+ {
1040
+ components: [
1041
+ { internalType: "string", name: "txid", type: "string" },
1042
+ { internalType: "uint32", name: "vout", type: "uint32" },
1043
+ { internalType: "uint256", name: "satoshis", type: "uint256" },
1044
+ {
1045
+ internalType: "string",
1046
+ name: "targetAddress",
1047
+ type: "string"
1048
+ },
1049
+ {
1050
+ internalType: "uint256",
1051
+ name: "targetAmount",
1052
+ type: "uint256"
1053
+ },
1054
+ {
1055
+ internalType: "string",
1056
+ name: "changeAddress",
1057
+ type: "string"
1058
+ },
1059
+ {
1060
+ internalType: "uint256",
1061
+ name: "authorizedAt",
1062
+ type: "uint256"
1063
+ }
1064
+ ],
1065
+ internalType: "struct LoanOperationsManager.AuthorizedSpend[]",
1066
+ name: "",
1067
+ type: "tuple[]"
1068
+ }
1069
+ ],
1070
+ stateMutability: "view",
1071
+ type: "function"
1072
+ }
1073
+ ];
1074
+ let rpcUrl;
1075
+ if (this.config.rpcUrl) {
1076
+ rpcUrl = this.config.rpcUrl;
1077
+ } else {
1078
+ rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.config.chain });
1079
+ }
1080
+ const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
1081
+ const contract = new ethers.Contract(
1082
+ this.config.contractAddress,
1083
+ getAuthorizedSpendsABI,
1084
+ provider
1085
+ );
1086
+ const result = await contract.getAuthorizedSpends(positionIdBytes32);
1087
+ return result.map((spend) => ({
1088
+ txid: spend.txid,
1089
+ vout: Number(spend.vout),
1090
+ satoshis: BigInt(spend.satoshis.toString()),
1091
+ positionId,
1092
+ targetAddress: spend.targetAddress,
1093
+ targetAmount: BigInt(spend.targetAmount.toString()),
1094
+ changeAddress: spend.changeAddress,
1095
+ timestamp: Number(spend.authorizedAt)
1096
+ }));
1097
+ }
1098
+ };
1099
+ function createVaultBalanceModule(config) {
1100
+ return new VaultBalanceModule(config);
1101
+ }
1102
+
1103
+ // src/modules/business-rules-math.module.ts
1104
+ function calculateTermStartFromExpiry(expiryTimestamp, termLengthMonths) {
1105
+ if (expiryTimestamp === 0) {
1106
+ return 0;
1107
+ }
1108
+ const SECONDS_PER_DAY = 86400;
1109
+ const DAYS_PER_MONTH = 30;
1110
+ const termDurationSeconds = termLengthMonths * DAYS_PER_MONTH * SECONDS_PER_DAY;
1111
+ return expiryTimestamp - termDurationSeconds;
1112
+ }
1113
+ function calculateTermStatus(termStartTimestamp, selectedTermMonths) {
1114
+ if (termStartTimestamp === 0) {
1115
+ return {
1116
+ termDurationDays: 0,
1117
+ termLengthDays: selectedTermMonths * 30,
1118
+ isExpired: false,
1119
+ daysUntilExpiry: selectedTermMonths * 30,
1120
+ daysIntoGracePeriod: 0
1121
+ };
1122
+ }
1123
+ const now = Math.floor(Date.now() / 1e3);
1124
+ const termLengthDays = selectedTermMonths * 30;
1125
+ const termDurationSeconds = now - termStartTimestamp;
1126
+ const termDurationDays = Math.floor(termDurationSeconds / 86400);
1127
+ const isExpired = termDurationDays > termLengthDays;
1128
+ const daysUntilExpiry = Math.max(0, termLengthDays - termDurationDays);
1129
+ const daysIntoGracePeriod = Math.max(0, termDurationDays - termLengthDays);
1130
+ return {
1131
+ termDurationDays,
1132
+ termLengthDays,
1133
+ isExpired,
1134
+ daysUntilExpiry,
1135
+ daysIntoGracePeriod
1136
+ };
1137
+ }
1138
+ function calculateLiquidationThreshold(isExpired, daysIntoGracePeriod) {
1139
+ if (!isExpired) {
1140
+ return ACTIVE_LOAN_LIQUIDATION_THRESHOLD_BPS;
1141
+ }
1142
+ const day = Math.min(daysIntoGracePeriod, GRACE_PERIOD_DAYS);
1143
+ const daySquared = day * day;
1144
+ const escalation = daySquared * (EXPIRED_LOAN_MAX_LIQUIDATION_THRESHOLD_BPS - EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS) / (GRACE_PERIOD_DAYS * GRACE_PERIOD_DAYS);
1145
+ const threshold = EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS + escalation;
1146
+ return threshold;
1147
+ }
1148
+ function calculateCollateralMetrics(availableBTCSats, btcPriceUsd, ucdDebt) {
1149
+ if (ucdDebt === 0n) {
1150
+ return {
1151
+ collateralValueUsd: 0n,
1152
+ collateralRatioBps: Number.MAX_SAFE_INTEGER
1153
+ };
1154
+ }
1155
+ const collateralValueUsd = availableBTCSats * btcPriceUsd / (SATOSHIS_PER_BITCOIN * PRICE_ORACLE_DECIMALS);
1156
+ const ratioBigInt = collateralValueUsd * UCD_TOKEN_DECIMALS * 10000n / ucdDebt;
1157
+ const collateralRatioBps = Number(ratioBigInt);
1158
+ return {
1159
+ collateralValueUsd,
1160
+ collateralRatioBps
1161
+ };
1162
+ }
1163
+ function hasSufficientCollateralForDebt(collateralValueUsd, currentDebt, additionalDebt, requiredRatioBps) {
1164
+ const newTotalDebt = currentDebt + additionalDebt;
1165
+ if (newTotalDebt === 0n) {
1166
+ return true;
1167
+ }
1168
+ const newCollateralRatioBps = Number(
1169
+ collateralValueUsd * UCD_TOKEN_DECIMALS * 10000n / newTotalDebt
1170
+ );
1171
+ return newCollateralRatioBps >= requiredRatioBps;
1172
+ }
1173
+ function calculateFeeBps(amount, feeBps) {
1174
+ if (amount < 0n) {
1175
+ throw new Error("Amount cannot be negative");
1176
+ }
1177
+ if (feeBps < 0 || feeBps > 1e4) {
1178
+ throw new Error(
1179
+ "Fee basis points must be between 0 and 10000 (0% to 100%)"
1180
+ );
1181
+ }
1182
+ const fee = amount * BigInt(feeBps) / 10000n;
1183
+ return fee;
1184
+ }
1185
+ function calculateBtcValueUsd(btcSats, btcPriceUsd8Decimals) {
1186
+ if (btcSats < 0n) {
1187
+ throw new Error("BTC amount cannot be negative");
1188
+ }
1189
+ if (btcPriceUsd8Decimals <= 0n) {
1190
+ throw new Error("BTC price must be positive");
1191
+ }
1192
+ const valueUsd = btcSats * btcPriceUsd8Decimals / SATOSHIS_PER_BITCOIN;
1193
+ return valueUsd;
1194
+ }
1195
+ function calculateCollateralRatioBps(collateralValueUsd8Decimals, debtUsd18Decimals) {
1196
+ if (collateralValueUsd8Decimals < 0n) {
1197
+ throw new Error("Collateral value cannot be negative");
1198
+ }
1199
+ if (debtUsd18Decimals <= 0n) {
1200
+ throw new Error("Debt must be positive to calculate ratio");
1201
+ }
1202
+ const scaledCollateral = collateralValueUsd8Decimals * 10000000000n;
1203
+ const ratioBps = scaledCollateral * 10000n / debtUsd18Decimals;
1204
+ const ratioBpsNumber = Number(ratioBps);
1205
+ if (ratioBpsNumber < 0 || ratioBpsNumber > 1e8) {
1206
+ throw new Error(
1207
+ `Collateral ratio out of bounds: ${ratioBpsNumber} bps (${ratioBpsNumber / 100}%)`
1208
+ );
1209
+ }
1210
+ return ratioBpsNumber;
1211
+ }
1212
+ function isCollateralSufficient(collateralValueUsd8Decimals, debtUsd18Decimals, minimumRatioBps) {
1213
+ if (minimumRatioBps < 0 || minimumRatioBps > 1e6) {
1214
+ throw new Error(
1215
+ "Minimum ratio must be between 0 and 1000000 bps (0% to 10000%)"
1216
+ );
1217
+ }
1218
+ const actualRatioBps = calculateCollateralRatioBps(
1219
+ collateralValueUsd8Decimals,
1220
+ debtUsd18Decimals
1221
+ );
1222
+ return actualRatioBps >= minimumRatioBps;
1223
+ }
1224
+ function bpsToPercent(basisPoints) {
1225
+ return basisPoints / 100;
1226
+ }
1227
+
1228
+ // src/modules/protocol-parameters.module.ts
1229
+ var ProtocolParametersModule = class {
1230
+ constructor(config) {
1231
+ this.termManagerAddress = config.termManagerAddress;
1232
+ this.loanOpsManagerAddress = config.loanOpsManagerAddress;
1233
+ this.chain = config.chain;
1234
+ }
1235
+ /**
1236
+ * Get liquidation threshold from LoanOperationsManager
1237
+ *
1238
+ * @returns Liquidation threshold in basis points
1239
+ */
1240
+ async getLiquidationThreshold() {
1241
+ const abi = [
1242
+ {
1243
+ inputs: [],
1244
+ name: "liquidationThreshold",
1245
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1246
+ stateMutability: "view",
1247
+ type: "function"
1248
+ }
1249
+ ];
1250
+ try {
1251
+ const result = await Lit.Actions.call({
1252
+ chain: this.chain,
1253
+ contractAddress: this.loanOpsManagerAddress,
1254
+ abi,
1255
+ methodName: "liquidationThreshold",
1256
+ params: []
1257
+ });
1258
+ return Number(result);
1259
+ } catch (error) {
1260
+ console.error(
1261
+ "[ProtocolParameters] Error fetching liquidation threshold:",
1262
+ error.message
1263
+ );
1264
+ throw new Error(
1265
+ `Failed to fetch liquidation threshold: ${error.message}`
1266
+ );
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Get term-specific fees from TermManager
1271
+ *
1272
+ * @param termMonths Loan term in months
1273
+ * @returns Term fees (origination and extension) in basis points
1274
+ */
1275
+ async getTermFees(termMonths) {
1276
+ const abi = [
1277
+ {
1278
+ inputs: [
1279
+ { internalType: "uint256", name: "_termMonths", type: "uint256" }
1280
+ ],
1281
+ name: "getTermFees",
1282
+ outputs: [
1283
+ { internalType: "uint88", name: "originationFee", type: "uint88" },
1284
+ { internalType: "uint88", name: "extensionFee", type: "uint88" }
1285
+ ],
1286
+ stateMutability: "view",
1287
+ type: "function"
1288
+ }
1289
+ ];
1290
+ try {
1291
+ const result = await Lit.Actions.call({
1292
+ chain: this.chain,
1293
+ contractAddress: this.termManagerAddress,
1294
+ abi,
1295
+ methodName: "getTermFees",
1296
+ params: [termMonths]
1297
+ });
1298
+ return {
1299
+ originationFeeBps: Number(result[0]),
1300
+ extensionFeeBps: Number(result[1])
1301
+ };
1302
+ } catch (error) {
1303
+ console.error(
1304
+ "[ProtocolParameters] Error fetching term fees:",
1305
+ error.message
1306
+ );
1307
+ throw new Error(`Failed to fetch term fees: ${error.message}`);
1308
+ }
1309
+ }
1310
+ };
1311
+ function createProtocolParametersModule(config) {
1312
+ return new ProtocolParametersModule(config);
1313
+ }
1314
+
1315
+ // src/modules/vault-snapshot.ts
1316
+ var VaultSnapshotModule = class {
1317
+ constructor(config) {
1318
+ this.config = config;
1319
+ }
1320
+ /**
1321
+ * Get complete vault snapshot
1322
+ *
1323
+ * This is the main entry point. Returns everything you need to know
1324
+ * about a vault's health in a single call.
1325
+ *
1326
+ * @param positionId - Position to snapshot
1327
+ * @returns Complete VaultSnapshot with all metrics
1328
+ */
1329
+ async getVaultSnapshot(positionId) {
1330
+ const positionState = await this.queryPositionState(positionId);
1331
+ const balanceResult = await this.config.vaultBalance.calculateTrustedBalance(
1332
+ positionId,
1333
+ positionState.vaultAddress
1334
+ );
1335
+ const btcPriceUsd = await this.config.priceOracle.getBTCPrice();
1336
+ const protocolParams = createProtocolParametersModule({
1337
+ termManagerAddress: this.config.termManagerAddress,
1338
+ loanOpsManagerAddress: this.config.loanOpsManagerAddress,
1339
+ chain: this.config.chain
1340
+ });
1341
+ const [liquidationThresholdBps, termFees] = await Promise.all([
1342
+ protocolParams.getLiquidationThreshold(),
1343
+ protocolParams.getTermFees(positionState.selectedTerm)
1344
+ ]);
1345
+ const termStatus = calculateTermStatus(
1346
+ positionState.termStartTimestamp,
1347
+ positionState.selectedTerm
1348
+ );
1349
+ const currentLiquidationThreshold = calculateLiquidationThreshold(
1350
+ termStatus.isExpired,
1351
+ termStatus.daysIntoGracePeriod
1352
+ );
1353
+ const collateralMetrics = calculateCollateralMetrics(
1354
+ balanceResult.availableBalance,
1355
+ btcPriceUsd,
1356
+ positionState.ucdDebt
1357
+ );
1358
+ const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
1359
+ const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
1360
+ return {
1361
+ // Raw state
1362
+ positionId: positionState.positionId,
1363
+ pkpId: positionState.pkpId,
1364
+ borrower: positionState.borrower,
1365
+ vaultAddress: positionState.vaultAddress,
1366
+ ucdDebt: positionState.ucdDebt,
1367
+ termStartTimestamp: positionState.termStartTimestamp,
1368
+ selectedTerm: positionState.selectedTerm,
1369
+ isActive: positionState.isActive,
1370
+ status: positionState.status,
1371
+ // Loan status enum value (0-7)
1372
+ // Bitcoin state
1373
+ totalBTCSats: balanceResult.totalBalance,
1374
+ totalUTXOs: balanceResult.totalUTXOs,
1375
+ authorizedSpendsSats: balanceResult.authorizedBalance,
1376
+ availableBTCSats: balanceResult.availableBalance,
1377
+ availableUTXOs: balanceResult.availableUTXOs,
1378
+ // Price data
1379
+ btcPriceUsd,
1380
+ // Calculated metrics
1381
+ collateralValueUsd: collateralMetrics.collateralValueUsd,
1382
+ collateralRatioBps: collateralMetrics.collateralRatioBps,
1383
+ // Term status
1384
+ termDurationDays: termStatus.termDurationDays,
1385
+ termLengthDays: termStatus.termLengthDays,
1386
+ isExpired: termStatus.isExpired,
1387
+ daysUntilExpiry: termStatus.daysUntilExpiry,
1388
+ daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
1389
+ // Liquidation risk
1390
+ currentLiquidationThreshold,
1391
+ isLiquidatable,
1392
+ marginToLiquidationBps,
1393
+ // Protocol parameters (from smart contracts)
1394
+ liquidationThresholdBps,
1395
+ originationFeeBps: termFees.originationFeeBps,
1396
+ extensionFeeBps: termFees.extensionFeeBps,
1397
+ // Metadata
1398
+ timestamp: Date.now()
1399
+ };
1400
+ }
1401
+ /**
1402
+ * Query position state from smart contract
1403
+ *
1404
+ * Gets the simple state that contract stores:
1405
+ * - positionId, pkpId, borrower, vaultAddress
1406
+ * - ucdDebt
1407
+ * - termStartTimestamp
1408
+ * - selectedTerm
1409
+ * - isActive
1410
+ * - status
1411
+ */
1412
+ async queryPositionState(positionId) {
1413
+ const positionIdBytes32 = positionId.startsWith("0x") ? positionId : `0x${positionId.padStart(64, "0")}`;
1414
+ const getPositionDetailsABI = [
1415
+ {
1416
+ inputs: [
1417
+ { internalType: "bytes32", name: "positionId", type: "bytes32" }
1418
+ ],
1419
+ name: "getPositionDetails",
1420
+ outputs: [
1421
+ {
1422
+ components: [
1423
+ { internalType: "bytes32", name: "positionId", type: "bytes32" },
1424
+ { internalType: "bytes32", name: "pkpId", type: "bytes32" },
1425
+ { internalType: "uint256", name: "ucdDebt", type: "uint256" },
1426
+ {
1427
+ internalType: "string",
1428
+ name: "vaultAddress",
1429
+ type: "string"
1430
+ },
1431
+ { internalType: "address", name: "borrower", type: "address" },
1432
+ { internalType: "uint40", name: "createdAt", type: "uint40" },
1433
+ { internalType: "uint40", name: "lastUpdated", type: "uint40" },
1434
+ {
1435
+ internalType: "uint16",
1436
+ name: "selectedTerm",
1437
+ type: "uint16"
1438
+ },
1439
+ { internalType: "uint40", name: "expiryAt", type: "uint40" },
1440
+ { internalType: "bool", name: "isActive", type: "bool" },
1441
+ {
1442
+ internalType: "enum LoanStatusLib.LoanStatus",
1443
+ name: "status",
1444
+ type: "uint8"
1445
+ }
1446
+ ],
1447
+ internalType: "struct IPositionManagerCore.Position",
1448
+ name: "",
1449
+ type: "tuple"
1450
+ }
1451
+ ],
1452
+ stateMutability: "view",
1453
+ type: "function"
1454
+ }
1455
+ ];
1456
+ let rpcUrl;
1457
+ if (this.config.rpcUrl) {
1458
+ rpcUrl = this.config.rpcUrl;
1459
+ } else {
1460
+ rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.config.chain });
1461
+ }
1462
+ const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
1463
+ const contract = new ethers.Contract(
1464
+ this.config.contractAddress,
1465
+ getPositionDetailsABI,
1466
+ provider
1467
+ );
1468
+ const position = await contract.getPositionDetails(positionIdBytes32);
1469
+ const expiryAt = Number(position.expiryAt);
1470
+ const selectedTermMonths = Number(position.selectedTerm);
1471
+ const termStartTimestamp = calculateTermStartFromExpiry(
1472
+ expiryAt,
1473
+ selectedTermMonths
1474
+ );
1475
+ return {
1476
+ positionId: position.positionId,
1477
+ pkpId: position.pkpId,
1478
+ borrower: position.borrower,
1479
+ vaultAddress: position.vaultAddress,
1480
+ ucdDebt: BigInt(position.ucdDebt.toString()),
1481
+ termStartTimestamp,
1482
+ selectedTerm: selectedTermMonths,
1483
+ isActive: position.isActive,
1484
+ status: Number(position.status)
1485
+ // Extract loan status enum value (0-7)
1486
+ };
1487
+ }
1488
+ /**
1489
+ * Quick check if position is liquidatable
1490
+ *
1491
+ * Convenience method when you only need to know if position can be liquidated.
1492
+ *
1493
+ * @param positionId - Position to check
1494
+ * @returns True if position is liquidatable
1495
+ */
1496
+ async isLiquidatable(positionId) {
1497
+ const snapshot = await this.getVaultSnapshot(positionId);
1498
+ return snapshot.isLiquidatable;
1499
+ }
1500
+ /**
1501
+ * Get collateral ratio for position
1502
+ *
1503
+ * Convenience method when you only need the collateral ratio.
1504
+ *
1505
+ * @param positionId - Position to check
1506
+ * @returns Collateral ratio in basis points
1507
+ */
1508
+ async getCollateralRatio(positionId) {
1509
+ const snapshot = await this.getVaultSnapshot(positionId);
1510
+ return snapshot.collateralRatioBps;
1511
+ }
1512
+ /**
1513
+ * Get available BTC balance
1514
+ *
1515
+ * Convenience method when you only need available balance.
1516
+ *
1517
+ * @param positionId - Position to check
1518
+ * @returns Available BTC in satoshis
1519
+ */
1520
+ async getAvailableBalance(positionId) {
1521
+ const snapshot = await this.getVaultSnapshot(positionId);
1522
+ return snapshot.availableBTCSats;
1523
+ }
1524
+ /**
1525
+ * Check if position has sufficient collateral for new debt
1526
+ *
1527
+ * Used by minting authorization to validate new mints.
1528
+ *
1529
+ * @param positionId - Position to check
1530
+ * @param additionalDebt - Additional UCD debt to add
1531
+ * @param requiredRatioBps - Required collateral ratio (e.g., 13000 for 130%)
1532
+ * @returns True if sufficient collateral exists
1533
+ */
1534
+ async hasSufficientCollateral(positionId, additionalDebt, requiredRatioBps) {
1535
+ const snapshot = await this.getVaultSnapshot(positionId);
1536
+ return hasSufficientCollateralForDebt(
1537
+ snapshot.collateralValueUsd,
1538
+ snapshot.ucdDebt,
1539
+ additionalDebt,
1540
+ requiredRatioBps
1541
+ );
1542
+ }
1543
+ };
1544
+ function createVaultSnapshotModule(config) {
1545
+ return new VaultSnapshotModule(config);
1546
+ }
1547
+
1548
+ // src/modules/system-health.module.ts
1549
+ var SystemHealthModule = class {
1550
+ /**
1551
+ * Check if contract is paused
1552
+ * @param chain Chain identifier
1553
+ * @param contractAddress Contract address to check
1554
+ * @returns True if contract is paused
1555
+ */
1556
+ static async checkPaused(chain, contractAddress) {
1557
+ const pausedAbi = [{
1558
+ inputs: [],
1559
+ name: "paused",
1560
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
1561
+ stateMutability: "view",
1562
+ type: "function"
1563
+ }];
1564
+ try {
1565
+ const result = await Lit.Actions.call({
1566
+ chain,
1567
+ contractAddress,
1568
+ abi: pausedAbi,
1569
+ methodName: "paused",
1570
+ params: []
1571
+ });
1572
+ return Boolean(result);
1573
+ } catch (error) {
1574
+ console.error(`Failed to check pause state for ${contractAddress}:`, error);
1575
+ return false;
1576
+ }
1577
+ }
1578
+ /**
1579
+ * Check oracle emergency mode
1580
+ * @param chain Chain identifier
1581
+ * @param contractAddress Price feed contract address
1582
+ * @returns True if oracle is in emergency mode
1583
+ */
1584
+ static async checkOracleEmergency(chain, contractAddress) {
1585
+ const emergencyModeAbi = [{
1586
+ inputs: [],
1587
+ name: "emergencyMode",
1588
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
1589
+ stateMutability: "view",
1590
+ type: "function"
1591
+ }];
1592
+ try {
1593
+ const result = await Lit.Actions.call({
1594
+ chain,
1595
+ contractAddress,
1596
+ abi: emergencyModeAbi,
1597
+ methodName: "emergencyMode",
1598
+ params: []
1599
+ });
1600
+ return Boolean(result);
1601
+ } catch (error) {
1602
+ console.error(`Failed to check oracle emergency mode for ${contractAddress}:`, error);
1603
+ return false;
1604
+ }
1605
+ }
1606
+ /**
1607
+ * Check circuit breaker status
1608
+ * @param chain Chain identifier
1609
+ * @param contractAddress Circuit breaker contract address
1610
+ * @returns True if circuit breaker is triggered
1611
+ */
1612
+ static async checkCircuitBreaker(chain, contractAddress) {
1613
+ const circuitBreakerAbi = [{
1614
+ inputs: [],
1615
+ name: "isTriggered",
1616
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
1617
+ stateMutability: "view",
1618
+ type: "function"
1619
+ }];
1620
+ try {
1621
+ const result = await Lit.Actions.call({
1622
+ chain,
1623
+ contractAddress,
1624
+ abi: circuitBreakerAbi,
1625
+ methodName: "isTriggered",
1626
+ params: []
1627
+ });
1628
+ return Boolean(result);
1629
+ } catch (error) {
1630
+ console.error(`Failed to check circuit breaker for ${contractAddress}:`, error);
1631
+ return false;
1632
+ }
1633
+ }
1634
+ /**
1635
+ * Check UCD token transfers paused
1636
+ * @param chain Chain identifier
1637
+ * @param contractAddress UCD token contract address
1638
+ * @returns True if transfers are paused
1639
+ */
1640
+ static async checkTransfersPaused(chain, contractAddress) {
1641
+ const transfersPausedAbi = [{
1642
+ inputs: [],
1643
+ name: "transfersPaused",
1644
+ outputs: [{ internalType: "bool", name: "", type: "bool" }],
1645
+ stateMutability: "view",
1646
+ type: "function"
1647
+ }];
1648
+ try {
1649
+ const result = await Lit.Actions.call({
1650
+ chain,
1651
+ contractAddress,
1652
+ abi: transfersPausedAbi,
1653
+ methodName: "transfersPaused",
1654
+ params: []
1655
+ });
1656
+ return Boolean(result);
1657
+ } catch (error) {
1658
+ console.error(`Failed to check transfers paused for ${contractAddress}:`, error);
1659
+ return false;
1660
+ }
1661
+ }
1662
+ };
1663
+
1664
+ // src/ucd-mint-validator.ts
1665
+ var go = async () => {
1666
+ const MIN_BTC_PRICE_USD = 1000000000000n;
1667
+ const MAX_BTC_PRICE_USD = 100000000000000n;
1668
+ const MAX_TIMESTAMP_AGE_MS = 5 * 60 * 1e3;
1669
+ const ABI_REMAINING_MINT_CAPACITY = [
1670
+ {
1671
+ inputs: [],
1672
+ name: "getRemainingMintCapacity",
1673
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1674
+ stateMutability: "view",
1675
+ type: "function"
1676
+ }
1677
+ ];
1678
+ const ABI_MAX_SUPPLY = [
1679
+ {
1680
+ inputs: [],
1681
+ name: "maxSupply",
1682
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1683
+ stateMutability: "view",
1684
+ type: "function"
1685
+ }
1686
+ ];
1687
+ const ABI_CURRENT_SUPPLY = [
1688
+ {
1689
+ inputs: [],
1690
+ name: "getCurrentSupply",
1691
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1692
+ stateMutability: "view",
1693
+ type: "function"
1694
+ }
1695
+ ];
1696
+ const ABI_MINIMUM_LOAN_VALUE_WEI = [
1697
+ {
1698
+ inputs: [],
1699
+ name: "minimumLoanValueWei",
1700
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1701
+ stateMutability: "view",
1702
+ type: "function"
1703
+ }
1704
+ ];
1705
+ const ABI_MAXIMUM_LOAN_VALUE_UCD = [
1706
+ {
1707
+ inputs: [],
1708
+ name: "maximumLoanValueUcd",
1709
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
1710
+ stateMutability: "view",
1711
+ type: "function"
1712
+ }
1713
+ ];
1714
+ console.log("[Step 0] Validating configuration...");
1715
+ const chain = globalThis.chain;
1716
+ const bitcoinProviderUrl = globalThis.bitcoinProviderUrl;
1717
+ if (!chain) {
1718
+ throw new Error(
1719
+ 'Missing required parameter: "chain". Must be "sepolia" or "ethereum"'
1720
+ );
1721
+ }
1722
+ if (!bitcoinProviderUrl) {
1723
+ throw new Error(
1724
+ 'Missing required parameter: "bitcoinProviderUrl". Must be an approved Bitcoin RPC provider URL'
1725
+ );
1726
+ }
1727
+ const validatedChain = validateChain(chain);
1728
+ const bitcoinNetwork = getBitcoinNetworkForChain(validatedChain);
1729
+ const bitcoinProviderConfig = getBitcoinProviderConfig(
1730
+ chain,
1731
+ bitcoinProviderUrl
1732
+ );
1733
+ console.log(` \u2705 Chain: ${chain} (${validatedChain})`);
1734
+ console.log(` \u2705 Bitcoin Network: ${bitcoinNetwork}`);
1735
+ console.log(
1736
+ ` \u2705 Bitcoin Provider: ${bitcoinProviderConfig.name} (${bitcoinProviderConfig.url})`
1737
+ );
1738
+ console.log(
1739
+ ` \u2705 Min Confirmations: ${bitcoinProviderConfig.minConfirmations}`
1740
+ );
1741
+ const networkConfig = getNetworkConfig(chain);
1742
+ const POSITION_MANAGER_ADDRESS = networkConfig.contracts.PositionManager;
1743
+ const LOAN_OPS_MANAGER_ADDRESS = networkConfig.contracts.LoanOperationsManagerModule;
1744
+ const TERM_MANAGER_ADDRESS = networkConfig.contracts.TermManagerModule;
1745
+ const UCD_CONTROLLER_ADDRESS = networkConfig.contracts.UCDController;
1746
+ console.log(` \u2705 Contract addresses loaded for ${chain}`);
1747
+ console.log("[Step 0.5] Critical system health checks...");
1748
+ const CIRCUIT_BREAKER_ADDRESS = networkConfig.contracts.CircuitBreakerModule;
1749
+ const PRICE_FEED_ADDRESS = networkConfig.contracts.PriceFeedConsumer;
1750
+ const UCD_TOKEN_ADDRESS = networkConfig.contracts.UCDToken;
1751
+ const ucdControllerPaused = await SystemHealthModule.checkPaused(
1752
+ chain,
1753
+ UCD_CONTROLLER_ADDRESS
1754
+ );
1755
+ if (ucdControllerPaused) {
1756
+ throw new Error(
1757
+ "UCD Controller is paused - minting operations are currently disabled. Contact protocol administrators for more information."
1758
+ );
1759
+ }
1760
+ console.log(" \u2705 UCD Controller is active");
1761
+ const circuitBreakerTriggered = await SystemHealthModule.checkCircuitBreaker(
1762
+ chain,
1763
+ CIRCUIT_BREAKER_ADDRESS
1764
+ );
1765
+ if (circuitBreakerTriggered) {
1766
+ throw new Error(
1767
+ "Circuit breaker has been triggered - protocol operations are halted. This is an emergency safety measure. Contact protocol administrators."
1768
+ );
1769
+ }
1770
+ console.log(" \u2705 Circuit breaker is not triggered");
1771
+ const oracleEmergency = await SystemHealthModule.checkOracleEmergency(
1772
+ chain,
1773
+ PRICE_FEED_ADDRESS
1774
+ );
1775
+ if (oracleEmergency) {
1776
+ throw new Error(
1777
+ "Price oracle is in emergency mode - price feeds are unreliable. Minting operations are temporarily disabled for safety."
1778
+ );
1779
+ }
1780
+ console.log(" \u2705 Price oracle is operational");
1781
+ const transfersPaused = await SystemHealthModule.checkTransfersPaused(
1782
+ chain,
1783
+ UCD_TOKEN_ADDRESS
1784
+ );
1785
+ if (transfersPaused) {
1786
+ console.log(" \u26A0\uFE0F WARNING: UCD token transfers are currently paused");
1787
+ console.log(" \u26A0\uFE0F Minting will succeed but tokens cannot be transferred until unpaused");
1788
+ console.log(" \u26A0\uFE0F Tokens will remain in your position and can be used for debt repayment");
1789
+ }
1790
+ console.log(" \u2705 System health checks complete");
1791
+ const auth = globalThis.auth;
1792
+ if (!auth || typeof auth !== "object") {
1793
+ throw new Error('Missing or invalid "auth" parameter - must be an object');
1794
+ }
1795
+ if (!auth.positionId) {
1796
+ throw new Error("auth.positionId is required");
1797
+ }
1798
+ if (!auth.callerAddress) {
1799
+ throw new Error("auth.callerAddress is required");
1800
+ }
1801
+ if (typeof auth.timestamp !== "number") {
1802
+ throw new Error("auth.timestamp must be a number");
1803
+ }
1804
+ if (!auth.signature) {
1805
+ throw new Error("auth.signature is required");
1806
+ }
1807
+ const currentTime = Date.now();
1808
+ const timestampAge = currentTime - auth.timestamp;
1809
+ if (timestampAge < 0) {
1810
+ throw new Error(
1811
+ `auth.timestamp is in the future: ${auth.timestamp} vs current ${currentTime}. This may indicate clock synchronization issues.`
1812
+ );
1813
+ }
1814
+ if (timestampAge > MAX_TIMESTAMP_AGE_MS) {
1815
+ throw new Error(
1816
+ `auth.timestamp is too old: ${Math.round(timestampAge / 1e3)}s ago (max ${Math.round(MAX_TIMESTAMP_AGE_MS / 1e3)}s). Please refresh your authorization and try again.`
1817
+ );
1818
+ }
1819
+ console.log(` \u2705 Timestamp validation passed (${Math.round(timestampAge / 1e3)}s old)`);
1820
+ if (globalThis.amount === void 0 || globalThis.amount === null) {
1821
+ throw new Error('Missing required parameter: "amount"');
1822
+ }
1823
+ const amount = BigInt(globalThis.amount);
1824
+ if (amount <= 0n) {
1825
+ throw new Error(`Amount must be positive, got: ${amount.toString()} wei`);
1826
+ }
1827
+ console.log(" Fetching protocol mint limits...");
1828
+ const minLoanValueResult = await Lit.Actions.call({
1829
+ chain,
1830
+ contractAddress: LOAN_OPS_MANAGER_ADDRESS,
1831
+ abi: ABI_MINIMUM_LOAN_VALUE_WEI,
1832
+ methodName: "minimumLoanValueWei",
1833
+ params: []
1834
+ });
1835
+ const MIN_MINT_AMOUNT = BigInt(minLoanValueResult.toString());
1836
+ const maxLoanValueUcdResult = await Lit.Actions.call({
1837
+ chain,
1838
+ contractAddress: LOAN_OPS_MANAGER_ADDRESS,
1839
+ abi: ABI_MAXIMUM_LOAN_VALUE_UCD,
1840
+ methodName: "maximumLoanValueUcd",
1841
+ params: []
1842
+ });
1843
+ const maxLoanValueUcd = BigInt(maxLoanValueUcdResult.toString());
1844
+ const MAX_MINT_AMOUNT = maxLoanValueUcd * 1000000000000000000n;
1845
+ console.log(` \u2705 Protocol limits loaded from LoanOperationsManager`);
1846
+ console.log(` - Minimum: ${MIN_MINT_AMOUNT.toString()} wei`);
1847
+ console.log(` - Maximum: ${MAX_MINT_AMOUNT.toString()} wei`);
1848
+ if (amount < MIN_MINT_AMOUNT) {
1849
+ throw new Error(
1850
+ `Amount too small: ${amount.toString()} wei. Minimum: ${MIN_MINT_AMOUNT.toString()} wei`
1851
+ );
1852
+ }
1853
+ if (amount > MAX_MINT_AMOUNT) {
1854
+ throw new Error(
1855
+ `Amount too large: ${amount.toString()} wei. Maximum: ${MAX_MINT_AMOUNT.toString()} wei`
1856
+ );
1857
+ }
1858
+ const publicKey = globalThis.publicKey;
1859
+ if (!publicKey) {
1860
+ throw new Error('Missing required parameter: "publicKey"');
1861
+ }
1862
+ if (typeof publicKey !== "string" || publicKey.length === 0) {
1863
+ throw new Error("Invalid publicKey: must be non-empty string");
1864
+ }
1865
+ let currentStep = "0.5";
1866
+ try {
1867
+ console.log("[UCD Mint Validator] Started");
1868
+ console.log(` Position: ${auth.positionId}`);
1869
+ console.log(` Amount: ${amount.toString()} wei`);
1870
+ const bitcoinProvider = new BitcoinDataProvider({
1871
+ providerUrl: bitcoinProviderConfig.url
1872
+ });
1873
+ const priceOracle = new PriceOracleModule();
1874
+ const vaultBalance = createVaultBalanceModule({
1875
+ contractAddress: LOAN_OPS_MANAGER_ADDRESS,
1876
+ chain,
1877
+ bitcoinProvider,
1878
+ minConfirmations: bitcoinProviderConfig.minConfirmations
1879
+ });
1880
+ currentStep = "1";
1881
+ console.log("[Step 1] Getting vault snapshot...");
1882
+ const vaultSnapshot = createVaultSnapshotModule({
1883
+ contractAddress: POSITION_MANAGER_ADDRESS,
1884
+ termManagerAddress: TERM_MANAGER_ADDRESS,
1885
+ loanOpsManagerAddress: LOAN_OPS_MANAGER_ADDRESS,
1886
+ chain,
1887
+ vaultBalance,
1888
+ priceOracle
1889
+ });
1890
+ const snapshot = await vaultSnapshot.getVaultSnapshot(auth.positionId);
1891
+ if (snapshot.btcPriceUsd < MIN_BTC_PRICE_USD || snapshot.btcPriceUsd > MAX_BTC_PRICE_USD) {
1892
+ throw new Error(
1893
+ `Bitcoin price ${snapshot.btcPriceUsd} is outside acceptable range (${MIN_BTC_PRICE_USD} - ${MAX_BTC_PRICE_USD}). This may indicate oracle manipulation or stale price data. Please try again later.`
1894
+ );
1895
+ }
1896
+ console.log(` \u2705 BTC price validation passed: $${(Number(snapshot.btcPriceUsd) / 1e8).toFixed(2)}`);
1897
+ currentStep = "2";
1898
+ console.log("[Step 2] Authenticating caller...");
1899
+ const isAuthorized = await AuthorizationModule.verifyMintAuthorization(
1900
+ auth,
1901
+ snapshot.borrower,
1902
+ // Expected owner from contract
1903
+ amount
1904
+ );
1905
+ if (!isAuthorized) {
1906
+ throw new Error("Caller not authorized");
1907
+ }
1908
+ console.log(" \u2705 Caller authorized");
1909
+ currentStep = "3";
1910
+ console.log("[Step 3] Validating position state...");
1911
+ const validMintingStates = getValidMintingStatuses();
1912
+ if (!validMintingStates.includes(snapshot.status)) {
1913
+ throw new Error(
1914
+ `Invalid position status for minting: ${loanStatusToString(
1915
+ snapshot.status
1916
+ )}. Must be PENDING_DEPOSIT, PENDING_MINT, or ACTIVE. Current status prevents minting operations.`
1917
+ );
1918
+ }
1919
+ console.log(
1920
+ ` \u2705 Position status valid for minting: ${loanStatusToString(
1921
+ snapshot.status
1922
+ )}`
1923
+ );
1924
+ if (snapshot.isExpired) {
1925
+ throw new Error(
1926
+ `Loan term expired ${snapshot.daysIntoGracePeriod} days ago - minting rejected. Must repay or extend term first.`
1927
+ );
1928
+ }
1929
+ if (snapshot.isLiquidatable) {
1930
+ throw new Error(
1931
+ `Position is liquidatable (${bpsToPercent(
1932
+ snapshot.collateralRatioBps
1933
+ )}% < ${bpsToPercent(
1934
+ snapshot.currentLiquidationThreshold
1935
+ )}%) - minting rejected. Add collateral first.`
1936
+ );
1937
+ }
1938
+ console.log(" \u2705 Position is healthy");
1939
+ console.log(` - Term: ${snapshot.daysUntilExpiry} days remaining`);
1940
+ console.log(
1941
+ ` - Current ratio: ${bpsToPercent(snapshot.collateralRatioBps)}%`
1942
+ );
1943
+ console.log(
1944
+ ` - Liquidation threshold: ${bpsToPercent(
1945
+ snapshot.currentLiquidationThreshold
1946
+ )}%`
1947
+ );
1948
+ currentStep = "4";
1949
+ console.log("[Step 4] Calculating fees...");
1950
+ const mintFee = calculateFeeBps(amount, snapshot.originationFeeBps);
1951
+ console.log(
1952
+ ` Mint fee: ${mintFee.toString()} wei (${bpsToPercent(snapshot.originationFeeBps)}%)`
1953
+ );
1954
+ currentStep = "4a";
1955
+ console.log("[Step 4a] Validating daily mint limit...");
1956
+ const totalMintAmount = amount + mintFee;
1957
+ const remainingCapacity = await Lit.Actions.call({
1958
+ chain,
1959
+ contractAddress: UCD_CONTROLLER_ADDRESS,
1960
+ abi: ABI_REMAINING_MINT_CAPACITY,
1961
+ methodName: "getRemainingMintCapacity",
1962
+ params: []
1963
+ });
1964
+ const remainingCapacityBigInt = BigInt(remainingCapacity.toString());
1965
+ if (totalMintAmount > remainingCapacityBigInt) {
1966
+ throw new Error(
1967
+ `Daily mint limit would be exceeded: attempting to mint ${totalMintAmount.toString()} wei (${amount.toString()} + ${mintFee.toString()} fee), but only ${remainingCapacityBigInt.toString()} wei remaining in daily capacity`
1968
+ );
1969
+ }
1970
+ console.log(" \u2705 Daily mint limit check passed");
1971
+ console.log(
1972
+ ` - Mint amount (with fee): ${totalMintAmount.toString()} wei`
1973
+ );
1974
+ console.log(
1975
+ ` - Remaining capacity: ${remainingCapacityBigInt.toString()} wei`
1976
+ );
1977
+ currentStep = "4b";
1978
+ console.log("[Step 4b] Validating max supply limit...");
1979
+ const maxSupplyResult = await Lit.Actions.call({
1980
+ chain,
1981
+ contractAddress: UCD_CONTROLLER_ADDRESS,
1982
+ abi: ABI_MAX_SUPPLY,
1983
+ methodName: "maxSupply",
1984
+ params: []
1985
+ });
1986
+ const maxSupply = BigInt(maxSupplyResult.toString());
1987
+ if (maxSupply > 0n) {
1988
+ const currentSupplyResult = await Lit.Actions.call({
1989
+ chain,
1990
+ contractAddress: UCD_CONTROLLER_ADDRESS,
1991
+ abi: ABI_CURRENT_SUPPLY,
1992
+ methodName: "getCurrentSupply",
1993
+ params: []
1994
+ });
1995
+ const currentSupply = BigInt(currentSupplyResult.toString());
1996
+ const projectedSupply = currentSupply + totalMintAmount;
1997
+ if (projectedSupply > maxSupply) {
1998
+ throw new Error(
1999
+ `Max supply would be exceeded: minting ${totalMintAmount.toString()} wei (${amount.toString()} + ${mintFee.toString()} fee) would result in total supply of ${projectedSupply.toString()} wei, exceeding max supply of ${maxSupply.toString()} wei`
2000
+ );
2001
+ }
2002
+ console.log(" \u2705 Max supply check passed");
2003
+ console.log(` - Current supply: ${currentSupply.toString()} wei`);
2004
+ console.log(` - After mint: ${projectedSupply.toString()} wei`);
2005
+ console.log(` - Max supply: ${maxSupply.toString()} wei`);
2006
+ console.log(
2007
+ ` - Remaining capacity: ${(maxSupply - currentSupply).toString()} wei`
2008
+ );
2009
+ } else {
2010
+ console.log(" \u2705 Max supply unlimited (not enforced)");
2011
+ }
2012
+ currentStep = "5";
2013
+ console.log("[Step 5] Calculating new state...");
2014
+ const newDebt = snapshot.ucdDebt + amount + mintFee;
2015
+ const newCollateral = snapshot.availableBTCSats;
2016
+ console.log(` Current debt: ${snapshot.ucdDebt.toString()} wei`);
2017
+ console.log(` New debt: ${newDebt.toString()} wei`);
2018
+ console.log(` Available collateral: ${newCollateral.toString()} sats`);
2019
+ currentStep = "6";
2020
+ console.log("[Step 6] Validating collateral ratio after mint...");
2021
+ const collateralValueUsd8Dec = calculateBtcValueUsd(
2022
+ newCollateral,
2023
+ snapshot.btcPriceUsd
2024
+ );
2025
+ const newCollateralRatioBps = calculateCollateralRatioBps(
2026
+ collateralValueUsd8Dec,
2027
+ newDebt
2028
+ );
2029
+ console.log(
2030
+ ` New collateral ratio: ${bpsToPercent(newCollateralRatioBps)}%`
2031
+ );
2032
+ console.log(
2033
+ ` Minimum required: ${bpsToPercent(snapshot.liquidationThresholdBps)}%`
2034
+ );
2035
+ if (!isCollateralSufficient(
2036
+ collateralValueUsd8Dec,
2037
+ newDebt,
2038
+ snapshot.liquidationThresholdBps
2039
+ )) {
2040
+ throw new Error(
2041
+ `Insufficient collateral: ${bpsToPercent(
2042
+ newCollateralRatioBps
2043
+ )}% < ${bpsToPercent(snapshot.liquidationThresholdBps)}%`
2044
+ );
2045
+ }
2046
+ console.log(" \u2705 Sufficient collateral");
2047
+ currentStep = "7";
2048
+ console.log("[Step 7] Building authorization message...");
2049
+ const authorizedSpendsHash = ethers.utils.keccak256(
2050
+ // @ts-ignore - ethers available in LIT runtime
2051
+ ethers.utils.defaultAbiCoder.encode(
2052
+ ["uint256"],
2053
+ // Simplified: just use total authorized amount
2054
+ [snapshot.authorizedSpendsSats]
2055
+ )
2056
+ );
2057
+ const messageParams = {
2058
+ authorizedSpendsHash,
2059
+ btcPrice: snapshot.btcPriceUsd.toString(),
2060
+ mintAmount: amount.toString(),
2061
+ mintFee: mintFee.toString(),
2062
+ newCollateral: newCollateral.toString(),
2063
+ newDebt: newDebt.toString()
2064
+ };
2065
+ const message = ethers.utils.keccak256(
2066
+ // @ts-ignore - ethers available in LIT runtime
2067
+ ethers.utils.defaultAbiCoder.encode(
2068
+ [
2069
+ "bytes32",
2070
+ "bytes32",
2071
+ "bytes32",
2072
+ "bytes32",
2073
+ "bytes32",
2074
+ "bytes32",
2075
+ "uint256"
2076
+ ],
2077
+ [
2078
+ auth.positionId,
2079
+ // @ts-ignore - ethers available in LIT runtime
2080
+ ethers.utils.hexZeroPad(
2081
+ ethers.utils.hexlify(BigInt(messageParams.authorizedSpendsHash)),
2082
+ 32
2083
+ ),
2084
+ // @ts-ignore - ethers available in LIT runtime
2085
+ ethers.utils.hexZeroPad(
2086
+ ethers.utils.hexlify(BigInt(messageParams.btcPrice)),
2087
+ 32
2088
+ ),
2089
+ // @ts-ignore - ethers available in LIT runtime
2090
+ ethers.utils.hexZeroPad(
2091
+ ethers.utils.hexlify(BigInt(messageParams.mintAmount)),
2092
+ 32
2093
+ ),
2094
+ // @ts-ignore - ethers available in LIT runtime
2095
+ ethers.utils.hexZeroPad(
2096
+ ethers.utils.hexlify(BigInt(messageParams.mintFee)),
2097
+ 32
2098
+ ),
2099
+ // @ts-ignore - ethers available in LIT runtime
2100
+ ethers.utils.hexZeroPad(
2101
+ ethers.utils.hexlify(BigInt(messageParams.newCollateral)),
2102
+ 32
2103
+ ),
2104
+ // @ts-ignore - ethers available in LIT runtime
2105
+ ethers.utils.hexZeroPad(
2106
+ ethers.utils.hexlify(BigInt(messageParams.newDebt)),
2107
+ 32
2108
+ ),
2109
+ auth.timestamp
2110
+ ]
2111
+ )
2112
+ );
2113
+ currentStep = "8";
2114
+ console.log("[Step 8] Signing authorization...");
2115
+ const signature = await Lit.Actions.signEcdsa({
2116
+ // @ts-ignore - ethers available in LIT runtime
2117
+ toSign: ethers.utils.arrayify(message),
2118
+ publicKey,
2119
+ sigName: "ucdMintAuth"
2120
+ });
2121
+ console.log("[UCD Mint Validator] \u2705 Complete");
2122
+ Lit.Actions.setResponse({
2123
+ response: JSON.stringify({
2124
+ approved: true,
2125
+ positionId: auth.positionId,
2126
+ mintAmount: amount.toString(),
2127
+ mintFee: mintFee.toString(),
2128
+ newDebt: newDebt.toString(),
2129
+ newCollateral: newCollateral.toString(),
2130
+ newCollateralRatioBps,
2131
+ btcPrice: snapshot.btcPriceUsd.toString(),
2132
+ authorizedSpendsHash,
2133
+ signature,
2134
+ timestamp: auth.timestamp,
2135
+ validatorPkp: publicKey
2136
+ })
2137
+ });
2138
+ } catch (error) {
2139
+ console.error("[UCD Mint Validator] \u274C Failed:", error.message);
2140
+ console.error(`[UCD Mint Validator] Failed at step: ${currentStep}`);
2141
+ Lit.Actions.setResponse({
2142
+ response: JSON.stringify({
2143
+ approved: false,
2144
+ reason: error.message || error.toString(),
2145
+ failedStep: currentStep,
2146
+ positionId: auth?.positionId,
2147
+ timestamp: Date.now()
2148
+ })
2149
+ });
2150
+ }
2151
+ };
2152
+ go();
2153
+ })();