@gvnrdao/dh-lit-actions 0.0.32 → 0.0.37

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.
Files changed (72) hide show
  1. package/package.json +6 -13
  2. package/pkg-dist/constants/chunks/lit-actions-registry.d.ts.map +1 -1
  3. package/pkg-dist/constants/chunks/lit-actions-registry.js +274 -0
  4. package/pkg-dist/constants/chunks/lit-actions-registry.js.map +1 -0
  5. package/pkg-dist/constants/chunks/package-registry.js.map +1 -0
  6. package/pkg-dist/constants/index.js.map +1 -0
  7. package/pkg-dist/index.js.map +1 -0
  8. package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.d.ts +6 -0
  9. package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.d.ts.map +1 -1
  10. package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.js.map +1 -0
  11. package/pkg-dist/interfaces/chunks/lit-action-config.i.js.map +1 -0
  12. package/pkg-dist/interfaces/chunks/lit-action-name.i.js.map +1 -0
  13. package/pkg-dist/interfaces/chunks/lit-action-registry.i.js.map +1 -0
  14. package/pkg-dist/interfaces/chunks/pkp-info.i.js.map +1 -0
  15. package/pkg-dist/interfaces/index.js.map +1 -0
  16. package/pkg-dist/package.json +1 -0
  17. package/pkg-dist/utils/chunks/cid-utils.js.map +1 -0
  18. package/pkg-dist/utils/chunks/connection-helpers.js.map +1 -0
  19. package/pkg-dist/utils/chunks/debug-logger.js.map +1 -0
  20. package/pkg-dist/utils/chunks/error-classification.js.map +1 -0
  21. package/pkg-dist/utils/chunks/lit-action-helpers.js.map +1 -0
  22. package/pkg-dist/utils/chunks/pkp-setup.js.map +1 -0
  23. package/pkg-dist/utils/chunks/session-signature-cache.js.map +1 -0
  24. package/pkg-dist/utils/index.js.map +1 -0
  25. package/out/authorization-dummy-b.hash +0 -1
  26. package/out/authorization-dummy-b.js +0 -44
  27. package/out/authorization-dummy.hash +0 -1
  28. package/out/authorization-dummy.js +0 -64
  29. package/out/btc-deposit-validator.hash +0 -1
  30. package/out/btc-deposit-validator.js +0 -1488
  31. package/out/pkp-validator-datil.hash +0 -1
  32. package/out/pkp-validator-datil.js +0 -232
  33. package/out/pkp-validator.hash +0 -1
  34. package/out/pkp-validator.js +0 -410
  35. package/out/ucd-mint-validator.hash +0 -1
  36. package/out/ucd-mint-validator.js +0 -2203
  37. package/pkg-dist/constants/chunks/lit-actions-registry.cjs +0 -175
  38. package/pkg-dist/constants/chunks/lit-actions-registry.cjs.map +0 -1
  39. package/pkg-dist/constants/chunks/package-registry.cjs.map +0 -1
  40. package/pkg-dist/constants/index.cjs.map +0 -1
  41. package/pkg-dist/index.cjs.map +0 -1
  42. package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.cjs.map +0 -1
  43. package/pkg-dist/interfaces/chunks/lit-action-config.i.cjs.map +0 -1
  44. package/pkg-dist/interfaces/chunks/lit-action-name.i.cjs.map +0 -1
  45. package/pkg-dist/interfaces/chunks/lit-action-registry.i.cjs.map +0 -1
  46. package/pkg-dist/interfaces/chunks/pkp-info.i.cjs.map +0 -1
  47. package/pkg-dist/interfaces/index.cjs.map +0 -1
  48. package/pkg-dist/utils/chunks/cid-utils.cjs.map +0 -1
  49. package/pkg-dist/utils/chunks/connection-helpers.cjs.map +0 -1
  50. package/pkg-dist/utils/chunks/debug-logger.cjs.map +0 -1
  51. package/pkg-dist/utils/chunks/error-classification.cjs.map +0 -1
  52. package/pkg-dist/utils/chunks/lit-action-helpers.cjs.map +0 -1
  53. package/pkg-dist/utils/chunks/pkp-setup.cjs.map +0 -1
  54. package/pkg-dist/utils/chunks/session-signature-cache.cjs.map +0 -1
  55. package/pkg-dist/utils/index.cjs.map +0 -1
  56. /package/pkg-dist/constants/chunks/{package-registry.cjs → package-registry.js} +0 -0
  57. /package/pkg-dist/constants/{index.cjs → index.js} +0 -0
  58. /package/pkg-dist/{index.cjs → index.js} +0 -0
  59. /package/pkg-dist/interfaces/chunks/{diamond-hands-lit-actions.i.cjs → diamond-hands-lit-actions.i.js} +0 -0
  60. /package/pkg-dist/interfaces/chunks/{lit-action-config.i.cjs → lit-action-config.i.js} +0 -0
  61. /package/pkg-dist/interfaces/chunks/{lit-action-name.i.cjs → lit-action-name.i.js} +0 -0
  62. /package/pkg-dist/interfaces/chunks/{lit-action-registry.i.cjs → lit-action-registry.i.js} +0 -0
  63. /package/pkg-dist/interfaces/chunks/{pkp-info.i.cjs → pkp-info.i.js} +0 -0
  64. /package/pkg-dist/interfaces/{index.cjs → index.js} +0 -0
  65. /package/pkg-dist/utils/chunks/{cid-utils.cjs → cid-utils.js} +0 -0
  66. /package/pkg-dist/utils/chunks/{connection-helpers.cjs → connection-helpers.js} +0 -0
  67. /package/pkg-dist/utils/chunks/{debug-logger.cjs → debug-logger.js} +0 -0
  68. /package/pkg-dist/utils/chunks/{error-classification.cjs → error-classification.js} +0 -0
  69. /package/pkg-dist/utils/chunks/{lit-action-helpers.cjs → lit-action-helpers.js} +0 -0
  70. /package/pkg-dist/utils/chunks/{pkp-setup.cjs → pkp-setup.js} +0 -0
  71. /package/pkg-dist/utils/chunks/{session-signature-cache.cjs → session-signature-cache.js} +0 -0
  72. /package/pkg-dist/utils/{index.cjs → index.js} +0 -0
@@ -1,1488 +0,0 @@
1
- // LIT Actions runtime provides: Lit, ethers, fetch
2
- var _LIT_ACTION_ = (() => {
3
- // src/constants/chunks/liquidation.ts
4
- var ACTIVE_LOAN_LIQUIDATION_THRESHOLD_BPS = 13e3;
5
- var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
6
- var EXPIRED_LOAN_MAX_LIQUIDATION_THRESHOLD_BPS = 2e4;
7
- var GRACE_PERIOD_DAYS = 30;
8
-
9
- // src/constants/chunks/decimals.ts
10
- var SATOSHIS_PER_BITCOIN = 100000000n;
11
- var PRICE_ORACLE_DECIMALS = 100000000n;
12
- var UCD_TOKEN_DECIMALS = 1000000000000000000n;
13
-
14
- // src/constants/chunks/bitcoin.ts
15
- var BITCOIN_DEFAULT_MIN_CONFIRMATIONS = 6;
16
- var MAX_BTC_AMOUNT = 25e6 * 1e8;
17
-
18
- // src/constants/chunks/quantum-time.ts
19
- var QUANTUM_WINDOW_SECONDS = 100;
20
- var DEAD_ZONE_SECONDS = 16;
21
- var SAFE_EXECUTION_WINDOW_SECONDS = QUANTUM_WINDOW_SECONDS - DEAD_ZONE_SECONDS;
22
-
23
- // deployments/deployment.sepolia.json
24
- var deployment_sepolia_default = {
25
- network: "sepolia",
26
- chainId: 11155111,
27
- timestamp: "2025-10-24T23:22:01.396Z",
28
- deployer: "0x2767441E044aCd9bbC21a759fB0517494875092d",
29
- contracts: {
30
- UpgradeValidator: "0x4246830c4bF03a6f4460ecEB9e6470562a5Cd0bB",
31
- UCDToken: "0x86060Fa5B3E01E003b8aFd9C1295F1F444a7dC60",
32
- UCDController: "0xEE24c338e394610b503768Ba7D8A8c26efdD3Aeb",
33
- PriceFeedConsumer: "0xA285198196B8f4f658DC81bf4e99A5DC6c191694",
34
- BTCProofValidator: "0x79469590b41AF8b21276914da7f18FF136B59afB",
35
- UCDMintingRewards: "0x914740bA9b454d1B3C116A9F55C0929487b7E052",
36
- PositionManagerCoreModule: "0x0f7E84538026Efdd118168b27f6da00035c75697",
37
- TermManagerModule: "0x73f0e9151bbEa3cB8b21628E2aAd54C3f6b3968A",
38
- LoanOperationsManagerModule: "0x71128A4e728ca4B6976E05aC77E50Ad7fe98Ae37",
39
- CollateralManagerModule: "0xAb9205f4b559F945Ae45c0857fc5c30d699509E1",
40
- LiquidationManagerModule: "0xe5A72334ED5e0F962be02229b36685d5Bb331cCF",
41
- CircuitBreakerModule: "0x55eE4Bb05008b73AebcAfa5FbC745348E645d8c6",
42
- CommunityManagerModule: "0x4E4481B0B3B08804C1d32A19Af66130426D82e89",
43
- AdminModule: "0xF96F14605f870A0cC2589A5d2b1D77E379762Af2",
44
- PositionManagerViews: "0x7bc05DBC27BBdab374E3FEBB4Efaa6CDE082fc27",
45
- PositionManager: "0xAC8d7b714865D4F304b80cd037b517796D56E4Ce",
46
- MockUSDC: "0x43670cD40c3c4a7acBA5B06B84b8Dd97D7E08f82",
47
- MockUSDT: "0xF8b7fB38b17a310960E47805Be9adba0b0e0394D",
48
- SimplePSMV2: "0x8C2D7C833b40eD23Bf1EABAf8B9105C80FD686A2",
49
- OperationAuthorizationRegistry: "0x400B85A6A901fdbF08f048cD5f3e37B8DD6c0D66"
50
- },
51
- upgraded: {
52
- LoanOperationsManagerModule: {
53
- previousImplementation: "0xFa3E83992d964E46d82Bb1F92844a8aC909130B2",
54
- newImplementation: "0xFa3E83992d964E46d82Bb1F92844a8aC909130B2",
55
- upgradedAt: "2025-10-24T23:22:01.396Z",
56
- upgradedBy: "0x2767441E044aCd9bbC21a759fB0517494875092d"
57
- }
58
- }
59
- };
60
-
61
- // deployments/deployment.localhost.json
62
- var deployment_localhost_default = {
63
- network: "localhost",
64
- chainId: 1337,
65
- timestamp: "2025-10-19T17:53:35.461Z",
66
- deployer: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
67
- contracts: {
68
- UpgradeValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
69
- UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
70
- UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
71
- PriceFeedConsumer: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
72
- BTCProofValidator: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
73
- UCDMintingRewards: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
74
- PositionManagerCoreModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
75
- TermManagerModule: "0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE",
76
- LoanOperationsManagerModule: "0x3Aa5ebB10DC797CAC828524e59A333d0A371443c",
77
- CollateralManagerModule: "0x59b670e9fA9D0A427751Af201D676719a970857b",
78
- LiquidationManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
79
- CircuitBreakerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
80
- CommunityManagerModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
81
- AdminModule: "0x67d269191c92Caf3cD7723F116c85e6E9bf55933",
82
- PositionManagerViews: "0xE6E340D132b5f46d1e472DebcD681B2aBc16e57E",
83
- PositionManager: "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB"
84
- }
85
- };
86
-
87
- // src/config/network-config.ts
88
- var NETWORK_DEPLOYMENTS = {
89
- sepolia: deployment_sepolia_default,
90
- localhost: deployment_localhost_default
91
- };
92
- function getNetworkConfig(network) {
93
- const normalizedNetwork = network.toLowerCase();
94
- if (!NETWORK_DEPLOYMENTS[normalizedNetwork]) {
95
- console.warn(
96
- `Warning: Network "${network}" not found, defaulting to sepolia`
97
- );
98
- return NETWORK_DEPLOYMENTS.sepolia;
99
- }
100
- return NETWORK_DEPLOYMENTS[normalizedNetwork];
101
- }
102
-
103
- // src/constants/chunks/bitcoin-network-config.ts
104
- var EVMChain = /* @__PURE__ */ ((EVMChain3) => {
105
- EVMChain3["SEPOLIA"] = "sepolia";
106
- EVMChain3["ETHEREUM"] = "ethereum";
107
- return EVMChain3;
108
- })(EVMChain || {});
109
- var CHAIN_TO_BITCOIN_NETWORK = {
110
- ["sepolia" /* SEPOLIA */]: "regtest" /* REGTEST */,
111
- ["ethereum" /* ETHEREUM */]: "mainnet" /* MAINNET */
112
- };
113
- var APPROVED_BITCOIN_PROVIDERS = {
114
- ["regtest" /* REGTEST */]: [
115
- {
116
- url: "https://diamond-hands-btc-faucet-6b39a1072059.herokuapp.com/api",
117
- network: "regtest" /* REGTEST */,
118
- minConfirmations: 1,
119
- name: "Diamond Hands"
120
- },
121
- {
122
- url: "http://diamond-hands-btc-faucet-6b39a1072059.herokuapp.com/api",
123
- network: "regtest" /* REGTEST */,
124
- minConfirmations: 1,
125
- name: "Diamond Hands"
126
- }
127
- ],
128
- ["testnet" /* TESTNET */]: [
129
- {
130
- url: "https://diamond-hands-btc-faucet-6b39a1072059.herokuapp.com/api",
131
- network: "testnet" /* TESTNET */,
132
- minConfirmations: 1,
133
- name: "Diamond Hands"
134
- },
135
- {
136
- url: "http://diamond-hands-btc-faucet-6b39a1072059.herokuapp.com/api",
137
- network: "testnet" /* TESTNET */,
138
- minConfirmations: 1,
139
- name: "Diamond Hands"
140
- }
141
- ],
142
- ["mainnet" /* MAINNET */]: [
143
- {
144
- url: "https://blockstream.info/api",
145
- network: "mainnet" /* MAINNET */,
146
- minConfirmations: 6,
147
- name: "Blockstream Mainnet"
148
- },
149
- {
150
- url: "https://mempool.space/api",
151
- network: "mainnet" /* MAINNET */,
152
- minConfirmations: 6,
153
- name: "Mempool.space Mainnet"
154
- }
155
- ]
156
- };
157
- function validateChain(chain) {
158
- const normalized = chain.toLowerCase().trim();
159
- if (normalized === "sepolia") {
160
- return "sepolia" /* SEPOLIA */;
161
- }
162
- if (normalized === "ethereum" || normalized === "mainnet") {
163
- return "ethereum" /* ETHEREUM */;
164
- }
165
- throw new Error(
166
- `Unsupported EVM chain: "${chain}". Supported chains: ${Object.values(EVMChain).join(", ")}`
167
- );
168
- }
169
- function getBitcoinNetworkForChain(chain) {
170
- return CHAIN_TO_BITCOIN_NETWORK[chain];
171
- }
172
- function validateBitcoinProvider(providerUrl, bitcoinNetwork) {
173
- const providers = APPROVED_BITCOIN_PROVIDERS[bitcoinNetwork];
174
- const config = providers.find((p) => p.url === providerUrl);
175
- if (!config) {
176
- const approvedUrls = providers.map((p) => `${p.name} (${p.url})`).join(", ");
177
- throw new Error(
178
- `Bitcoin provider not approved for ${bitcoinNetwork}. Provided URL: ${providerUrl}. Approved providers: ${approvedUrls}`
179
- );
180
- }
181
- return config;
182
- }
183
- function getBitcoinProviderConfig(chain, providerUrl) {
184
- const validatedChain = validateChain(chain);
185
- const bitcoinNetwork = getBitcoinNetworkForChain(validatedChain);
186
- const providerConfig = validateBitcoinProvider(providerUrl, bitcoinNetwork);
187
- if (providerConfig.network !== bitcoinNetwork) {
188
- throw new Error(
189
- `Bitcoin provider network mismatch. Provider ${providerConfig.name} is for ${providerConfig.network}, but chain ${chain} requires ${bitcoinNetwork}`
190
- );
191
- }
192
- return providerConfig;
193
- }
194
-
195
- // src/modules/bitcoin-data-provider.module.ts
196
- var DEFAULT_MIN_CONFIRMATIONS = BITCOIN_DEFAULT_MIN_CONFIRMATIONS;
197
- var BitcoinDataProvider = class {
198
- constructor(config) {
199
- this.config = config;
200
- this.timeout = config.timeout || 1e4;
201
- }
202
- /**
203
- * Fetch all UTXOs for a Bitcoin address
204
- *
205
- * @param address - Bitcoin address (legacy, segwit, or taproot)
206
- * @returns Array of UTXOs
207
- */
208
- async getUTXOs(address) {
209
- console.log(`[Bitcoin Data] Original address: "${address}"`);
210
- const cleanAddress = this.stripNetworkPrefix(address);
211
- console.log(`[Bitcoin Data] Cleaned address: "${cleanAddress}"`);
212
- if (this.config.rpcHelper) {
213
- return await this.fetchUTXOsFromRPC(cleanAddress);
214
- }
215
- try {
216
- return await this.fetchUTXOsFromProvider(
217
- this.config.providerUrl,
218
- cleanAddress
219
- );
220
- } catch (error) {
221
- console.warn(`[Bitcoin Data] Primary provider failed: ${error.message}`);
222
- if (this.config.fallbackProviders && this.config.fallbackProviders.length > 0) {
223
- const sortedFallbacks = [...this.config.fallbackProviders].sort(
224
- (a, b) => a.priority - b.priority
225
- );
226
- for (const fallback of sortedFallbacks) {
227
- try {
228
- console.log(
229
- `[Bitcoin Data] Trying fallback: ${fallback.name} (${fallback.url})`
230
- );
231
- return await this.fetchUTXOsFromProvider(fallback.url, cleanAddress);
232
- } catch (fallbackError) {
233
- console.warn(
234
- `[Bitcoin Data] Fallback ${fallback.name} failed: ${fallbackError.message}`
235
- );
236
- continue;
237
- }
238
- }
239
- }
240
- throw new Error(`Failed to fetch UTXOs: ${error.message}`);
241
- }
242
- }
243
- /**
244
- * Strip network prefix from Bitcoin address
245
- *
246
- * Addresses may be stored with network prefix (e.g., "REGTEST_address")
247
- * but APIs expect clean addresses without prefix.
248
- *
249
- * @param address - Address with optional prefix
250
- * @returns Clean address without prefix
251
- */
252
- stripNetworkPrefix(address) {
253
- const prefixPattern = /^(REGTEST_|TESTNET_|MAINNET_)/;
254
- return address.replace(prefixPattern, "");
255
- }
256
- /**
257
- * Fetch UTXOs using Bitcoin RPC (for regtest/testing)
258
- */
259
- async fetchUTXOsFromRPC(address) {
260
- if (!this.config.rpcHelper) {
261
- throw new Error("RPC helper not configured");
262
- }
263
- const wallet = this.config.rpcWallet || "";
264
- const unspentOutputs = await this.config.rpcHelper.listUnspent(
265
- wallet,
266
- address
267
- );
268
- const utxos = unspentOutputs.map((utxo) => ({
269
- txid: utxo.txid,
270
- vout: utxo.vout,
271
- satoshis: BigInt(Math.round(utxo.amount * 1e8)),
272
- // BTC to satoshis as bigint
273
- confirmations: utxo.confirmations
274
- }));
275
- return utxos;
276
- }
277
- /**
278
- * Parse Diamond Hands Faucet UTXO response
279
- *
280
- * Expected format:
281
- * {
282
- * "success": true,
283
- * "address": "...",
284
- * "utxos": [{txid, vout, amount, confirmations, scriptPubKey}],
285
- * "totalBalance": 33000000,
286
- * "utxoCount": 6
287
- * }
288
- *
289
- * @param data Raw response from faucet API
290
- * @returns Array of normalized UTXOs
291
- */
292
- parseDiamondHandsFaucetUTXOs(data) {
293
- if (!data.success) {
294
- throw new Error(
295
- `Diamond Hands Faucet error: ${data.error || data.message || "Unknown error"}`
296
- );
297
- }
298
- if (!Array.isArray(data.utxos)) {
299
- throw new Error(
300
- "Invalid Diamond Hands Faucet response: utxos must be an array"
301
- );
302
- }
303
- const utxos = data.utxos.map((utxo) => ({
304
- txid: utxo.txid,
305
- vout: utxo.vout,
306
- satoshis: BigInt(utxo.amount),
307
- // Faucet uses "amount" (satoshis)
308
- confirmations: utxo.confirmations || 0
309
- }));
310
- return utxos;
311
- }
312
- /**
313
- * Parse Blockstream/Mempool.space UTXO response
314
- *
315
- * Expected format: Array of {txid, vout, value, confirmations}
316
- *
317
- * @param data Raw response from provider API
318
- * @returns Array of normalized UTXOs
319
- */
320
- parseBlockstreamUTXOs(data) {
321
- if (!Array.isArray(data)) {
322
- throw new Error(
323
- "Invalid Blockstream response format: expected an array"
324
- );
325
- }
326
- const utxos = data.map((utxo) => ({
327
- txid: utxo.txid,
328
- vout: utxo.vout !== void 0 ? utxo.vout : utxo.n,
329
- satoshis: BigInt(utxo.value !== void 0 ? utxo.value : utxo.satoshis),
330
- confirmations: utxo.confirmations || 0
331
- }));
332
- return utxos;
333
- }
334
- /**
335
- * Internal method to fetch UTXOs from a specific provider
336
- *
337
- * Supports multiple provider API formats:
338
- * - Blockstream/Mempool.space: /api/address/{address}/utxos
339
- * - Diamond Hands Faucet: /api/faucet/utxos/{address}
340
- */
341
- async fetchUTXOsFromProvider(providerUrl, address) {
342
- const allProviders = Object.values(APPROVED_BITCOIN_PROVIDERS).flat();
343
- const matched = allProviders.find((p) => p.url === providerUrl);
344
- const providerName = matched?.name || providerUrl;
345
- let endpoint;
346
- if (providerName === "Diamond Hands") {
347
- endpoint = `${providerUrl}/faucet/balance/${address}/utxos`;
348
- } else {
349
- endpoint = `${providerUrl}/address/${address}/utxos`;
350
- }
351
- console.log(`Fetching UTXOs from ${endpoint}`);
352
- const controller = new AbortController();
353
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
354
- try {
355
- const response = await fetch(endpoint, {
356
- signal: controller.signal,
357
- headers: {
358
- "Accept": "application/json",
359
- "Content-Type": "application/json"
360
- }
361
- });
362
- clearTimeout(timeoutId);
363
- if (!response.ok) {
364
- throw new Error(
365
- `Bitcoin provider error: ${response.status} ${response.statusText}`
366
- );
367
- }
368
- const data = await response.json();
369
- console.log(`[Line 222 Context] Raw API Response:`, JSON.stringify(data, null, 2));
370
- if (providerName === "Diamond Hands") {
371
- return this.parseDiamondHandsFaucetUTXOs(data);
372
- } else {
373
- return this.parseBlockstreamUTXOs(data);
374
- }
375
- } catch (error) {
376
- clearTimeout(timeoutId);
377
- if (error.name === "AbortError") {
378
- throw new Error(`Request timeout after ${this.timeout}ms`);
379
- }
380
- throw error;
381
- }
382
- }
383
- /**
384
- * Get complete UTXO set with balance summary
385
- *
386
- * This is the SINGLE method that vault-balance module should call.
387
- * Returns everything needed to know about Bitcoin network state for an address.
388
- *
389
- * @param address - Bitcoin address
390
- * @param minConfirmations - Minimum confirmations for "confirmed" status (default: 6)
391
- * @returns Complete UTXO set with balance breakdown
392
- */
393
- async getUTXOSet(address, minConfirmations = DEFAULT_MIN_CONFIRMATIONS) {
394
- const utxos = await this.getUTXOs(address);
395
- const totalBalance = utxos.reduce((sum, utxo) => sum + utxo.satoshis, 0n);
396
- const confirmedBalance = utxos.filter((utxo) => utxo.confirmations >= minConfirmations).reduce((sum, utxo) => sum + utxo.satoshis, 0n);
397
- const unconfirmedBalance = totalBalance - confirmedBalance;
398
- return {
399
- utxos,
400
- totalBalance,
401
- totalUTXOs: utxos.length,
402
- confirmedBalance,
403
- unconfirmedBalance
404
- };
405
- }
406
- /**
407
- * Get transaction with confirmation count
408
- *
409
- * Checks if transaction exists on Bitcoin network and returns confirmation count.
410
- * Used to validate authorized spends have been broadcasted.
411
- *
412
- * @param txid - Transaction ID to check
413
- * @returns Transaction with confirmation count, or null if not found
414
- */
415
- async getTransaction(txid) {
416
- if (this.config.rpcHelper) {
417
- try {
418
- const wallet = this.config.rpcWallet || "";
419
- const tx = await this.config.rpcHelper.getTransaction(wallet, txid);
420
- return {
421
- txid: tx.txid,
422
- confirmations: tx.confirmations || 0
423
- };
424
- } catch (error) {
425
- if (error.message?.includes("Invalid or non-wallet transaction") || error.code === -5) {
426
- return null;
427
- }
428
- throw error;
429
- }
430
- }
431
- const controller = new AbortController();
432
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
433
- try {
434
- const response = await fetch(
435
- `${this.config.providerUrl}/api/tx/${txid}`,
436
- {
437
- signal: controller.signal
438
- }
439
- );
440
- clearTimeout(timeoutId);
441
- if (!response.ok) {
442
- if (response.status === 404) {
443
- return null;
444
- }
445
- throw new Error(
446
- `Bitcoin provider error: ${response.status} ${response.statusText}`
447
- );
448
- }
449
- const data = await response.json();
450
- if (data.status) {
451
- return {
452
- txid: data.txid,
453
- confirmations: data.status.confirmed ? data.status.block_height ? 1 : 0 : 0
454
- };
455
- }
456
- return null;
457
- } catch (error) {
458
- clearTimeout(timeoutId);
459
- if (error.name === "AbortError") {
460
- throw new Error(`Request timeout after ${this.timeout}ms`);
461
- }
462
- if (error.message?.includes("404")) {
463
- return null;
464
- }
465
- throw error;
466
- }
467
- }
468
- // FUTURE: Multi-provider consensus methods will be added here
469
- // async getUTXOsWithConsensus(address: string): Promise<UTXO[]> {
470
- // // Fetch from all providers
471
- // // Return UTXOs that appear in 2+ sources
472
- // // Use median value if amounts differ
473
- // }
474
- };
475
-
476
- // src/modules/price-oracle.module.ts
477
- var PriceOracleModule = class {
478
- constructor(options, sources) {
479
- this.mode = options?.mode || "full";
480
- this.sources = sources || this.getDefaultSources();
481
- this.sources.sort((a, b) => a.priority - b.priority);
482
- }
483
- getDefaultSources() {
484
- const DEFAULT_HEADERS = {
485
- "User-Agent": "diamond-hands-lit-action/1.0",
486
- Accept: "application/json"
487
- };
488
- const fetchJson = async (url) => {
489
- const controller = new AbortController();
490
- const timeoutMs = this.mode === "fast" ? 4e3 : 8e3;
491
- const id = setTimeout(() => controller.abort(), timeoutMs);
492
- try {
493
- const res = await fetch(url, { headers: DEFAULT_HEADERS, signal: controller.signal });
494
- if (!res.ok) {
495
- throw new Error(`HTTP ${res.status} ${res.statusText}`);
496
- }
497
- return await res.json();
498
- } finally {
499
- clearTimeout(id);
500
- }
501
- };
502
- return [
503
- {
504
- name: "CoinGecko",
505
- fetchPrice: async () => {
506
- const url = new URL("https://api.coingecko.com/api/v3/simple/price");
507
- url.searchParams.set("ids", "bitcoin");
508
- url.searchParams.set("vs_currencies", "usd");
509
- const data = await fetchJson(url.toString());
510
- const price = Number(data?.bitcoin?.usd);
511
- if (!Number.isFinite(price) || price <= 0) {
512
- throw new Error("Invalid CoinGecko price payload");
513
- }
514
- if (this.mode !== "fast") {
515
- const allPrices = await Lit.Actions.broadcastAndCollect({
516
- name: "coinGeckoPrice",
517
- value: price.toString()
518
- });
519
- const prices = allPrices.map((p) => parseFloat(p));
520
- prices.sort((a, b) => a - b);
521
- return prices[Math.floor(prices.length / 2)];
522
- }
523
- return price;
524
- },
525
- priority: 1
526
- },
527
- {
528
- name: "Binance",
529
- fetchPrice: async () => {
530
- const url = new URL("https://api.binance.com/api/v3/ticker/price");
531
- url.searchParams.set("symbol", "BTCUSDT");
532
- const data = await fetchJson(url.toString());
533
- const price = Number(data?.price);
534
- if (!Number.isFinite(price) || price <= 0) {
535
- throw new Error("Invalid Binance price payload");
536
- }
537
- if (this.mode !== "fast") {
538
- const allPrices = await Lit.Actions.broadcastAndCollect({
539
- name: "binancePrice",
540
- value: price.toString()
541
- });
542
- const prices = allPrices.map((p) => parseFloat(p));
543
- prices.sort((a, b) => a - b);
544
- return prices[Math.floor(prices.length / 2)];
545
- }
546
- return price;
547
- },
548
- priority: 2
549
- },
550
- {
551
- name: "Coinbase",
552
- fetchPrice: async () => {
553
- const data = await fetchJson("https://api.coinbase.com/v2/prices/BTC-USD/spot");
554
- const price = Number(data?.data?.amount);
555
- if (!Number.isFinite(price) || price <= 0) {
556
- throw new Error("Invalid Coinbase price payload");
557
- }
558
- if (this.mode !== "fast") {
559
- const allPrices = await Lit.Actions.broadcastAndCollect({
560
- name: "coinbasePrice",
561
- value: price.toString()
562
- });
563
- const prices = allPrices.map((p) => parseFloat(p));
564
- prices.sort((a, b) => a - b);
565
- return prices[Math.floor(prices.length / 2)];
566
- }
567
- return price;
568
- },
569
- priority: 3
570
- }
571
- ];
572
- }
573
- /**
574
- * Get BTC price in USD with 8 decimals
575
- * Fetches from real price sources in priority order
576
- */
577
- async getBTCPrice() {
578
- console.log(`[Price Oracle] Fetching BTC price from external sources...`);
579
- for (const source of this.sources) {
580
- try {
581
- console.log(`[Price Oracle] Trying ${source.name}...`);
582
- const priceUSD = await source.fetchPrice();
583
- console.log(
584
- `[Price Oracle] \u2705 ${source.name}: $${priceUSD.toLocaleString()}`
585
- );
586
- const priceWith8Decimals = BigInt(Math.floor(priceUSD * 1e8));
587
- console.log(
588
- `[Price Oracle] Price with 8 decimals: ${priceWith8Decimals}`
589
- );
590
- return priceWith8Decimals;
591
- } catch (error) {
592
- console.warn(
593
- `[Price Oracle] \u26A0\uFE0F ${source.name} failed: ${error.message}`
594
- );
595
- continue;
596
- }
597
- }
598
- throw new Error("All price sources failed");
599
- }
600
- /**
601
- * Consensus across multiple sources with outlier detection
602
- * Returns median price from valid sources after filtering outliers
603
- *
604
- * Master's Wisdom: Two-Path Validation
605
- * 1. If ONE source is outlier (>2% from median) → FILTER it, continue with remaining
606
- * 2. If ALL sources too dispersed (>5% spread) → REJECT all, throw error
607
- *
608
- * Logic Flow:
609
- * 1. Calculate initial median from all sources
610
- * 2. Filter individual outliers (>2% from median)
611
- * 3. If outliers found: remove them, require minimum 2 sources remaining
612
- * 4. Recalculate median from valid sources only
613
- * 5. Check if remaining sources are too dispersed (max/min ratio > 1.05)
614
- * 6. Return final median or throw appropriate error
615
- */
616
- async getBTCPriceConsensus() {
617
- console.log(`[Price Oracle] Fetching BTC price with consensus...`);
618
- const OUTLIER_DEVIATION = 5e-3;
619
- const DISPERSION_THRESHOLD = 0.05;
620
- const results = await Promise.allSettled(
621
- this.sources.map(async (source) => {
622
- const price = await source.fetchPrice();
623
- return { source: source.name, price };
624
- })
625
- );
626
- const successful = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
627
- if (successful.length === 0) {
628
- throw new Error("No price sources returned data");
629
- }
630
- console.log(
631
- `[Price Oracle] Got prices from ${successful.length}/${this.sources.length} sources:`
632
- );
633
- successful.forEach((s) => {
634
- console.log(` ${s.source}: $${s.price.toLocaleString()}`);
635
- });
636
- const prices = successful.map((s) => s.price);
637
- prices.sort((a, b) => a - b);
638
- const initialMedian = prices[Math.floor(prices.length / 2)];
639
- const minPrice = prices[0];
640
- const maxPrice = prices[prices.length - 1];
641
- const dispersionRatio = maxPrice / minPrice;
642
- const validPrices = successful.filter((s) => {
643
- const deviation = Math.abs(s.price - initialMedian) / initialMedian;
644
- return deviation <= OUTLIER_DEVIATION;
645
- });
646
- if (dispersionRatio > 1 + DISPERSION_THRESHOLD && validPrices.length === successful.length) {
647
- throw new Error(
648
- `Price consensus failed: sources too dispersed (${((dispersionRatio - 1) * 100).toFixed(1)}% spread)`
649
- );
650
- }
651
- if (validPrices.length < successful.length) {
652
- const outliers = successful.filter(
653
- (s) => !validPrices.find((v) => v.source === s.source)
654
- );
655
- console.log(`[Price Oracle] \u26A0\uFE0F Detected ${outliers.length} outlier(s):`);
656
- outliers.forEach((o) => {
657
- const deviation = Math.abs(o.price - initialMedian) / initialMedian;
658
- console.log(
659
- ` ${o.source}: $${o.price.toLocaleString()} (${(deviation * 100).toFixed(1)}% deviation)`
660
- );
661
- });
662
- if (validPrices.length < 2) {
663
- throw new Error(
664
- "Price consensus failed: insufficient valid sources after outlier removal"
665
- );
666
- }
667
- console.log(
668
- `[Price Oracle] \u2705 Outliers filtered, continuing with ${validPrices.length} valid sources`
669
- );
670
- const validPricesOnly = validPrices.map((v) => v.price);
671
- validPricesOnly.sort((a, b) => a - b);
672
- const finalMedian = validPricesOnly[Math.floor(validPricesOnly.length / 2)];
673
- console.log(
674
- `[Price Oracle] \u2705 Consensus price (median): $${finalMedian.toLocaleString()}`
675
- );
676
- return BigInt(Math.floor(finalMedian * 1e8));
677
- }
678
- console.log(
679
- `[Price Oracle] \u2705 Consensus price (median): $${initialMedian.toLocaleString()}`
680
- );
681
- return BigInt(Math.floor(initialMedian * 1e8));
682
- }
683
- };
684
-
685
- // src/modules/vault-balance.module.ts
686
- var VaultBalanceModule = class {
687
- constructor(config) {
688
- this.config = config;
689
- this.bitcoinProvider = config.bitcoinProvider;
690
- }
691
- /**
692
- * Calculate trusted balance for a position
693
- *
694
- * This is the main entry point for determining available balance.
695
- *
696
- * CRITICAL: Validates that all authorized spends have been properly broadcasted
697
- * to Bitcoin network with sufficient confirmations. Throws errors if:
698
- * 1. Authorized transaction not found on network (not broadcasted)
699
- * 2. Authorized transaction has insufficient confirmations (network updating)
700
- *
701
- * @param positionId - The position ID to check
702
- * @param vaultAddress - Bitcoin address of the vault
703
- * @returns Complete breakdown of vault balance
704
- * @throws Error if authorized spends are in invalid state
705
- */
706
- async calculateTrustedBalance(positionId, vaultAddress) {
707
- const minConfirmations = this.config.minConfirmations || 6;
708
- const utxoSet = await this.bitcoinProvider.getUTXOSet(
709
- vaultAddress,
710
- minConfirmations
711
- );
712
- const authorizedSpends = await this.getAuthorizedSpendsFromContract(positionId);
713
- for (const spend of authorizedSpends) {
714
- const utxoStillExists = utxoSet.utxos.some(
715
- (utxo) => utxo.txid === spend.txid && utxo.vout === spend.vout
716
- );
717
- if (utxoStillExists) {
718
- throw new Error(
719
- `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.`
720
- );
721
- }
722
- }
723
- const authorizedBalance = authorizedSpends.reduce(
724
- (sum, spend) => sum + spend.satoshis,
725
- 0n
726
- );
727
- const availableUTXOs = utxoSet.utxos.filter(
728
- (utxo) => !this.isUTXOAuthorized(utxo, authorizedSpends)
729
- );
730
- const availableBalance = availableUTXOs.reduce(
731
- (sum, utxo) => sum + utxo.satoshis,
732
- 0n
733
- );
734
- return {
735
- // Total from Bitcoin
736
- totalUTXOs: utxoSet.utxos,
737
- totalBalance: utxoSet.totalBalance,
738
- // Authorized from Contract
739
- authorizedUTXOs: authorizedSpends,
740
- authorizedBalance,
741
- // Available = Total - Authorized (THIS IS THE TRUSTED BALANCE)
742
- availableUTXOs,
743
- availableBalance,
744
- // Metadata
745
- vaultAddress,
746
- positionId,
747
- timestamp: Date.now()
748
- };
749
- }
750
- /**
751
- * Get just the trusted balance amount
752
- *
753
- * Convenience method when you only need the number.
754
- *
755
- * @param positionId - The position ID to check
756
- * @param vaultAddress - Bitcoin address of the vault
757
- * @returns Available balance in satoshis
758
- */
759
- async getTrustedBalance(positionId, vaultAddress) {
760
- const result = await this.calculateTrustedBalance(positionId, vaultAddress);
761
- return result.availableBalance;
762
- }
763
- /**
764
- * Get available UTXOs that can be used for new authorizations
765
- *
766
- * This is useful when you need to select specific UTXOs for authorization.
767
- *
768
- * @param positionId - The position ID to check
769
- * @param vaultAddress - Bitcoin address of the vault
770
- * @returns Array of available UTXOs
771
- */
772
- async getAvailableUTXOs(positionId, vaultAddress) {
773
- const result = await this.calculateTrustedBalance(positionId, vaultAddress);
774
- return result.availableUTXOs;
775
- }
776
- /**
777
- * Check if a specific UTXO is available for authorization
778
- *
779
- * @param positionId - The position ID to check
780
- * @param vaultAddress - Bitcoin address of the vault
781
- * @param txid - Transaction ID of the UTXO
782
- * @param vout - Output index of the UTXO
783
- * @returns True if UTXO is available, false otherwise
784
- */
785
- async isUTXOAvailable(positionId, vaultAddress, txid, vout) {
786
- const availableUTXOs = await this.getAvailableUTXOs(
787
- positionId,
788
- vaultAddress
789
- );
790
- return availableUTXOs.some(
791
- (utxo) => utxo.txid === txid && utxo.vout === vout
792
- );
793
- }
794
- /**
795
- * Check if a UTXO is authorized (appears in authorized spends)
796
- *
797
- * @param utxo - The UTXO to check
798
- * @param authorizedSpends - List of authorized spends from contract
799
- * @returns True if UTXO is authorized, false otherwise
800
- */
801
- isUTXOAuthorized(utxo, authorizedSpends) {
802
- return authorizedSpends.some(
803
- (spend) => spend.txid === utxo.txid && spend.vout === utxo.vout
804
- );
805
- }
806
- /**
807
- * Query smart contract for authorized spends
808
- *
809
- * Calls the contract's getAuthorizedSpends(positionId) view function
810
- * to retrieve all authorized Bitcoin spends for this position.
811
- *
812
- * Contract struct AuthorizedSpend:
813
- * - txid: string
814
- * - vout: uint32
815
- * - satoshis: uint256
816
- * - authorizedAt: uint256 (timestamp)
817
- *
818
- * @param positionId - The position ID to query
819
- * @returns Array of authorized spends for this position
820
- */
821
- async getAuthorizedSpendsFromContract(positionId) {
822
- const positionIdBytes32 = positionId.startsWith("0x") ? positionId : `0x${positionId.padStart(64, "0")}`;
823
- const getAuthorizedSpendsABI = [
824
- {
825
- inputs: [
826
- { internalType: "bytes32", name: "positionId", type: "bytes32" }
827
- ],
828
- name: "getAuthorizedSpends",
829
- outputs: [
830
- {
831
- components: [
832
- { internalType: "string", name: "txid", type: "string" },
833
- { internalType: "uint32", name: "vout", type: "uint32" },
834
- { internalType: "uint256", name: "satoshis", type: "uint256" },
835
- {
836
- internalType: "string",
837
- name: "targetAddress",
838
- type: "string"
839
- },
840
- {
841
- internalType: "uint256",
842
- name: "targetAmount",
843
- type: "uint256"
844
- },
845
- {
846
- internalType: "string",
847
- name: "changeAddress",
848
- type: "string"
849
- },
850
- {
851
- internalType: "uint256",
852
- name: "authorizedAt",
853
- type: "uint256"
854
- }
855
- ],
856
- internalType: "struct LoanOperationsManager.AuthorizedSpend[]",
857
- name: "",
858
- type: "tuple[]"
859
- }
860
- ],
861
- stateMutability: "view",
862
- type: "function"
863
- }
864
- ];
865
- let rpcUrl;
866
- if (this.config.rpcUrl) {
867
- rpcUrl = this.config.rpcUrl;
868
- } else {
869
- rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.config.chain });
870
- }
871
- const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
872
- const contract = new ethers.Contract(
873
- this.config.contractAddress,
874
- getAuthorizedSpendsABI,
875
- provider
876
- );
877
- const result = await contract.getAuthorizedSpends(positionIdBytes32);
878
- return result.map((spend) => ({
879
- txid: spend.txid,
880
- vout: Number(spend.vout),
881
- satoshis: BigInt(spend.satoshis.toString()),
882
- positionId,
883
- targetAddress: spend.targetAddress,
884
- targetAmount: BigInt(spend.targetAmount.toString()),
885
- changeAddress: spend.changeAddress,
886
- timestamp: Number(spend.authorizedAt)
887
- }));
888
- }
889
- };
890
- function createVaultBalanceModule(config) {
891
- return new VaultBalanceModule(config);
892
- }
893
-
894
- // src/modules/business-rules-math.module.ts
895
- function calculateTermStartFromExpiry(expiryTimestamp, termLengthMonths) {
896
- if (expiryTimestamp === 0) {
897
- return 0;
898
- }
899
- const SECONDS_PER_DAY = 86400;
900
- const DAYS_PER_MONTH = 30;
901
- const termDurationSeconds = termLengthMonths * DAYS_PER_MONTH * SECONDS_PER_DAY;
902
- return expiryTimestamp - termDurationSeconds;
903
- }
904
- function calculateTermStatus(termStartTimestamp, selectedTermMonths) {
905
- if (termStartTimestamp === 0) {
906
- return {
907
- termDurationDays: 0,
908
- termLengthDays: selectedTermMonths * 30,
909
- isExpired: false,
910
- daysUntilExpiry: selectedTermMonths * 30,
911
- daysIntoGracePeriod: 0
912
- };
913
- }
914
- const now = Math.floor(Date.now() / 1e3);
915
- const termLengthDays = selectedTermMonths * 30;
916
- const termDurationSeconds = now - termStartTimestamp;
917
- const termDurationDays = Math.floor(termDurationSeconds / 86400);
918
- const isExpired = termDurationDays > termLengthDays;
919
- const daysUntilExpiry = Math.max(0, termLengthDays - termDurationDays);
920
- const daysIntoGracePeriod = Math.max(0, termDurationDays - termLengthDays);
921
- return {
922
- termDurationDays,
923
- termLengthDays,
924
- isExpired,
925
- daysUntilExpiry,
926
- daysIntoGracePeriod
927
- };
928
- }
929
- function calculateLiquidationThreshold(isExpired, daysIntoGracePeriod) {
930
- if (!isExpired) {
931
- return ACTIVE_LOAN_LIQUIDATION_THRESHOLD_BPS;
932
- }
933
- const day = Math.min(daysIntoGracePeriod, GRACE_PERIOD_DAYS);
934
- const daySquared = day * day;
935
- const escalation = daySquared * (EXPIRED_LOAN_MAX_LIQUIDATION_THRESHOLD_BPS - EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS) / (GRACE_PERIOD_DAYS * GRACE_PERIOD_DAYS);
936
- const threshold = EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS + escalation;
937
- return threshold;
938
- }
939
- function calculateCollateralMetrics(availableBTCSats, btcPriceUsd, ucdDebt) {
940
- if (ucdDebt === 0n) {
941
- return {
942
- collateralValueUsd: 0n,
943
- collateralRatioBps: Number.MAX_SAFE_INTEGER
944
- };
945
- }
946
- const collateralValueUsd = availableBTCSats * btcPriceUsd / (SATOSHIS_PER_BITCOIN * PRICE_ORACLE_DECIMALS);
947
- const ratioBigInt = collateralValueUsd * UCD_TOKEN_DECIMALS * 10000n / ucdDebt;
948
- const collateralRatioBps = Number(ratioBigInt);
949
- return {
950
- collateralValueUsd,
951
- collateralRatioBps
952
- };
953
- }
954
- function hasSufficientCollateralForDebt(collateralValueUsd, currentDebt, additionalDebt, requiredRatioBps) {
955
- const newTotalDebt = currentDebt + additionalDebt;
956
- if (newTotalDebt === 0n) {
957
- return true;
958
- }
959
- const newCollateralRatioBps = Number(
960
- collateralValueUsd * UCD_TOKEN_DECIMALS * 10000n / newTotalDebt
961
- );
962
- return newCollateralRatioBps >= requiredRatioBps;
963
- }
964
-
965
- // src/modules/protocol-parameters.module.ts
966
- var ProtocolParametersModule = class {
967
- constructor(config) {
968
- this.termManagerAddress = config.termManagerAddress;
969
- this.loanOpsManagerAddress = config.loanOpsManagerAddress;
970
- this.chain = config.chain;
971
- }
972
- /**
973
- * Get liquidation threshold from LoanOperationsManager
974
- *
975
- * @returns Liquidation threshold in basis points
976
- */
977
- async getLiquidationThreshold() {
978
- try {
979
- const rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.chain });
980
- const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
981
- const abi = [
982
- {
983
- inputs: [],
984
- name: "liquidationThreshold",
985
- outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
986
- stateMutability: "view",
987
- type: "function"
988
- }
989
- ];
990
- const contract = new ethers.Contract(this.loanOpsManagerAddress, abi, provider);
991
- const result = await contract.liquidationThreshold();
992
- return Number(result.toString());
993
- } catch (error) {
994
- console.error(
995
- "[ProtocolParameters] Error fetching liquidation threshold:",
996
- error.message
997
- );
998
- throw new Error(
999
- `Failed to fetch liquidation threshold: ${error.message}`
1000
- );
1001
- }
1002
- }
1003
- /**
1004
- * Get term-specific fees from TermManager
1005
- *
1006
- * @param termMonths Loan term in months
1007
- * @returns Term fees (origination and extension) in basis points
1008
- */
1009
- async getTermFees(termMonths) {
1010
- try {
1011
- const rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.chain });
1012
- const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
1013
- const abi = [
1014
- {
1015
- inputs: [
1016
- { internalType: "uint256", name: "_termMonths", type: "uint256" }
1017
- ],
1018
- name: "getTermFees",
1019
- outputs: [
1020
- { internalType: "uint88", name: "originationFee", type: "uint88" },
1021
- { internalType: "uint88", name: "extensionFee", type: "uint88" }
1022
- ],
1023
- stateMutability: "view",
1024
- type: "function"
1025
- }
1026
- ];
1027
- const contract = new ethers.Contract(this.termManagerAddress, abi, provider);
1028
- const result = await contract.getTermFees(termMonths);
1029
- return {
1030
- originationFeeBps: Number(result.originationFee?.toString?.() ?? result[0]?.toString?.() ?? result[0]),
1031
- extensionFeeBps: Number(result.extensionFee?.toString?.() ?? result[1]?.toString?.() ?? result[1])
1032
- };
1033
- } catch (error) {
1034
- console.error(
1035
- "[ProtocolParameters] Error fetching term fees:",
1036
- error.message
1037
- );
1038
- throw new Error(`Failed to fetch term fees: ${error.message}`);
1039
- }
1040
- }
1041
- };
1042
- function createProtocolParametersModule(config) {
1043
- return new ProtocolParametersModule(config);
1044
- }
1045
-
1046
- // src/modules/vault-snapshot.ts
1047
- var VaultSnapshotModule = class {
1048
- constructor(config) {
1049
- this.config = config;
1050
- }
1051
- /**
1052
- * Get complete vault snapshot
1053
- *
1054
- * This is the main entry point. Returns everything you need to know
1055
- * about a vault's health in a single call.
1056
- *
1057
- * @param positionId - Position to snapshot
1058
- * @returns Complete VaultSnapshot with all metrics
1059
- */
1060
- async getVaultSnapshot(positionId) {
1061
- const positionState = await this.queryPositionState(positionId);
1062
- console.log(`[Vault Snapshot] Raw vault address from contract: "${positionState.vaultAddress}"`);
1063
- const balanceResult = await this.config.vaultBalance.calculateTrustedBalance(
1064
- positionId,
1065
- positionState.vaultAddress
1066
- );
1067
- const btcPriceUsd = await this.config.priceOracle.getBTCPrice();
1068
- const protocolParams = createProtocolParametersModule({
1069
- termManagerAddress: this.config.termManagerAddress,
1070
- loanOpsManagerAddress: this.config.loanOpsManagerAddress,
1071
- chain: this.config.chain
1072
- });
1073
- const [liquidationThresholdBps, termFees] = await Promise.all([
1074
- protocolParams.getLiquidationThreshold(),
1075
- protocolParams.getTermFees(positionState.selectedTerm)
1076
- ]);
1077
- const termStatus = calculateTermStatus(
1078
- positionState.termStartTimestamp,
1079
- positionState.selectedTerm
1080
- );
1081
- const currentLiquidationThreshold = calculateLiquidationThreshold(
1082
- termStatus.isExpired,
1083
- termStatus.daysIntoGracePeriod
1084
- );
1085
- const collateralMetrics = calculateCollateralMetrics(
1086
- balanceResult.availableBalance,
1087
- btcPriceUsd,
1088
- positionState.ucdDebt
1089
- );
1090
- const isLiquidatable = collateralMetrics.collateralRatioBps < currentLiquidationThreshold;
1091
- const marginToLiquidationBps = collateralMetrics.collateralRatioBps - currentLiquidationThreshold;
1092
- return {
1093
- // Raw state
1094
- positionId: positionState.positionId,
1095
- pkpId: positionState.pkpId,
1096
- borrower: positionState.borrower,
1097
- vaultAddress: positionState.vaultAddress,
1098
- ucdDebt: positionState.ucdDebt,
1099
- termStartTimestamp: positionState.termStartTimestamp,
1100
- selectedTerm: positionState.selectedTerm,
1101
- isActive: positionState.isActive,
1102
- status: positionState.status,
1103
- // Loan status enum value (0-7)
1104
- // Bitcoin state
1105
- totalBTCSats: balanceResult.totalBalance,
1106
- totalUTXOs: balanceResult.totalUTXOs,
1107
- authorizedSpendsSats: balanceResult.authorizedBalance,
1108
- availableBTCSats: balanceResult.availableBalance,
1109
- availableUTXOs: balanceResult.availableUTXOs,
1110
- // Price data
1111
- btcPriceUsd,
1112
- // Calculated metrics
1113
- collateralValueUsd: collateralMetrics.collateralValueUsd,
1114
- collateralRatioBps: collateralMetrics.collateralRatioBps,
1115
- // Term status
1116
- termDurationDays: termStatus.termDurationDays,
1117
- termLengthDays: termStatus.termLengthDays,
1118
- isExpired: termStatus.isExpired,
1119
- daysUntilExpiry: termStatus.daysUntilExpiry,
1120
- daysIntoGracePeriod: termStatus.daysIntoGracePeriod,
1121
- // Liquidation risk
1122
- currentLiquidationThreshold,
1123
- isLiquidatable,
1124
- marginToLiquidationBps,
1125
- // Protocol parameters (from smart contracts)
1126
- liquidationThresholdBps,
1127
- originationFeeBps: termFees.originationFeeBps,
1128
- extensionFeeBps: termFees.extensionFeeBps,
1129
- // Metadata
1130
- timestamp: Date.now()
1131
- };
1132
- }
1133
- /**
1134
- * Query position state from smart contract
1135
- *
1136
- * Gets the simple state that contract stores:
1137
- * - positionId, pkpId, borrower, vaultAddress
1138
- * - ucdDebt
1139
- * - termStartTimestamp
1140
- * - selectedTerm
1141
- * - isActive
1142
- * - status
1143
- */
1144
- async queryPositionState(positionId) {
1145
- const positionIdBytes32 = positionId.startsWith("0x") ? positionId : `0x${positionId.padStart(64, "0")}`;
1146
- const getPositionDetailsABI = [
1147
- {
1148
- inputs: [
1149
- { internalType: "bytes32", name: "positionId", type: "bytes32" }
1150
- ],
1151
- name: "getPositionDetails",
1152
- outputs: [
1153
- {
1154
- components: [
1155
- { internalType: "bytes32", name: "positionId", type: "bytes32" },
1156
- { internalType: "bytes32", name: "pkpId", type: "bytes32" },
1157
- { internalType: "uint256", name: "ucdDebt", type: "uint256" },
1158
- {
1159
- internalType: "string",
1160
- name: "vaultAddress",
1161
- type: "string"
1162
- },
1163
- { internalType: "address", name: "borrower", type: "address" },
1164
- { internalType: "uint40", name: "createdAt", type: "uint40" },
1165
- { internalType: "uint40", name: "lastUpdated", type: "uint40" },
1166
- {
1167
- internalType: "uint16",
1168
- name: "selectedTerm",
1169
- type: "uint16"
1170
- },
1171
- { internalType: "uint40", name: "expiryAt", type: "uint40" },
1172
- { internalType: "bool", name: "isActive", type: "bool" },
1173
- {
1174
- internalType: "enum LoanStatusLib.LoanStatus",
1175
- name: "status",
1176
- type: "uint8"
1177
- }
1178
- ],
1179
- internalType: "struct IPositionManagerCore.Position",
1180
- name: "",
1181
- type: "tuple"
1182
- }
1183
- ],
1184
- stateMutability: "view",
1185
- type: "function"
1186
- }
1187
- ];
1188
- let rpcUrl;
1189
- if (this.config.rpcUrl) {
1190
- rpcUrl = this.config.rpcUrl;
1191
- } else {
1192
- rpcUrl = await Lit.Actions.getRpcUrl({ chain: this.config.chain });
1193
- }
1194
- const provider = new ethers.providers.JsonRpcProvider(rpcUrl);
1195
- const contract = new ethers.Contract(
1196
- this.config.contractAddress,
1197
- getPositionDetailsABI,
1198
- provider
1199
- );
1200
- const position = await contract.getPositionDetails(positionIdBytes32);
1201
- const expiryAt = Number(position.expiryAt);
1202
- const selectedTermMonths = Number(position.selectedTerm);
1203
- const termStartTimestamp = calculateTermStartFromExpiry(
1204
- expiryAt,
1205
- selectedTermMonths
1206
- );
1207
- return {
1208
- positionId: position.positionId,
1209
- pkpId: position.pkpId,
1210
- borrower: position.borrower,
1211
- vaultAddress: this.parseVaultAddress(
1212
- position.vaultAddress,
1213
- validateChain(this.config.chain)
1214
- ),
1215
- ucdDebt: BigInt(position.ucdDebt.toString()),
1216
- termStartTimestamp,
1217
- selectedTerm: selectedTermMonths,
1218
- isActive: position.isActive,
1219
- status: Number(position.status)
1220
- // Extract loan status enum value (0-7)
1221
- };
1222
- }
1223
- /**
1224
- * Parse vault address based on format and network
1225
- *
1226
- * Supports two formats:
1227
- * 1. Single string: "mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"
1228
- * 2. JSON object: {"mainnet":"bc1q...","testnet":"mqM3ZR2N6DzZkafSdFqxEAkWovAU3uYTAN"}
1229
- *
1230
- * @param rawVaultAddress Raw vault address from contract
1231
- * @param chain EVM chain (sepolia, ethereum)
1232
- * @returns Parsed Bitcoin address for the appropriate network
1233
- */
1234
- parseVaultAddress(rawVaultAddress, chain) {
1235
- try {
1236
- const parsed = JSON.parse(rawVaultAddress);
1237
- if (typeof parsed === "object" && parsed !== null) {
1238
- const chainLower = chain.toLowerCase();
1239
- if (chainLower === "sepolia") {
1240
- return parsed.regtest || parsed.testnet || parsed.mainnet;
1241
- }
1242
- const bitcoinNetwork = getBitcoinNetworkForChain(chain);
1243
- if (bitcoinNetwork === "testnet" /* TESTNET */) {
1244
- return parsed.testnet || parsed.regtest || parsed.mainnet;
1245
- } else {
1246
- return parsed.mainnet;
1247
- }
1248
- }
1249
- } catch (e) {
1250
- }
1251
- return rawVaultAddress;
1252
- }
1253
- /**
1254
- * Quick check if position is liquidatable
1255
- *
1256
- * Convenience method when you only need to know if position can be liquidated.
1257
- *
1258
- * @param positionId - Position to check
1259
- * @returns True if position is liquidatable
1260
- */
1261
- async isLiquidatable(positionId) {
1262
- const snapshot = await this.getVaultSnapshot(positionId);
1263
- return snapshot.isLiquidatable;
1264
- }
1265
- /**
1266
- * Get collateral ratio for position
1267
- *
1268
- * Convenience method when you only need the collateral ratio.
1269
- *
1270
- * @param positionId - Position to check
1271
- * @returns Collateral ratio in basis points
1272
- */
1273
- async getCollateralRatio(positionId) {
1274
- const snapshot = await this.getVaultSnapshot(positionId);
1275
- return snapshot.collateralRatioBps;
1276
- }
1277
- /**
1278
- * Get available BTC balance
1279
- *
1280
- * Convenience method when you only need available balance.
1281
- *
1282
- * @param positionId - Position to check
1283
- * @returns Available BTC in satoshis
1284
- */
1285
- async getAvailableBalance(positionId) {
1286
- const snapshot = await this.getVaultSnapshot(positionId);
1287
- return snapshot.availableBTCSats;
1288
- }
1289
- /**
1290
- * Check if position has sufficient collateral for new debt
1291
- *
1292
- * Used by minting authorization to validate new mints.
1293
- *
1294
- * @param positionId - Position to check
1295
- * @param additionalDebt - Additional UCD debt to add
1296
- * @param requiredRatioBps - Required collateral ratio (e.g., 13000 for 130%)
1297
- * @returns True if sufficient collateral exists
1298
- */
1299
- async hasSufficientCollateral(positionId, additionalDebt, requiredRatioBps) {
1300
- const snapshot = await this.getVaultSnapshot(positionId);
1301
- return hasSufficientCollateralForDebt(
1302
- snapshot.collateralValueUsd,
1303
- snapshot.ucdDebt,
1304
- additionalDebt,
1305
- requiredRatioBps
1306
- );
1307
- }
1308
- };
1309
- function createVaultSnapshotModule(config) {
1310
- return new VaultSnapshotModule(config);
1311
- }
1312
-
1313
- // src/btc-deposit-validator.ts
1314
- var go = async () => {
1315
- const MIN_BTC_PRICE_USD = 1000000000000n;
1316
- const MAX_BTC_PRICE_USD = 100000000000000n;
1317
- console.log("[BTC Deposit Validator] Starting validation...");
1318
- const { auth, publicKey, chain, bitcoinProviderUrl } = globalThis;
1319
- if (!auth || typeof auth !== "object") throw new Error("auth is required");
1320
- if (!auth.positionId) throw new Error("auth.positionId is required");
1321
- if (!auth.callerAddress) throw new Error("auth.callerAddress is required");
1322
- if (typeof auth.timestamp !== "number") throw new Error("auth.timestamp must be number");
1323
- if (!publicKey || typeof publicKey !== "string") throw new Error("publicKey is required");
1324
- if (!chain || typeof chain !== "string") throw new Error("chain is required");
1325
- console.log(` Chain: ${chain}`);
1326
- validateChain(chain);
1327
- let POSITION_MANAGER_ADDRESS = "";
1328
- let TERM_MANAGER_ADDRESS = "";
1329
- let LOAN_OPS_MANAGER_ADDRESS = "";
1330
- let bitcoinProviderConfig;
1331
- if (!globalThis.signOnly) {
1332
- const networkConfig = getNetworkConfig(chain);
1333
- POSITION_MANAGER_ADDRESS = networkConfig.contracts.PositionManager;
1334
- TERM_MANAGER_ADDRESS = networkConfig.contracts.TermManagerModule;
1335
- LOAN_OPS_MANAGER_ADDRESS = networkConfig.contracts.LoanOperationsManagerModule;
1336
- bitcoinProviderConfig = getBitcoinProviderConfig(chain, bitcoinProviderUrl);
1337
- console.log(` Bitcoin network: ${bitcoinProviderConfig.network}`);
1338
- console.log(` Min confirmations: ${bitcoinProviderConfig.minConfirmations}`);
1339
- } else {
1340
- console.log(" signOnly=true \u2192 skipping network and contract initialization");
1341
- }
1342
- let vaultBalanceSats = 0n;
1343
- let priceOracle;
1344
- if (!globalThis.signOnly) {
1345
- console.log("[Step 1] Initializing Bitcoin data provider...");
1346
- const bitcoinProvider = new BitcoinDataProvider({
1347
- providerUrl: bitcoinProviderConfig.url
1348
- });
1349
- priceOracle = new PriceOracleModule({ mode: globalThis.disablePriceCheck ? "fast" : "fast" });
1350
- const vaultBalance = createVaultBalanceModule({
1351
- contractAddress: LOAN_OPS_MANAGER_ADDRESS,
1352
- chain,
1353
- bitcoinProvider,
1354
- minConfirmations: bitcoinProviderConfig.minConfirmations
1355
- });
1356
- console.log("[Step 2] Getting vault snapshot...");
1357
- const vaultSnapshot = createVaultSnapshotModule({
1358
- contractAddress: POSITION_MANAGER_ADDRESS,
1359
- termManagerAddress: TERM_MANAGER_ADDRESS,
1360
- loanOpsManagerAddress: LOAN_OPS_MANAGER_ADDRESS,
1361
- chain,
1362
- vaultBalance,
1363
- priceOracle
1364
- });
1365
- const snapshot = await vaultSnapshot.getVaultSnapshot(auth.positionId);
1366
- vaultBalanceSats = snapshot.availableBTCSats;
1367
- if (globalThis.disablePriceCheck) {
1368
- console.log("[Step 3] Skipping BTC price validation (disablePriceCheck=true)");
1369
- } else {
1370
- console.log("[Step 3] Validating BTC price...");
1371
- if (snapshot.btcPriceUsd < MIN_BTC_PRICE_USD || snapshot.btcPriceUsd > MAX_BTC_PRICE_USD) {
1372
- throw new Error(
1373
- `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.`
1374
- );
1375
- }
1376
- }
1377
- console.log(` \u2705 BTC price validation passed or skipped`);
1378
- console.log(` Vault balance: ${vaultBalanceSats} sats`);
1379
- } else {
1380
- const override = globalThis.vaultBalanceOverride;
1381
- vaultBalanceSats = override ? BigInt(override) : 1n;
1382
- console.log(`[signOnly] Using vaultBalanceSats=${vaultBalanceSats.toString()} for message`);
1383
- }
1384
- console.log("[Step 3a] Validating BTC amount bounds...");
1385
- if (vaultBalanceSats > MAX_BTC_AMOUNT) {
1386
- throw new Error(
1387
- `BTC amount ${vaultBalanceSats} exceeds maximum allowed ${MAX_BTC_AMOUNT}. This may indicate corrupted data or malicious oracle manipulation.`
1388
- );
1389
- }
1390
- console.log(` \u2705 BTC amount within bounds (max: ${MAX_BTC_AMOUNT} sats)`);
1391
- if (!globalThis.signOnly && vaultBalanceSats === 0n) {
1392
- console.log(" \u2139\uFE0F No balance detected in vault");
1393
- Lit.Actions.setResponse({
1394
- response: JSON.stringify({
1395
- ok: true,
1396
- hasBalance: false,
1397
- vaultBalance: "0",
1398
- positionId: auth.positionId,
1399
- callerAddress: auth.callerAddress,
1400
- timestamp: auth.timestamp,
1401
- validatorPkp: publicKey
1402
- })
1403
- });
1404
- return;
1405
- }
1406
- if (!globalThis.signOnly) {
1407
- console.log(` \u2705 Vault has balance: ${vaultBalanceSats} sats`);
1408
- } else {
1409
- console.log(` \u2705 signOnly: proceeding to sign without balance check`);
1410
- }
1411
- console.log("[Step 5] Building authorization message...");
1412
- const message = ethers.utils.keccak256(
1413
- // @ts-ignore - ethers is provided in the LIT runtime
1414
- ethers.utils.defaultAbiCoder.encode(
1415
- ["bytes32", "uint256", "uint256", "address"],
1416
- [
1417
- auth.positionId,
1418
- vaultBalanceSats.toString(),
1419
- auth.timestamp,
1420
- auth.callerAddress
1421
- ]
1422
- )
1423
- );
1424
- console.log("[Step 6] Signing authorization...");
1425
- console.log(" \u{1F510} PKP Signing Parameters:");
1426
- console.log(" publicKey:", publicKey);
1427
- console.log(" message hash:", message);
1428
- console.log(" message length:", message.length);
1429
- console.log(" sigName: depositAuth");
1430
- try {
1431
- const signatureStartTime = Date.now();
1432
- console.log(" \u23F1\uFE0F Starting Lit.Actions.signEcdsa() call...");
1433
- let signature;
1434
- try {
1435
- signature = await Lit.Actions.signEcdsa({
1436
- // @ts-ignore - ethers is provided in the LIT runtime
1437
- toSign: ethers.utils.arrayify(message),
1438
- publicKey,
1439
- sigName: "depositAuth"
1440
- });
1441
- } catch (e) {
1442
- Lit.Actions.setResponse({
1443
- response: JSON.stringify({
1444
- ok: false,
1445
- error: `Signing failed: ${e?.message || String(e)}`,
1446
- positionId: auth.positionId,
1447
- timestamp: auth.timestamp
1448
- })
1449
- });
1450
- return;
1451
- }
1452
- const signatureDuration = Date.now() - signatureStartTime;
1453
- console.log(` \u2705 Lit.Actions.signEcdsa() completed in ${signatureDuration}ms`);
1454
- console.log(" \u{1F4DD} Signature result:", JSON.stringify(signature, null, 2));
1455
- console.log("[BTC Deposit Validator] \u2705 Complete");
1456
- Lit.Actions.setResponse({
1457
- response: JSON.stringify({
1458
- ok: true,
1459
- hasBalance: !globalThis.signOnly ? true : void 0,
1460
- message,
1461
- signature,
1462
- vaultBalance: vaultBalanceSats.toString(),
1463
- positionId: auth.positionId,
1464
- callerAddress: auth.callerAddress,
1465
- timestamp: auth.timestamp,
1466
- validatorPkp: publicKey,
1467
- // btcPriceUsd omitted in signOnly mode
1468
- ...priceOracle ? { btcPriceUsd: void 0 } : {}
1469
- })
1470
- });
1471
- } catch (error) {
1472
- const errorMessage = error instanceof Error ? error.message : String(error);
1473
- console.error(" \u274C PKP signing failed:", errorMessage);
1474
- console.error(" Error details:", error);
1475
- Lit.Actions.setResponse({
1476
- response: JSON.stringify({
1477
- ok: false,
1478
- error: `PKP signing failed: ${errorMessage}`,
1479
- positionId: auth.positionId,
1480
- callerAddress: auth.callerAddress,
1481
- timestamp: auth.timestamp,
1482
- validatorPkp: publicKey
1483
- })
1484
- });
1485
- }
1486
- };
1487
- go();
1488
- })();