@gvnrdao/dh-lit-actions 0.0.32 → 0.0.34

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