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