@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.
- package/package.json +6 -13
- package/pkg-dist/constants/chunks/lit-actions-registry.d.ts.map +1 -1
- package/pkg-dist/constants/chunks/lit-actions-registry.js +274 -0
- package/pkg-dist/constants/chunks/lit-actions-registry.js.map +1 -0
- package/pkg-dist/constants/chunks/package-registry.js.map +1 -0
- package/pkg-dist/constants/index.js.map +1 -0
- package/pkg-dist/index.js.map +1 -0
- package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.d.ts +6 -0
- package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.d.ts.map +1 -1
- package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.js.map +1 -0
- package/pkg-dist/interfaces/chunks/lit-action-config.i.js.map +1 -0
- package/pkg-dist/interfaces/chunks/lit-action-name.i.js.map +1 -0
- package/pkg-dist/interfaces/chunks/lit-action-registry.i.js.map +1 -0
- package/pkg-dist/interfaces/chunks/pkp-info.i.js.map +1 -0
- package/pkg-dist/interfaces/index.js.map +1 -0
- package/pkg-dist/package.json +1 -0
- package/pkg-dist/utils/chunks/cid-utils.js.map +1 -0
- package/pkg-dist/utils/chunks/connection-helpers.js.map +1 -0
- package/pkg-dist/utils/chunks/debug-logger.js.map +1 -0
- package/pkg-dist/utils/chunks/error-classification.js.map +1 -0
- package/pkg-dist/utils/chunks/lit-action-helpers.js.map +1 -0
- package/pkg-dist/utils/chunks/pkp-setup.js.map +1 -0
- package/pkg-dist/utils/chunks/session-signature-cache.js.map +1 -0
- package/pkg-dist/utils/index.js.map +1 -0
- package/out/authorization-dummy-b.hash +0 -1
- package/out/authorization-dummy-b.js +0 -44
- package/out/authorization-dummy.hash +0 -1
- package/out/authorization-dummy.js +0 -64
- package/out/btc-deposit-validator.hash +0 -1
- package/out/btc-deposit-validator.js +0 -1488
- package/out/pkp-validator-datil.hash +0 -1
- package/out/pkp-validator-datil.js +0 -232
- package/out/pkp-validator.hash +0 -1
- package/out/pkp-validator.js +0 -410
- package/out/ucd-mint-validator.hash +0 -1
- package/out/ucd-mint-validator.js +0 -2203
- package/pkg-dist/constants/chunks/lit-actions-registry.cjs +0 -175
- package/pkg-dist/constants/chunks/lit-actions-registry.cjs.map +0 -1
- package/pkg-dist/constants/chunks/package-registry.cjs.map +0 -1
- package/pkg-dist/constants/index.cjs.map +0 -1
- package/pkg-dist/index.cjs.map +0 -1
- package/pkg-dist/interfaces/chunks/diamond-hands-lit-actions.i.cjs.map +0 -1
- package/pkg-dist/interfaces/chunks/lit-action-config.i.cjs.map +0 -1
- package/pkg-dist/interfaces/chunks/lit-action-name.i.cjs.map +0 -1
- package/pkg-dist/interfaces/chunks/lit-action-registry.i.cjs.map +0 -1
- package/pkg-dist/interfaces/chunks/pkp-info.i.cjs.map +0 -1
- package/pkg-dist/interfaces/index.cjs.map +0 -1
- package/pkg-dist/utils/chunks/cid-utils.cjs.map +0 -1
- package/pkg-dist/utils/chunks/connection-helpers.cjs.map +0 -1
- package/pkg-dist/utils/chunks/debug-logger.cjs.map +0 -1
- package/pkg-dist/utils/chunks/error-classification.cjs.map +0 -1
- package/pkg-dist/utils/chunks/lit-action-helpers.cjs.map +0 -1
- package/pkg-dist/utils/chunks/pkp-setup.cjs.map +0 -1
- package/pkg-dist/utils/chunks/session-signature-cache.cjs.map +0 -1
- package/pkg-dist/utils/index.cjs.map +0 -1
- /package/pkg-dist/constants/chunks/{package-registry.cjs → package-registry.js} +0 -0
- /package/pkg-dist/constants/{index.cjs → index.js} +0 -0
- /package/pkg-dist/{index.cjs → index.js} +0 -0
- /package/pkg-dist/interfaces/chunks/{diamond-hands-lit-actions.i.cjs → diamond-hands-lit-actions.i.js} +0 -0
- /package/pkg-dist/interfaces/chunks/{lit-action-config.i.cjs → lit-action-config.i.js} +0 -0
- /package/pkg-dist/interfaces/chunks/{lit-action-name.i.cjs → lit-action-name.i.js} +0 -0
- /package/pkg-dist/interfaces/chunks/{lit-action-registry.i.cjs → lit-action-registry.i.js} +0 -0
- /package/pkg-dist/interfaces/chunks/{pkp-info.i.cjs → pkp-info.i.js} +0 -0
- /package/pkg-dist/interfaces/{index.cjs → index.js} +0 -0
- /package/pkg-dist/utils/chunks/{cid-utils.cjs → cid-utils.js} +0 -0
- /package/pkg-dist/utils/chunks/{connection-helpers.cjs → connection-helpers.js} +0 -0
- /package/pkg-dist/utils/chunks/{debug-logger.cjs → debug-logger.js} +0 -0
- /package/pkg-dist/utils/chunks/{error-classification.cjs → error-classification.js} +0 -0
- /package/pkg-dist/utils/chunks/{lit-action-helpers.cjs → lit-action-helpers.js} +0 -0
- /package/pkg-dist/utils/chunks/{pkp-setup.cjs → pkp-setup.js} +0 -0
- /package/pkg-dist/utils/chunks/{session-signature-cache.cjs → session-signature-cache.js} +0 -0
- /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
|
-
})();
|