@gvnrdao/dh-sdk 0.0.307 → 0.0.309
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/browser/dist/browser.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +507 -140
- package/dist/index.mjs +432 -62
- package/dist/modules/diamond-hands-sdk.d.ts +62 -17
- package/dist/utils/server-session.d.ts +5 -0
- package/dist/utils/stale-spend-invalidate-message.d.ts +46 -0
- package/dist/utils/withdrawal-reconciliation.utils.d.ts +3 -2
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1514,6 +1514,19 @@ var require_pkg_src = __commonJS({
|
|
|
1514
1514
|
publicKey: "0x046da0ad6da7dd4a0063cbeac6f2bdb6a9889c2178b23470833356a8ee5630f7f0eee0db348d9d9aaaadd23eccf0063c8f68ee5e446abe30b882bf1de4c932bc21",
|
|
1515
1515
|
ethAddress: "0xc7e1588Db94Bbe92Ab209B833235144Fc5b97607"
|
|
1516
1516
|
}
|
|
1517
|
+
},
|
|
1518
|
+
btcUtxoInvalidator: {
|
|
1519
|
+
cid: "QmRue1EAjuJwCDTtTH4czdkkmDEfNpT7iudvwtjaRs6AdA",
|
|
1520
|
+
authorizedCidHex: cidToHex(
|
|
1521
|
+
"QmRue1EAjuJwCDTtTH4czdkkmDEfNpT7iudvwtjaRs6AdA"
|
|
1522
|
+
),
|
|
1523
|
+
name: "Btc Utxo Invalidator",
|
|
1524
|
+
description: "Production Btc Utxo Invalidator",
|
|
1525
|
+
version: "1.0.0",
|
|
1526
|
+
deployed: true,
|
|
1527
|
+
deployedAt: 1784801043540,
|
|
1528
|
+
size: 60717,
|
|
1529
|
+
hash: "2ca9736edf3d54bba7445491920cba28429029c32a2f2830cad831a37a3cb310"
|
|
1517
1530
|
}
|
|
1518
1531
|
};
|
|
1519
1532
|
function getDeploymentsForNetwork(network) {
|
|
@@ -2259,7 +2272,7 @@ ${errorReport}`);
|
|
|
2259
2272
|
}
|
|
2260
2273
|
init_debug_logger();
|
|
2261
2274
|
init_session_signature_cache();
|
|
2262
|
-
var
|
|
2275
|
+
var import_ethers17 = __require("ethers");
|
|
2263
2276
|
var EXPIRED_LOAN_MIN_LIQUIDATION_THRESHOLD_BPS = 11e3;
|
|
2264
2277
|
var GRACE_PERIOD_DAYS = 30;
|
|
2265
2278
|
var SATOSHIS_PER_BITCOIN = 100000000n;
|
|
@@ -5089,12 +5102,12 @@ ${auth.clientId}`;
|
|
|
5089
5102
|
}
|
|
5090
5103
|
async function executeVaultSnapshot(params) {
|
|
5091
5104
|
global.ethers = {
|
|
5092
|
-
...
|
|
5105
|
+
...import_ethers17.ethers,
|
|
5093
5106
|
providers: {
|
|
5094
|
-
StaticJsonRpcProvider:
|
|
5107
|
+
StaticJsonRpcProvider: import_ethers17.ethers.JsonRpcProvider
|
|
5095
5108
|
},
|
|
5096
|
-
Contract:
|
|
5097
|
-
utils:
|
|
5109
|
+
Contract: import_ethers17.ethers.Contract,
|
|
5110
|
+
utils: import_ethers17.ethers
|
|
5098
5111
|
// v6 moved utils to top level
|
|
5099
5112
|
};
|
|
5100
5113
|
global.Lit = {
|
|
@@ -5552,7 +5565,7 @@ var init_network_configs = __esm({
|
|
|
5552
5565
|
});
|
|
5553
5566
|
|
|
5554
5567
|
// src/modules/diamond-hands-sdk.ts
|
|
5555
|
-
import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as
|
|
5568
|
+
import { Contract as Contract6, Interface as Interface4, JsonRpcProvider, AbiCoder as AbiCoder2, keccak256 as keccak2566, solidityPackedKeccak256 as solidityPackedKeccak2565, toUtf8Bytes as toUtf8Bytes6, getBytes as getBytes5, zeroPadValue as zeroPadValue5, toBeHex as toBeHex2, parseEther, formatEther, formatUnits, computeAddress, Signature, ZeroHash, ZeroAddress as ZeroAddress3, MaxUint256 } from "ethers";
|
|
5556
5569
|
|
|
5557
5570
|
// src/types/result.ts
|
|
5558
5571
|
function success(value) {
|
|
@@ -6413,7 +6426,7 @@ async function generateExtendAuthorization(positionId, newTerm, chainId, signerO
|
|
|
6413
6426
|
import { hexlify, randomBytes } from "ethers";
|
|
6414
6427
|
function buildLoginDomain(chainId) {
|
|
6415
6428
|
return {
|
|
6416
|
-
name: "Diamond Hands
|
|
6429
|
+
name: "Diamond Hands",
|
|
6417
6430
|
version: "1",
|
|
6418
6431
|
chainId
|
|
6419
6432
|
};
|
|
@@ -6528,6 +6541,8 @@ function createDefaultSessionStore() {
|
|
|
6528
6541
|
|
|
6529
6542
|
// src/utils/server-session.ts
|
|
6530
6543
|
var REFRESH_LEEWAY_SECONDS = 30;
|
|
6544
|
+
var LOGIN_RETRY_BASE_MS = 3e4;
|
|
6545
|
+
var LOGIN_RETRY_MAX_MS = 5 * 6e4;
|
|
6531
6546
|
var ServerLoginError = class extends Error {
|
|
6532
6547
|
constructor(message, status) {
|
|
6533
6548
|
super(message);
|
|
@@ -6548,6 +6563,11 @@ var ServerSession = class {
|
|
|
6548
6563
|
fetchImpl;
|
|
6549
6564
|
cached = null;
|
|
6550
6565
|
inFlight = null;
|
|
6566
|
+
/**
|
|
6567
|
+
* Set when a prompted login failed; suppresses further prompts until `until`.
|
|
6568
|
+
* `error` is replayed to callers so they still see the real failure reason.
|
|
6569
|
+
*/
|
|
6570
|
+
promptBackoff = null;
|
|
6551
6571
|
/**
|
|
6552
6572
|
* Bumped by `clearPersisted()` (logout / disconnect). An in-flight
|
|
6553
6573
|
* `getOrRefresh()` captures this at start and refuses to write its freshly
|
|
@@ -6622,6 +6642,7 @@ var ServerSession = class {
|
|
|
6622
6642
|
this.generation += 1;
|
|
6623
6643
|
this.cached = null;
|
|
6624
6644
|
this.inFlight = null;
|
|
6645
|
+
this.promptBackoff = null;
|
|
6625
6646
|
if (!this.store)
|
|
6626
6647
|
return;
|
|
6627
6648
|
try {
|
|
@@ -6718,19 +6739,39 @@ var ServerSession = class {
|
|
|
6718
6739
|
this.inFlight = (async () => {
|
|
6719
6740
|
try {
|
|
6720
6741
|
const existing = await this.tryResolveSilently(gen);
|
|
6721
|
-
if (existing)
|
|
6742
|
+
if (existing) {
|
|
6743
|
+
this.promptBackoff = null;
|
|
6722
6744
|
return existing;
|
|
6745
|
+
}
|
|
6746
|
+
if (this.promptBackoff && this.now() * 1e3 < this.promptBackoff.until) {
|
|
6747
|
+
throw this.promptBackoff.error;
|
|
6748
|
+
}
|
|
6723
6749
|
this.onSignaturePrompt?.();
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
this.
|
|
6732
|
-
|
|
6750
|
+
let session;
|
|
6751
|
+
try {
|
|
6752
|
+
const payload = await buildSignedLoginPayload(
|
|
6753
|
+
this.signer,
|
|
6754
|
+
this.chainId,
|
|
6755
|
+
this.serviceEndpoint
|
|
6756
|
+
);
|
|
6757
|
+
session = await this.login(payload);
|
|
6758
|
+
if (notCleared()) {
|
|
6759
|
+
this.persist(payload, session);
|
|
6760
|
+
this.cached = session;
|
|
6761
|
+
}
|
|
6762
|
+
} catch (error) {
|
|
6763
|
+
const failures = (this.promptBackoff?.failures ?? 0) + 1;
|
|
6764
|
+
this.promptBackoff = {
|
|
6765
|
+
failures,
|
|
6766
|
+
error,
|
|
6767
|
+
until: this.now() * 1e3 + Math.min(
|
|
6768
|
+
LOGIN_RETRY_BASE_MS * 2 ** (failures - 1),
|
|
6769
|
+
LOGIN_RETRY_MAX_MS
|
|
6770
|
+
)
|
|
6771
|
+
};
|
|
6772
|
+
throw error;
|
|
6733
6773
|
}
|
|
6774
|
+
this.promptBackoff = null;
|
|
6734
6775
|
return session;
|
|
6735
6776
|
} finally {
|
|
6736
6777
|
if (notCleared())
|
|
@@ -7068,10 +7109,87 @@ async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = def
|
|
|
7068
7109
|
};
|
|
7069
7110
|
}
|
|
7070
7111
|
|
|
7071
|
-
// src/utils/
|
|
7072
|
-
import {
|
|
7112
|
+
// src/utils/stale-spend-invalidate-message.ts
|
|
7113
|
+
import {
|
|
7114
|
+
keccak256 as keccak2563,
|
|
7115
|
+
toUtf8Bytes as toUtf8Bytes3,
|
|
7116
|
+
solidityPackedKeccak256 as solidityPackedKeccak2563,
|
|
7117
|
+
getBytes as getBytes3,
|
|
7118
|
+
zeroPadValue
|
|
7119
|
+
} from "ethers";
|
|
7120
|
+
var RECOVERY_INVALIDATE_ACTION = "btc-utxo-invalidator";
|
|
7073
7121
|
var QUANTUM_SECONDS = 60;
|
|
7074
7122
|
var QUANTUM_SAFE_THRESHOLD = 40;
|
|
7123
|
+
function buildInvalidateMessageHash(params) {
|
|
7124
|
+
const positionIdBytes32 = zeroPadValue(
|
|
7125
|
+
params.positionId.startsWith("0x") ? params.positionId : `0x${params.positionId}`,
|
|
7126
|
+
32
|
|
7127
|
+
);
|
|
7128
|
+
const actionHash = keccak2563(toUtf8Bytes3(RECOVERY_INVALIDATE_ACTION));
|
|
7129
|
+
return solidityPackedKeccak2563(
|
|
7130
|
+
[
|
|
7131
|
+
"bytes32",
|
|
7132
|
+
// positionId
|
|
7133
|
+
"uint256",
|
|
7134
|
+
// timestamp
|
|
7135
|
+
"uint256",
|
|
7136
|
+
// chainId
|
|
7137
|
+
"string",
|
|
7138
|
+
// utxoTxid (display order)
|
|
7139
|
+
"uint32",
|
|
7140
|
+
// utxoVout
|
|
7141
|
+
"bytes32"
|
|
7142
|
+
// actionHash
|
|
7143
|
+
],
|
|
7144
|
+
[
|
|
7145
|
+
positionIdBytes32,
|
|
7146
|
+
params.timestamp,
|
|
7147
|
+
params.chainId,
|
|
7148
|
+
params.utxoTxid,
|
|
7149
|
+
params.utxoVout,
|
|
7150
|
+
actionHash
|
|
7151
|
+
]
|
|
7152
|
+
);
|
|
7153
|
+
}
|
|
7154
|
+
async function buildInvalidateStaleSpendEnvelope(params) {
|
|
7155
|
+
const { positionId, utxoTxid, utxoVout, chainId, signer } = params;
|
|
7156
|
+
const nowSec = Date.now() / 1e3;
|
|
7157
|
+
const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
|
|
7158
|
+
const secsIntoQuantum = nowSec - currentQuantumStart;
|
|
7159
|
+
let timestamp;
|
|
7160
|
+
if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
|
|
7161
|
+
const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
|
|
7162
|
+
const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
|
|
7163
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
7164
|
+
timestamp = nextQuantumStart;
|
|
7165
|
+
} else {
|
|
7166
|
+
timestamp = currentQuantumStart;
|
|
7167
|
+
}
|
|
7168
|
+
const messageHash = buildInvalidateMessageHash({
|
|
7169
|
+
positionId,
|
|
7170
|
+
timestamp,
|
|
7171
|
+
chainId,
|
|
7172
|
+
utxoTxid,
|
|
7173
|
+
utxoVout
|
|
7174
|
+
});
|
|
7175
|
+
const signature = await signer.signMessage(getBytes3(messageHash));
|
|
7176
|
+
const borrowerAddress = await signer.getAddress();
|
|
7177
|
+
return {
|
|
7178
|
+
positionId,
|
|
7179
|
+
utxoTxid,
|
|
7180
|
+
utxoVout,
|
|
7181
|
+
timestamp,
|
|
7182
|
+
chainId,
|
|
7183
|
+
action: RECOVERY_INVALIDATE_ACTION,
|
|
7184
|
+
signature,
|
|
7185
|
+
borrowerAddress
|
|
7186
|
+
};
|
|
7187
|
+
}
|
|
7188
|
+
|
|
7189
|
+
// src/utils/btc-withdrawal-message.ts
|
|
7190
|
+
import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, solidityPackedKeccak256 as solidityPackedKeccak2564, getBytes as getBytes4, zeroPadValue as zeroPadValue2 } from "ethers";
|
|
7191
|
+
var QUANTUM_SECONDS2 = 60;
|
|
7192
|
+
var QUANTUM_SAFE_THRESHOLD2 = 40;
|
|
7075
7193
|
function hashBtcInputSet(utxos) {
|
|
7076
7194
|
if (!Array.isArray(utxos) || utxos.length === 0) {
|
|
7077
7195
|
throw new Error("hashBtcInputSet: at least one input is required");
|
|
@@ -7100,7 +7218,7 @@ function hashBtcInputSet(utxos) {
|
|
|
7100
7218
|
seen.add(key);
|
|
7101
7219
|
}
|
|
7102
7220
|
const canonical = normalized.sort((a, b) => a.txid < b.txid ? -1 : a.txid > b.txid ? 1 : a.vout - b.vout).map((u) => `${u.txid}:${u.vout}:${u.value}`).join("|");
|
|
7103
|
-
return
|
|
7221
|
+
return keccak2564(toUtf8Bytes4(canonical));
|
|
7104
7222
|
}
|
|
7105
7223
|
async function buildBtcExecuteEnvelope(params) {
|
|
7106
7224
|
const {
|
|
@@ -7134,26 +7252,26 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
7134
7252
|
}
|
|
7135
7253
|
const utxosHash = hashBtcInputSet(utxos);
|
|
7136
7254
|
const nowSec = Date.now() / 1e3;
|
|
7137
|
-
const currentQuantumStart = Math.floor(nowSec /
|
|
7255
|
+
const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS2) * QUANTUM_SECONDS2;
|
|
7138
7256
|
const secsIntoQuantum = nowSec - currentQuantumStart;
|
|
7139
7257
|
let timestamp;
|
|
7140
|
-
if (secsIntoQuantum >
|
|
7141
|
-
const nextQuantumStart = currentQuantumStart +
|
|
7258
|
+
if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD2) {
|
|
7259
|
+
const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS2;
|
|
7142
7260
|
const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
|
|
7143
7261
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
7144
7262
|
timestamp = nextQuantumStart;
|
|
7145
7263
|
} else {
|
|
7146
7264
|
timestamp = currentQuantumStart;
|
|
7147
7265
|
}
|
|
7148
|
-
const actionHash =
|
|
7149
|
-
|
|
7266
|
+
const actionHash = keccak2564(
|
|
7267
|
+
toUtf8Bytes4("execute-btc-withdrawal")
|
|
7150
7268
|
);
|
|
7151
7269
|
const utxoIdentifier = `${txid}:${vout}`;
|
|
7152
|
-
const positionIdBytes32 =
|
|
7270
|
+
const positionIdBytes32 = zeroPadValue2(
|
|
7153
7271
|
positionId.startsWith("0x") ? positionId : `0x${positionId}`,
|
|
7154
7272
|
32
|
|
7155
7273
|
);
|
|
7156
|
-
const messageHash =
|
|
7274
|
+
const messageHash = solidityPackedKeccak2564(
|
|
7157
7275
|
[
|
|
7158
7276
|
"bytes32",
|
|
7159
7277
|
// positionId
|
|
@@ -7186,7 +7304,7 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
7186
7304
|
actionHash
|
|
7187
7305
|
]
|
|
7188
7306
|
);
|
|
7189
|
-
const userSignature = await signer.signMessage(
|
|
7307
|
+
const userSignature = await signer.signMessage(getBytes4(messageHash));
|
|
7190
7308
|
const borrowerAddress = await signer.getAddress();
|
|
7191
7309
|
return {
|
|
7192
7310
|
positionId,
|
|
@@ -7250,7 +7368,7 @@ function maybeTemperSignature(signature, shouldTemper, operationName) {
|
|
|
7250
7368
|
}
|
|
7251
7369
|
|
|
7252
7370
|
// src/utils/address-conversion.utils.ts
|
|
7253
|
-
import { zeroPadValue as
|
|
7371
|
+
import { zeroPadValue as zeroPadValue3 } from "ethers";
|
|
7254
7372
|
|
|
7255
7373
|
// src/utils/chunks/bitcoin-utils.ts
|
|
7256
7374
|
import { sha256 as nobleSha256 } from "@noble/hashes/sha256";
|
|
@@ -8834,7 +8952,7 @@ function createPKPManager(config) {
|
|
|
8834
8952
|
}
|
|
8835
8953
|
|
|
8836
8954
|
// src/modules/loan/loan-creator.module.ts
|
|
8837
|
-
import { Interface as Interface2, zeroPadValue as
|
|
8955
|
+
import { Interface as Interface2, zeroPadValue as zeroPadValue4, toBeHex, SigningKey } from "ethers";
|
|
8838
8956
|
var import_dh_lit_actions = __toESM(require_pkg_src());
|
|
8839
8957
|
var LoanCreator = class {
|
|
8840
8958
|
config;
|
|
@@ -9164,7 +9282,7 @@ var LoanCreator = class {
|
|
|
9164
9282
|
try {
|
|
9165
9283
|
const mainnetVaultAddress = bitcoinAddresses.mainnet;
|
|
9166
9284
|
const regtestVaultAddress = bitcoinAddresses.regtest;
|
|
9167
|
-
const pkpIdBytes32 =
|
|
9285
|
+
const pkpIdBytes32 = zeroPadValue4(
|
|
9168
9286
|
toBeHex(BigInt(pkpData.tokenId)),
|
|
9169
9287
|
32
|
|
9170
9288
|
);
|
|
@@ -9345,7 +9463,7 @@ var LoanCreator = class {
|
|
|
9345
9463
|
}
|
|
9346
9464
|
}
|
|
9347
9465
|
if (this.config.debug) {
|
|
9348
|
-
const pkpIdBytes32Check =
|
|
9466
|
+
const pkpIdBytes32Check = zeroPadValue4(
|
|
9349
9467
|
toBeHex(BigInt(pkpData.tokenId)),
|
|
9350
9468
|
32
|
|
9351
9469
|
);
|
|
@@ -10108,7 +10226,7 @@ function createLoanQuery(config) {
|
|
|
10108
10226
|
}
|
|
10109
10227
|
|
|
10110
10228
|
// src/modules/withdrawal-address/withdrawal-address.module.ts
|
|
10111
|
-
import { keccak256 as
|
|
10229
|
+
import { keccak256 as keccak2565, toUtf8Bytes as toUtf8Bytes5, ZeroAddress as ZeroAddress2 } from "ethers";
|
|
10112
10230
|
var WithdrawalAddressModule = class {
|
|
10113
10231
|
config;
|
|
10114
10232
|
graphClient;
|
|
@@ -10143,7 +10261,7 @@ var WithdrawalAddressModule = class {
|
|
|
10143
10261
|
}
|
|
10144
10262
|
/** keccak256 of the canonicalized address — matches the on-chain key. */
|
|
10145
10263
|
static addressHash(btcAddress) {
|
|
10146
|
-
return
|
|
10264
|
+
return keccak2565(toUtf8Bytes5(normalizeBitcoinAddress(btcAddress)));
|
|
10147
10265
|
}
|
|
10148
10266
|
/**
|
|
10149
10267
|
* Add a Bitcoin address to the caller's allowlist. It becomes usable only
|
|
@@ -17592,8 +17710,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
17592
17710
|
throw new Error(`Position not found: ${request.positionId}`);
|
|
17593
17711
|
}
|
|
17594
17712
|
const currentDebt = position.ucdDebt.toString();
|
|
17595
|
-
const ucdDebtHash =
|
|
17596
|
-
const contractHash =
|
|
17713
|
+
const ucdDebtHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
|
|
17714
|
+
const contractHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(
|
|
17597
17715
|
["address", "address", "address", "address"],
|
|
17598
17716
|
[
|
|
17599
17717
|
positionManagerAddress,
|
|
@@ -17670,7 +17788,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
17670
17788
|
const currentPosition = await positionCore["getPositionDetails"](
|
|
17671
17789
|
positionIdBytes32
|
|
17672
17790
|
);
|
|
17673
|
-
const currentDebtHash =
|
|
17791
|
+
const currentDebtHash = keccak2566(
|
|
17674
17792
|
AbiCoder2.defaultAbiCoder().encode(
|
|
17675
17793
|
["uint256"],
|
|
17676
17794
|
[currentPosition.ucdDebt]
|
|
@@ -21406,7 +21524,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21406
21524
|
* been broadcast to the Bitcoin network. An empty array means no pending withdrawals.
|
|
21407
21525
|
*
|
|
21408
21526
|
* @param positionId - Position identifier
|
|
21409
|
-
* @returns Array of pending withdrawals, each including a pre-computed utxoKey
|
|
21527
|
+
* @returns Array of pending withdrawals, each including a pre-computed utxoKey (the key recoverStaleSpend and the admin clearing path operate on)
|
|
21410
21528
|
*/
|
|
21411
21529
|
async getPendingWithdrawals(positionId) {
|
|
21412
21530
|
this.ensureInitialized();
|
|
@@ -21438,7 +21556,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21438
21556
|
targetAddress: spend.targetAddress,
|
|
21439
21557
|
targetAmount: Number(spend.targetAmount),
|
|
21440
21558
|
authorizedAt: Number(spend.authorizedAt),
|
|
21441
|
-
utxoKey:
|
|
21559
|
+
utxoKey: solidityPackedKeccak2565(
|
|
21442
21560
|
["string", "uint32"],
|
|
21443
21561
|
[spend.txid, Number(spend.vout)]
|
|
21444
21562
|
)
|
|
@@ -21451,11 +21569,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21451
21569
|
* broadcast happened. Callers MUST use the returned `status` to decide what
|
|
21452
21570
|
* to offer:
|
|
21453
21571
|
* - EXECUTABLE → offer Execute (the only status that may).
|
|
21454
|
-
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof
|
|
21455
|
-
*
|
|
21572
|
+
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof —
|
|
21573
|
+
* feed it to recoverStaleSpend to clear the reservation.
|
|
21574
|
+
* - SPENT_MISMATCH → unexecutable; recoverStaleSpend with the spending txid
|
|
21575
|
+
* as invalidator clears the reservation.
|
|
21456
21576
|
* - CORRUPT → authorization contradicts the chain (e.g. declared
|
|
21457
|
-
* satoshis ≠ real output value);
|
|
21458
|
-
*
|
|
21577
|
+
* satoshis ≠ real output value); Execute can only die at
|
|
21578
|
+
* the signer guard, and no borrower-side cancel exists
|
|
21579
|
+
* (audit #62210) — clearing needs the AdminModule
|
|
21580
|
+
* cancelStaleSpendByAdmin operator override.
|
|
21459
21581
|
* - UNFUNDED → funding tx unknown/unconfirmed; wait.
|
|
21460
21582
|
*
|
|
21461
21583
|
* @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
|
|
@@ -21479,18 +21601,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21479
21601
|
spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
|
|
21480
21602
|
);
|
|
21481
21603
|
}
|
|
21482
|
-
|
|
21483
|
-
|
|
21484
|
-
|
|
21485
|
-
|
|
21486
|
-
|
|
21487
|
-
|
|
21488
|
-
|
|
21489
|
-
* @param positionId - Position identifier
|
|
21490
|
-
* @param txid - Bitcoin UTXO transaction ID (from getPendingWithdrawals)
|
|
21491
|
-
* @param vout - Output index (from getPendingWithdrawals)
|
|
21492
|
-
* @returns Transaction hash of the cancellation EVM transaction
|
|
21493
|
-
*/
|
|
21604
|
+
// NOTE: there is deliberately no cancelPendingWithdrawal. Borrower-callable
|
|
21605
|
+
// cleanup was permanently disabled by audit #62210 (markSpentByOwner /
|
|
21606
|
+
// cancelStaleSpendByOwner revert with BorrowerBTCSpendMutationDisabled — a
|
|
21607
|
+
// borrower cancel could be replayed to mint UCD against the same UTXO).
|
|
21608
|
+
// Reservations clear via recoverStaleSpend below (trustless attestation once
|
|
21609
|
+
// the UTXO is provably consumed) or the AdminModule-timelocked
|
|
21610
|
+
// cancelStaleSpendByAdmin operator override.
|
|
21494
21611
|
/**
|
|
21495
21612
|
* Recover (clear) a stale BTC reservation via LIT-attested proof.
|
|
21496
21613
|
*
|
|
@@ -21642,6 +21759,256 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21642
21759
|
};
|
|
21643
21760
|
}
|
|
21644
21761
|
}
|
|
21762
|
+
/**
|
|
21763
|
+
* Sign + broadcast the PKP vault→vault self-spend that invalidates one
|
|
21764
|
+
* stuck authorized outpoint (btc-utxo-invalidator 1.0.0). Once the returned
|
|
21765
|
+
* `invalidatorTxid` reaches >=6 Bitcoin confirmations, `recoverStaleSpend`
|
|
21766
|
+
* clears the on-chain reservation — the trustless replacement for the
|
|
21767
|
+
* AdminModule cancelStaleSpendByAdmin path on unspent stuck/corrupt
|
|
21768
|
+
* reservations (incident 2026-07-22). Requires the borrower (or position
|
|
21769
|
+
* delegate) signer; the signature is scoped to exactly this outpoint.
|
|
21770
|
+
*/
|
|
21771
|
+
async invalidateStaleSpend(params) {
|
|
21772
|
+
this.ensureInitialized();
|
|
21773
|
+
const chainId = this.config.chainId ?? this.config.networkOverride?.chainId;
|
|
21774
|
+
if (!chainId) {
|
|
21775
|
+
return {
|
|
21776
|
+
success: false,
|
|
21777
|
+
failedStep: "config",
|
|
21778
|
+
error: "chainId required for invalidateStaleSpend"
|
|
21779
|
+
};
|
|
21780
|
+
}
|
|
21781
|
+
const signer = this.getSignerOrThrow();
|
|
21782
|
+
const envelope = await buildInvalidateStaleSpendEnvelope({
|
|
21783
|
+
positionId: params.positionId,
|
|
21784
|
+
utxoTxid: params.utxoTxid,
|
|
21785
|
+
utxoVout: params.utxoVout,
|
|
21786
|
+
chainId,
|
|
21787
|
+
signer
|
|
21788
|
+
});
|
|
21789
|
+
if (this.config.mode === "service" && this.config.serviceEndpoint) {
|
|
21790
|
+
const headers = {
|
|
21791
|
+
"Content-Type": "application/json",
|
|
21792
|
+
...await this.getAuthHeader()
|
|
21793
|
+
};
|
|
21794
|
+
const resp = await fetch(
|
|
21795
|
+
`${this.config.serviceEndpoint}/api/lit/stale-spend/invalidate`,
|
|
21796
|
+
{
|
|
21797
|
+
method: "POST",
|
|
21798
|
+
headers,
|
|
21799
|
+
body: JSON.stringify({
|
|
21800
|
+
positionId: this.toBytes32(params.positionId),
|
|
21801
|
+
utxoTxid: params.utxoTxid,
|
|
21802
|
+
utxoVout: params.utxoVout,
|
|
21803
|
+
chainId,
|
|
21804
|
+
auth: {
|
|
21805
|
+
timestamp: envelope.timestamp,
|
|
21806
|
+
action: envelope.action,
|
|
21807
|
+
signature: envelope.signature
|
|
21808
|
+
}
|
|
21809
|
+
})
|
|
21810
|
+
}
|
|
21811
|
+
);
|
|
21812
|
+
const json = await resp.json();
|
|
21813
|
+
return json.data ?? {
|
|
21814
|
+
success: false,
|
|
21815
|
+
error: json.error ?? "stale-spend invalidate request failed"
|
|
21816
|
+
};
|
|
21817
|
+
}
|
|
21818
|
+
const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
|
|
21819
|
+
if (!rpcUrl) {
|
|
21820
|
+
return {
|
|
21821
|
+
success: false,
|
|
21822
|
+
failedStep: "config",
|
|
21823
|
+
error: "ethRpcUrl required for invalidateStaleSpend \u2014 set DiamondHandsSDKConfig.ethRpcUrl or pass rpcUrl"
|
|
21824
|
+
};
|
|
21825
|
+
}
|
|
21826
|
+
const positionDetails = await this.getPositionDetailsView(
|
|
21827
|
+
params.positionId
|
|
21828
|
+
);
|
|
21829
|
+
if (!positionDetails?.pkpId || positionDetails.pkpId === ZeroHash) {
|
|
21830
|
+
return { success: false, error: "Position has no PKP" };
|
|
21831
|
+
}
|
|
21832
|
+
const pkpCache = this.cacheManager.getCache("pkp-data", {
|
|
21833
|
+
maxSize: 500,
|
|
21834
|
+
ttlMs: 24 * 60 * 60 * 1e3
|
|
21835
|
+
});
|
|
21836
|
+
let publicKey = pkpCache.get(params.positionId)?.publicKey;
|
|
21837
|
+
if (!publicKey) {
|
|
21838
|
+
const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
|
|
21839
|
+
pkpId: positionDetails.pkpId
|
|
21840
|
+
});
|
|
21841
|
+
publicKey = derived.publicKey;
|
|
21842
|
+
}
|
|
21843
|
+
if (!publicKey) {
|
|
21844
|
+
return { success: false, error: "Failed to resolve PKP public key" };
|
|
21845
|
+
}
|
|
21846
|
+
const chipotlePkpAddress = computeAddress(publicKey);
|
|
21847
|
+
const dc = this.config.contractAddresses || {};
|
|
21848
|
+
const chainName = chainId === 1 ? "ethereum" : chainId === 11155111 ? "sepolia" : chainId === 1337 || chainId === 31337 ? "hardhat" : void 0;
|
|
21849
|
+
if (!chainName) {
|
|
21850
|
+
return {
|
|
21851
|
+
success: false,
|
|
21852
|
+
error: `Unsupported chainId ${chainId} for invalidateStaleSpend (supported: 1, 11155111, 1337, 31337)`
|
|
21853
|
+
};
|
|
21854
|
+
}
|
|
21855
|
+
const signResult = await this.litOps.signBtcUtxoInvalidator({
|
|
21856
|
+
auth: {
|
|
21857
|
+
positionId: this.toBytes32(params.positionId),
|
|
21858
|
+
utxoTxid: params.utxoTxid,
|
|
21859
|
+
utxoVout: params.utxoVout,
|
|
21860
|
+
timestamp: envelope.timestamp,
|
|
21861
|
+
chainId,
|
|
21862
|
+
action: envelope.action,
|
|
21863
|
+
signature: envelope.signature
|
|
21864
|
+
},
|
|
21865
|
+
publicKey,
|
|
21866
|
+
pkpId: chipotlePkpAddress,
|
|
21867
|
+
chain: chainName,
|
|
21868
|
+
rpcUrl,
|
|
21869
|
+
...this.config.bitcoinProviders?.[0]?.url && (chainId === 1337 || chainId === 31337) ? { devBitcoinProviderUrl: this.config.bitcoinProviders[0].url } : {},
|
|
21870
|
+
contractAddresses: {
|
|
21871
|
+
PositionManager: dc.positionManager ?? "",
|
|
21872
|
+
BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
|
|
21873
|
+
...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {},
|
|
21874
|
+
...dc.positionDelegateRegistry ? { PositionDelegateRegistry: dc.positionDelegateRegistry } : {}
|
|
21875
|
+
}
|
|
21876
|
+
});
|
|
21877
|
+
if (!signResult.success) {
|
|
21878
|
+
return {
|
|
21879
|
+
success: false,
|
|
21880
|
+
error: signResult.error ?? "btc-utxo-invalidator signing failed",
|
|
21881
|
+
failedStep: signResult.failedStep
|
|
21882
|
+
};
|
|
21883
|
+
}
|
|
21884
|
+
if (signResult.alreadySpent && typeof signResult.alreadySpent === "object") {
|
|
21885
|
+
return {
|
|
21886
|
+
success: true,
|
|
21887
|
+
alreadySpent: { spendingTxid: signResult.alreadySpent.spendingTxid }
|
|
21888
|
+
};
|
|
21889
|
+
}
|
|
21890
|
+
if (!signResult.signatures?.length || !signResult.utxo || !signResult.pkpBtcAddress || typeof signResult.invalidatorAmount !== "number") {
|
|
21891
|
+
return {
|
|
21892
|
+
success: false,
|
|
21893
|
+
error: "btc-utxo-invalidator returned incomplete data (signatures/utxo/pkpBtcAddress/invalidatorAmount)"
|
|
21894
|
+
};
|
|
21895
|
+
}
|
|
21896
|
+
const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
|
|
21897
|
+
if (!broadcastUrl) {
|
|
21898
|
+
return {
|
|
21899
|
+
success: false,
|
|
21900
|
+
error: "invalidateStaleSpend broadcast requires config.bitcoinProviders[0].url (esplora base)"
|
|
21901
|
+
};
|
|
21902
|
+
}
|
|
21903
|
+
const { txid: invalidatorTxid } = await broadcastSignedBitcoinTransaction({
|
|
21904
|
+
signatures: signResult.signatures,
|
|
21905
|
+
utxos: [signResult.utxo],
|
|
21906
|
+
pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
|
|
21907
|
+
destination: signResult.pkpBtcAddress,
|
|
21908
|
+
userReceivesAmount: signResult.invalidatorAmount,
|
|
21909
|
+
changeAmount: 0,
|
|
21910
|
+
pkpBtcAddress: signResult.pkpBtcAddress,
|
|
21911
|
+
bitcoinNetwork: chainId === 1 ? "mainnet" : "regtest",
|
|
21912
|
+
bitcoinRpcUrl: broadcastUrl
|
|
21913
|
+
});
|
|
21914
|
+
return {
|
|
21915
|
+
success: true,
|
|
21916
|
+
invalidatorTxid,
|
|
21917
|
+
invalidatorAmount: signResult.invalidatorAmount,
|
|
21918
|
+
networkFee: signResult.networkFee,
|
|
21919
|
+
valueMismatch: signResult.valueMismatch
|
|
21920
|
+
};
|
|
21921
|
+
}
|
|
21922
|
+
/**
|
|
21923
|
+
* Resumable, stateless clearing state machine for one stuck reservation —
|
|
21924
|
+
* Bitcoin itself is the state store, so every call either advances the flow
|
|
21925
|
+
* one step or reports where it stands (survives reloads and the multi-day
|
|
21926
|
+
* 6-confirmation wait; no internal polling).
|
|
21927
|
+
*
|
|
21928
|
+
* (a) reservation gone → phase "cleared"
|
|
21929
|
+
* (b) outpoint spent, >=6 confs → recoverStaleSpend → "cleared"/"failed"
|
|
21930
|
+
* outpoint spent, <6 confs → "awaiting-confirmations" (x/6)
|
|
21931
|
+
* (c) outpoint unspent → invalidateStaleSpend → "invalidator-broadcast"
|
|
21932
|
+
*/
|
|
21933
|
+
async clearStuckReservation(params) {
|
|
21934
|
+
this.ensureInitialized();
|
|
21935
|
+
const REQUIRED_CONFS = 6;
|
|
21936
|
+
const esploraBaseUrl = params.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
|
|
21937
|
+
const reconciled = await this.reconcilePendingWithdrawals(
|
|
21938
|
+
params.positionId,
|
|
21939
|
+
esploraBaseUrl ? { esploraBaseUrl } : void 0
|
|
21940
|
+
);
|
|
21941
|
+
const row = reconciled.find(
|
|
21942
|
+
(r) => r.spend.txid.toLowerCase() === params.utxoTxid.toLowerCase() && r.spend.vout === params.utxoVout
|
|
21943
|
+
);
|
|
21944
|
+
if (!row) {
|
|
21945
|
+
return { phase: "cleared" };
|
|
21946
|
+
}
|
|
21947
|
+
if (row.spendingTxid && esploraBaseUrl) {
|
|
21948
|
+
const base = esploraBaseUrl.replace(/\/+$/, "");
|
|
21949
|
+
const txRes = await fetch(`${base}/tx/${row.spendingTxid}`);
|
|
21950
|
+
const tipRes = await fetch(`${base}/blocks/tip/height`);
|
|
21951
|
+
let confirmations = 0;
|
|
21952
|
+
if (txRes.ok && tipRes.ok) {
|
|
21953
|
+
const tx = await txRes.json();
|
|
21954
|
+
const tip = Number(await tipRes.text());
|
|
21955
|
+
if (tx.status?.confirmed && typeof tx.status.block_height === "number" && Number.isFinite(tip)) {
|
|
21956
|
+
confirmations = Math.max(0, tip - tx.status.block_height + 1);
|
|
21957
|
+
}
|
|
21958
|
+
}
|
|
21959
|
+
if (confirmations < REQUIRED_CONFS) {
|
|
21960
|
+
return {
|
|
21961
|
+
phase: "awaiting-confirmations",
|
|
21962
|
+
invalidatorTxid: row.spendingTxid,
|
|
21963
|
+
confirmations,
|
|
21964
|
+
required: REQUIRED_CONFS
|
|
21965
|
+
};
|
|
21966
|
+
}
|
|
21967
|
+
const recovery = await this.recoverStaleSpend({
|
|
21968
|
+
positionId: params.positionId,
|
|
21969
|
+
utxoTxid: params.utxoTxid,
|
|
21970
|
+
utxoVout: params.utxoVout,
|
|
21971
|
+
invalidatorTxid: row.spendingTxid
|
|
21972
|
+
});
|
|
21973
|
+
if (recovery.success) {
|
|
21974
|
+
return {
|
|
21975
|
+
phase: "cleared",
|
|
21976
|
+
invalidatorTxid: row.spendingTxid,
|
|
21977
|
+
transactionHash: recovery.transactionHash,
|
|
21978
|
+
classification: recovery.classification
|
|
21979
|
+
};
|
|
21980
|
+
}
|
|
21981
|
+
return {
|
|
21982
|
+
phase: "failed",
|
|
21983
|
+
invalidatorTxid: row.spendingTxid,
|
|
21984
|
+
error: recovery.error ?? "stale-spend recovery failed"
|
|
21985
|
+
};
|
|
21986
|
+
}
|
|
21987
|
+
const invalidation = await this.invalidateStaleSpend({
|
|
21988
|
+
positionId: params.positionId,
|
|
21989
|
+
utxoTxid: params.utxoTxid,
|
|
21990
|
+
utxoVout: params.utxoVout
|
|
21991
|
+
});
|
|
21992
|
+
if (!invalidation.success) {
|
|
21993
|
+
return {
|
|
21994
|
+
phase: "failed",
|
|
21995
|
+
error: invalidation.error ?? "invalidator signing/broadcast failed"
|
|
21996
|
+
};
|
|
21997
|
+
}
|
|
21998
|
+
if (invalidation.alreadySpent) {
|
|
21999
|
+
return {
|
|
22000
|
+
phase: "awaiting-confirmations",
|
|
22001
|
+
invalidatorTxid: invalidation.alreadySpent.spendingTxid,
|
|
22002
|
+
confirmations: 0,
|
|
22003
|
+
required: REQUIRED_CONFS
|
|
22004
|
+
};
|
|
22005
|
+
}
|
|
22006
|
+
return {
|
|
22007
|
+
phase: "invalidator-broadcast",
|
|
22008
|
+
invalidatorTxid: invalidation.invalidatorTxid,
|
|
22009
|
+
valueMismatch: invalidation.valueMismatch
|
|
22010
|
+
};
|
|
22011
|
+
}
|
|
21645
22012
|
/**
|
|
21646
22013
|
* Withdraw BTC and execute transfer (Complete Flow)
|
|
21647
22014
|
*
|
|
@@ -22732,7 +23099,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22732
23099
|
* Convert decimal position ID to bytes32 format
|
|
22733
23100
|
*/
|
|
22734
23101
|
toBytes32(value) {
|
|
22735
|
-
return
|
|
23102
|
+
return zeroPadValue5(toBeHex2(BigInt(value)), 32);
|
|
22736
23103
|
}
|
|
22737
23104
|
/**
|
|
22738
23105
|
* Check if an error indicates a technical failure vs business logic rejection
|
|
@@ -22950,8 +23317,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22950
23317
|
const MIN_REVEAL_DELAY = 60;
|
|
22951
23318
|
const MAX_RANDOM_DELAY = 240;
|
|
22952
23319
|
const entropyInput = positionId + quantumTimestamp.toString();
|
|
22953
|
-
const hash =
|
|
22954
|
-
|
|
23320
|
+
const hash = keccak2566(
|
|
23321
|
+
toUtf8Bytes6(entropyInput)
|
|
22955
23322
|
);
|
|
22956
23323
|
const randomValue = BigInt(hash);
|
|
22957
23324
|
const randomDelay = Number(randomValue % BigInt(MAX_RANDOM_DELAY));
|
|
@@ -22976,12 +23343,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22976
23343
|
const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
|
|
22977
23344
|
const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
|
|
22978
23345
|
const intentTimestamp = Math.floor(Date.now() / 1e3);
|
|
22979
|
-
const intentActionHash =
|
|
22980
|
-
const intentHash =
|
|
23346
|
+
const intentActionHash = keccak2566(toUtf8Bytes6("liquidate-position"));
|
|
23347
|
+
const intentHash = solidityPackedKeccak2565(
|
|
22981
23348
|
["bytes32", "uint256", "uint256", "address", "bytes32"],
|
|
22982
23349
|
[canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
|
|
22983
23350
|
);
|
|
22984
|
-
const intentSignature = await signer.signMessage(
|
|
23351
|
+
const intentSignature = await signer.signMessage(getBytes5(intentHash));
|
|
22985
23352
|
const response = await fetch(endpoint, {
|
|
22986
23353
|
method: "POST",
|
|
22987
23354
|
headers: { "Content-Type": "application/json" },
|
|
@@ -23033,7 +23400,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
23033
23400
|
const vrfSeedRaw = await lmResult.value["vrfSeeds"](positionId);
|
|
23034
23401
|
const vrfSeed = BigInt(vrfSeedRaw.toString());
|
|
23035
23402
|
if (vrfSeed !== 0n) {
|
|
23036
|
-
const finalEntropy =
|
|
23403
|
+
const finalEntropy = solidityPackedKeccak2565(
|
|
23037
23404
|
["uint256", "uint256", "bytes32"],
|
|
23038
23405
|
[vrfSeed, BigInt(quantumTimestamp.toString()), positionId]
|
|
23039
23406
|
);
|
|
@@ -23354,6 +23721,7 @@ export {
|
|
|
23354
23721
|
PKPManager,
|
|
23355
23722
|
PaymentType,
|
|
23356
23723
|
PositionStatus,
|
|
23724
|
+
RECOVERY_INVALIDATE_ACTION,
|
|
23357
23725
|
SDKError,
|
|
23358
23726
|
SDK_DEFAULTS,
|
|
23359
23727
|
SEPOLIA_CONTRACTS,
|
|
@@ -23366,6 +23734,8 @@ export {
|
|
|
23366
23734
|
assertProtocolNotPaused,
|
|
23367
23735
|
assertSafeServiceEndpoint,
|
|
23368
23736
|
buildBtcExecuteEnvelope,
|
|
23737
|
+
buildInvalidateMessageHash,
|
|
23738
|
+
buildInvalidateStaleSpendEnvelope,
|
|
23369
23739
|
collectFailures,
|
|
23370
23740
|
collectSuccesses,
|
|
23371
23741
|
combine,
|