@gvnrdao/dh-sdk 0.0.307 → 0.0.308
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 +468 -129
- package/dist/index.mjs +393 -51
- package/dist/modules/diamond-hands-sdk.d.ts +62 -17
- 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) {
|
|
@@ -7068,10 +7081,87 @@ async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = def
|
|
|
7068
7081
|
};
|
|
7069
7082
|
}
|
|
7070
7083
|
|
|
7071
|
-
// src/utils/
|
|
7072
|
-
import {
|
|
7084
|
+
// src/utils/stale-spend-invalidate-message.ts
|
|
7085
|
+
import {
|
|
7086
|
+
keccak256 as keccak2563,
|
|
7087
|
+
toUtf8Bytes as toUtf8Bytes3,
|
|
7088
|
+
solidityPackedKeccak256 as solidityPackedKeccak2563,
|
|
7089
|
+
getBytes as getBytes3,
|
|
7090
|
+
zeroPadValue
|
|
7091
|
+
} from "ethers";
|
|
7092
|
+
var RECOVERY_INVALIDATE_ACTION = "btc-utxo-invalidator";
|
|
7073
7093
|
var QUANTUM_SECONDS = 60;
|
|
7074
7094
|
var QUANTUM_SAFE_THRESHOLD = 40;
|
|
7095
|
+
function buildInvalidateMessageHash(params) {
|
|
7096
|
+
const positionIdBytes32 = zeroPadValue(
|
|
7097
|
+
params.positionId.startsWith("0x") ? params.positionId : `0x${params.positionId}`,
|
|
7098
|
+
32
|
|
7099
|
+
);
|
|
7100
|
+
const actionHash = keccak2563(toUtf8Bytes3(RECOVERY_INVALIDATE_ACTION));
|
|
7101
|
+
return solidityPackedKeccak2563(
|
|
7102
|
+
[
|
|
7103
|
+
"bytes32",
|
|
7104
|
+
// positionId
|
|
7105
|
+
"uint256",
|
|
7106
|
+
// timestamp
|
|
7107
|
+
"uint256",
|
|
7108
|
+
// chainId
|
|
7109
|
+
"string",
|
|
7110
|
+
// utxoTxid (display order)
|
|
7111
|
+
"uint32",
|
|
7112
|
+
// utxoVout
|
|
7113
|
+
"bytes32"
|
|
7114
|
+
// actionHash
|
|
7115
|
+
],
|
|
7116
|
+
[
|
|
7117
|
+
positionIdBytes32,
|
|
7118
|
+
params.timestamp,
|
|
7119
|
+
params.chainId,
|
|
7120
|
+
params.utxoTxid,
|
|
7121
|
+
params.utxoVout,
|
|
7122
|
+
actionHash
|
|
7123
|
+
]
|
|
7124
|
+
);
|
|
7125
|
+
}
|
|
7126
|
+
async function buildInvalidateStaleSpendEnvelope(params) {
|
|
7127
|
+
const { positionId, utxoTxid, utxoVout, chainId, signer } = params;
|
|
7128
|
+
const nowSec = Date.now() / 1e3;
|
|
7129
|
+
const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS) * QUANTUM_SECONDS;
|
|
7130
|
+
const secsIntoQuantum = nowSec - currentQuantumStart;
|
|
7131
|
+
let timestamp;
|
|
7132
|
+
if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD) {
|
|
7133
|
+
const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS;
|
|
7134
|
+
const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
|
|
7135
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
7136
|
+
timestamp = nextQuantumStart;
|
|
7137
|
+
} else {
|
|
7138
|
+
timestamp = currentQuantumStart;
|
|
7139
|
+
}
|
|
7140
|
+
const messageHash = buildInvalidateMessageHash({
|
|
7141
|
+
positionId,
|
|
7142
|
+
timestamp,
|
|
7143
|
+
chainId,
|
|
7144
|
+
utxoTxid,
|
|
7145
|
+
utxoVout
|
|
7146
|
+
});
|
|
7147
|
+
const signature = await signer.signMessage(getBytes3(messageHash));
|
|
7148
|
+
const borrowerAddress = await signer.getAddress();
|
|
7149
|
+
return {
|
|
7150
|
+
positionId,
|
|
7151
|
+
utxoTxid,
|
|
7152
|
+
utxoVout,
|
|
7153
|
+
timestamp,
|
|
7154
|
+
chainId,
|
|
7155
|
+
action: RECOVERY_INVALIDATE_ACTION,
|
|
7156
|
+
signature,
|
|
7157
|
+
borrowerAddress
|
|
7158
|
+
};
|
|
7159
|
+
}
|
|
7160
|
+
|
|
7161
|
+
// src/utils/btc-withdrawal-message.ts
|
|
7162
|
+
import { keccak256 as keccak2564, toUtf8Bytes as toUtf8Bytes4, solidityPackedKeccak256 as solidityPackedKeccak2564, getBytes as getBytes4, zeroPadValue as zeroPadValue2 } from "ethers";
|
|
7163
|
+
var QUANTUM_SECONDS2 = 60;
|
|
7164
|
+
var QUANTUM_SAFE_THRESHOLD2 = 40;
|
|
7075
7165
|
function hashBtcInputSet(utxos) {
|
|
7076
7166
|
if (!Array.isArray(utxos) || utxos.length === 0) {
|
|
7077
7167
|
throw new Error("hashBtcInputSet: at least one input is required");
|
|
@@ -7100,7 +7190,7 @@ function hashBtcInputSet(utxos) {
|
|
|
7100
7190
|
seen.add(key);
|
|
7101
7191
|
}
|
|
7102
7192
|
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
|
|
7193
|
+
return keccak2564(toUtf8Bytes4(canonical));
|
|
7104
7194
|
}
|
|
7105
7195
|
async function buildBtcExecuteEnvelope(params) {
|
|
7106
7196
|
const {
|
|
@@ -7134,26 +7224,26 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
7134
7224
|
}
|
|
7135
7225
|
const utxosHash = hashBtcInputSet(utxos);
|
|
7136
7226
|
const nowSec = Date.now() / 1e3;
|
|
7137
|
-
const currentQuantumStart = Math.floor(nowSec /
|
|
7227
|
+
const currentQuantumStart = Math.floor(nowSec / QUANTUM_SECONDS2) * QUANTUM_SECONDS2;
|
|
7138
7228
|
const secsIntoQuantum = nowSec - currentQuantumStart;
|
|
7139
7229
|
let timestamp;
|
|
7140
|
-
if (secsIntoQuantum >
|
|
7141
|
-
const nextQuantumStart = currentQuantumStart +
|
|
7230
|
+
if (secsIntoQuantum > QUANTUM_SAFE_THRESHOLD2) {
|
|
7231
|
+
const nextQuantumStart = currentQuantumStart + QUANTUM_SECONDS2;
|
|
7142
7232
|
const waitMs = Math.max(0, (nextQuantumStart - nowSec) * 1e3 + 500);
|
|
7143
7233
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
7144
7234
|
timestamp = nextQuantumStart;
|
|
7145
7235
|
} else {
|
|
7146
7236
|
timestamp = currentQuantumStart;
|
|
7147
7237
|
}
|
|
7148
|
-
const actionHash =
|
|
7149
|
-
|
|
7238
|
+
const actionHash = keccak2564(
|
|
7239
|
+
toUtf8Bytes4("execute-btc-withdrawal")
|
|
7150
7240
|
);
|
|
7151
7241
|
const utxoIdentifier = `${txid}:${vout}`;
|
|
7152
|
-
const positionIdBytes32 =
|
|
7242
|
+
const positionIdBytes32 = zeroPadValue2(
|
|
7153
7243
|
positionId.startsWith("0x") ? positionId : `0x${positionId}`,
|
|
7154
7244
|
32
|
|
7155
7245
|
);
|
|
7156
|
-
const messageHash =
|
|
7246
|
+
const messageHash = solidityPackedKeccak2564(
|
|
7157
7247
|
[
|
|
7158
7248
|
"bytes32",
|
|
7159
7249
|
// positionId
|
|
@@ -7186,7 +7276,7 @@ async function buildBtcExecuteEnvelope(params) {
|
|
|
7186
7276
|
actionHash
|
|
7187
7277
|
]
|
|
7188
7278
|
);
|
|
7189
|
-
const userSignature = await signer.signMessage(
|
|
7279
|
+
const userSignature = await signer.signMessage(getBytes4(messageHash));
|
|
7190
7280
|
const borrowerAddress = await signer.getAddress();
|
|
7191
7281
|
return {
|
|
7192
7282
|
positionId,
|
|
@@ -7250,7 +7340,7 @@ function maybeTemperSignature(signature, shouldTemper, operationName) {
|
|
|
7250
7340
|
}
|
|
7251
7341
|
|
|
7252
7342
|
// src/utils/address-conversion.utils.ts
|
|
7253
|
-
import { zeroPadValue as
|
|
7343
|
+
import { zeroPadValue as zeroPadValue3 } from "ethers";
|
|
7254
7344
|
|
|
7255
7345
|
// src/utils/chunks/bitcoin-utils.ts
|
|
7256
7346
|
import { sha256 as nobleSha256 } from "@noble/hashes/sha256";
|
|
@@ -8834,7 +8924,7 @@ function createPKPManager(config) {
|
|
|
8834
8924
|
}
|
|
8835
8925
|
|
|
8836
8926
|
// src/modules/loan/loan-creator.module.ts
|
|
8837
|
-
import { Interface as Interface2, zeroPadValue as
|
|
8927
|
+
import { Interface as Interface2, zeroPadValue as zeroPadValue4, toBeHex, SigningKey } from "ethers";
|
|
8838
8928
|
var import_dh_lit_actions = __toESM(require_pkg_src());
|
|
8839
8929
|
var LoanCreator = class {
|
|
8840
8930
|
config;
|
|
@@ -9164,7 +9254,7 @@ var LoanCreator = class {
|
|
|
9164
9254
|
try {
|
|
9165
9255
|
const mainnetVaultAddress = bitcoinAddresses.mainnet;
|
|
9166
9256
|
const regtestVaultAddress = bitcoinAddresses.regtest;
|
|
9167
|
-
const pkpIdBytes32 =
|
|
9257
|
+
const pkpIdBytes32 = zeroPadValue4(
|
|
9168
9258
|
toBeHex(BigInt(pkpData.tokenId)),
|
|
9169
9259
|
32
|
|
9170
9260
|
);
|
|
@@ -9345,7 +9435,7 @@ var LoanCreator = class {
|
|
|
9345
9435
|
}
|
|
9346
9436
|
}
|
|
9347
9437
|
if (this.config.debug) {
|
|
9348
|
-
const pkpIdBytes32Check =
|
|
9438
|
+
const pkpIdBytes32Check = zeroPadValue4(
|
|
9349
9439
|
toBeHex(BigInt(pkpData.tokenId)),
|
|
9350
9440
|
32
|
|
9351
9441
|
);
|
|
@@ -10108,7 +10198,7 @@ function createLoanQuery(config) {
|
|
|
10108
10198
|
}
|
|
10109
10199
|
|
|
10110
10200
|
// src/modules/withdrawal-address/withdrawal-address.module.ts
|
|
10111
|
-
import { keccak256 as
|
|
10201
|
+
import { keccak256 as keccak2565, toUtf8Bytes as toUtf8Bytes5, ZeroAddress as ZeroAddress2 } from "ethers";
|
|
10112
10202
|
var WithdrawalAddressModule = class {
|
|
10113
10203
|
config;
|
|
10114
10204
|
graphClient;
|
|
@@ -10143,7 +10233,7 @@ var WithdrawalAddressModule = class {
|
|
|
10143
10233
|
}
|
|
10144
10234
|
/** keccak256 of the canonicalized address — matches the on-chain key. */
|
|
10145
10235
|
static addressHash(btcAddress) {
|
|
10146
|
-
return
|
|
10236
|
+
return keccak2565(toUtf8Bytes5(normalizeBitcoinAddress(btcAddress)));
|
|
10147
10237
|
}
|
|
10148
10238
|
/**
|
|
10149
10239
|
* Add a Bitcoin address to the caller's allowlist. It becomes usable only
|
|
@@ -17592,8 +17682,8 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
17592
17682
|
throw new Error(`Position not found: ${request.positionId}`);
|
|
17593
17683
|
}
|
|
17594
17684
|
const currentDebt = position.ucdDebt.toString();
|
|
17595
|
-
const ucdDebtHash =
|
|
17596
|
-
const contractHash =
|
|
17685
|
+
const ucdDebtHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(["uint256"], [currentDebt]));
|
|
17686
|
+
const contractHash = keccak2566(AbiCoder2.defaultAbiCoder().encode(
|
|
17597
17687
|
["address", "address", "address", "address"],
|
|
17598
17688
|
[
|
|
17599
17689
|
positionManagerAddress,
|
|
@@ -17670,7 +17760,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
17670
17760
|
const currentPosition = await positionCore["getPositionDetails"](
|
|
17671
17761
|
positionIdBytes32
|
|
17672
17762
|
);
|
|
17673
|
-
const currentDebtHash =
|
|
17763
|
+
const currentDebtHash = keccak2566(
|
|
17674
17764
|
AbiCoder2.defaultAbiCoder().encode(
|
|
17675
17765
|
["uint256"],
|
|
17676
17766
|
[currentPosition.ucdDebt]
|
|
@@ -21406,7 +21496,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21406
21496
|
* been broadcast to the Bitcoin network. An empty array means no pending withdrawals.
|
|
21407
21497
|
*
|
|
21408
21498
|
* @param positionId - Position identifier
|
|
21409
|
-
* @returns Array of pending withdrawals, each including a pre-computed utxoKey
|
|
21499
|
+
* @returns Array of pending withdrawals, each including a pre-computed utxoKey (the key recoverStaleSpend and the admin clearing path operate on)
|
|
21410
21500
|
*/
|
|
21411
21501
|
async getPendingWithdrawals(positionId) {
|
|
21412
21502
|
this.ensureInitialized();
|
|
@@ -21438,7 +21528,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21438
21528
|
targetAddress: spend.targetAddress,
|
|
21439
21529
|
targetAmount: Number(spend.targetAmount),
|
|
21440
21530
|
authorizedAt: Number(spend.authorizedAt),
|
|
21441
|
-
utxoKey:
|
|
21531
|
+
utxoKey: solidityPackedKeccak2565(
|
|
21442
21532
|
["string", "uint32"],
|
|
21443
21533
|
[spend.txid, Number(spend.vout)]
|
|
21444
21534
|
)
|
|
@@ -21451,11 +21541,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21451
21541
|
* broadcast happened. Callers MUST use the returned `status` to decide what
|
|
21452
21542
|
* to offer:
|
|
21453
21543
|
* - EXECUTABLE → offer Execute (the only status that may).
|
|
21454
|
-
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof
|
|
21455
|
-
*
|
|
21544
|
+
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof —
|
|
21545
|
+
* feed it to recoverStaleSpend to clear the reservation.
|
|
21546
|
+
* - SPENT_MISMATCH → unexecutable; recoverStaleSpend with the spending txid
|
|
21547
|
+
* as invalidator clears the reservation.
|
|
21456
21548
|
* - CORRUPT → authorization contradicts the chain (e.g. declared
|
|
21457
|
-
* satoshis ≠ real output value);
|
|
21458
|
-
*
|
|
21549
|
+
* satoshis ≠ real output value); Execute can only die at
|
|
21550
|
+
* the signer guard, and no borrower-side cancel exists
|
|
21551
|
+
* (audit #62210) — clearing needs the AdminModule
|
|
21552
|
+
* cancelStaleSpendByAdmin operator override.
|
|
21459
21553
|
* - UNFUNDED → funding tx unknown/unconfirmed; wait.
|
|
21460
21554
|
*
|
|
21461
21555
|
* @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
|
|
@@ -21479,18 +21573,13 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21479
21573
|
spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
|
|
21480
21574
|
);
|
|
21481
21575
|
}
|
|
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
|
-
*/
|
|
21576
|
+
// NOTE: there is deliberately no cancelPendingWithdrawal. Borrower-callable
|
|
21577
|
+
// cleanup was permanently disabled by audit #62210 (markSpentByOwner /
|
|
21578
|
+
// cancelStaleSpendByOwner revert with BorrowerBTCSpendMutationDisabled — a
|
|
21579
|
+
// borrower cancel could be replayed to mint UCD against the same UTXO).
|
|
21580
|
+
// Reservations clear via recoverStaleSpend below (trustless attestation once
|
|
21581
|
+
// the UTXO is provably consumed) or the AdminModule-timelocked
|
|
21582
|
+
// cancelStaleSpendByAdmin operator override.
|
|
21494
21583
|
/**
|
|
21495
21584
|
* Recover (clear) a stale BTC reservation via LIT-attested proof.
|
|
21496
21585
|
*
|
|
@@ -21642,6 +21731,256 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21642
21731
|
};
|
|
21643
21732
|
}
|
|
21644
21733
|
}
|
|
21734
|
+
/**
|
|
21735
|
+
* Sign + broadcast the PKP vault→vault self-spend that invalidates one
|
|
21736
|
+
* stuck authorized outpoint (btc-utxo-invalidator 1.0.0). Once the returned
|
|
21737
|
+
* `invalidatorTxid` reaches >=6 Bitcoin confirmations, `recoverStaleSpend`
|
|
21738
|
+
* clears the on-chain reservation — the trustless replacement for the
|
|
21739
|
+
* AdminModule cancelStaleSpendByAdmin path on unspent stuck/corrupt
|
|
21740
|
+
* reservations (incident 2026-07-22). Requires the borrower (or position
|
|
21741
|
+
* delegate) signer; the signature is scoped to exactly this outpoint.
|
|
21742
|
+
*/
|
|
21743
|
+
async invalidateStaleSpend(params) {
|
|
21744
|
+
this.ensureInitialized();
|
|
21745
|
+
const chainId = this.config.chainId ?? this.config.networkOverride?.chainId;
|
|
21746
|
+
if (!chainId) {
|
|
21747
|
+
return {
|
|
21748
|
+
success: false,
|
|
21749
|
+
failedStep: "config",
|
|
21750
|
+
error: "chainId required for invalidateStaleSpend"
|
|
21751
|
+
};
|
|
21752
|
+
}
|
|
21753
|
+
const signer = this.getSignerOrThrow();
|
|
21754
|
+
const envelope = await buildInvalidateStaleSpendEnvelope({
|
|
21755
|
+
positionId: params.positionId,
|
|
21756
|
+
utxoTxid: params.utxoTxid,
|
|
21757
|
+
utxoVout: params.utxoVout,
|
|
21758
|
+
chainId,
|
|
21759
|
+
signer
|
|
21760
|
+
});
|
|
21761
|
+
if (this.config.mode === "service" && this.config.serviceEndpoint) {
|
|
21762
|
+
const headers = {
|
|
21763
|
+
"Content-Type": "application/json",
|
|
21764
|
+
...await this.getAuthHeader()
|
|
21765
|
+
};
|
|
21766
|
+
const resp = await fetch(
|
|
21767
|
+
`${this.config.serviceEndpoint}/api/lit/stale-spend/invalidate`,
|
|
21768
|
+
{
|
|
21769
|
+
method: "POST",
|
|
21770
|
+
headers,
|
|
21771
|
+
body: JSON.stringify({
|
|
21772
|
+
positionId: this.toBytes32(params.positionId),
|
|
21773
|
+
utxoTxid: params.utxoTxid,
|
|
21774
|
+
utxoVout: params.utxoVout,
|
|
21775
|
+
chainId,
|
|
21776
|
+
auth: {
|
|
21777
|
+
timestamp: envelope.timestamp,
|
|
21778
|
+
action: envelope.action,
|
|
21779
|
+
signature: envelope.signature
|
|
21780
|
+
}
|
|
21781
|
+
})
|
|
21782
|
+
}
|
|
21783
|
+
);
|
|
21784
|
+
const json = await resp.json();
|
|
21785
|
+
return json.data ?? {
|
|
21786
|
+
success: false,
|
|
21787
|
+
error: json.error ?? "stale-spend invalidate request failed"
|
|
21788
|
+
};
|
|
21789
|
+
}
|
|
21790
|
+
const rpcUrl = params.rpcUrl ?? this.config.ethRpcUrl;
|
|
21791
|
+
if (!rpcUrl) {
|
|
21792
|
+
return {
|
|
21793
|
+
success: false,
|
|
21794
|
+
failedStep: "config",
|
|
21795
|
+
error: "ethRpcUrl required for invalidateStaleSpend \u2014 set DiamondHandsSDKConfig.ethRpcUrl or pass rpcUrl"
|
|
21796
|
+
};
|
|
21797
|
+
}
|
|
21798
|
+
const positionDetails = await this.getPositionDetailsView(
|
|
21799
|
+
params.positionId
|
|
21800
|
+
);
|
|
21801
|
+
if (!positionDetails?.pkpId || positionDetails.pkpId === ZeroHash) {
|
|
21802
|
+
return { success: false, error: "Position has no PKP" };
|
|
21803
|
+
}
|
|
21804
|
+
const pkpCache = this.cacheManager.getCache("pkp-data", {
|
|
21805
|
+
maxSize: 500,
|
|
21806
|
+
ttlMs: 24 * 60 * 60 * 1e3
|
|
21807
|
+
});
|
|
21808
|
+
let publicKey = pkpCache.get(params.positionId)?.publicKey;
|
|
21809
|
+
if (!publicKey) {
|
|
21810
|
+
const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
|
|
21811
|
+
pkpId: positionDetails.pkpId
|
|
21812
|
+
});
|
|
21813
|
+
publicKey = derived.publicKey;
|
|
21814
|
+
}
|
|
21815
|
+
if (!publicKey) {
|
|
21816
|
+
return { success: false, error: "Failed to resolve PKP public key" };
|
|
21817
|
+
}
|
|
21818
|
+
const chipotlePkpAddress = computeAddress(publicKey);
|
|
21819
|
+
const dc = this.config.contractAddresses || {};
|
|
21820
|
+
const chainName = chainId === 1 ? "ethereum" : chainId === 11155111 ? "sepolia" : chainId === 1337 || chainId === 31337 ? "hardhat" : void 0;
|
|
21821
|
+
if (!chainName) {
|
|
21822
|
+
return {
|
|
21823
|
+
success: false,
|
|
21824
|
+
error: `Unsupported chainId ${chainId} for invalidateStaleSpend (supported: 1, 11155111, 1337, 31337)`
|
|
21825
|
+
};
|
|
21826
|
+
}
|
|
21827
|
+
const signResult = await this.litOps.signBtcUtxoInvalidator({
|
|
21828
|
+
auth: {
|
|
21829
|
+
positionId: this.toBytes32(params.positionId),
|
|
21830
|
+
utxoTxid: params.utxoTxid,
|
|
21831
|
+
utxoVout: params.utxoVout,
|
|
21832
|
+
timestamp: envelope.timestamp,
|
|
21833
|
+
chainId,
|
|
21834
|
+
action: envelope.action,
|
|
21835
|
+
signature: envelope.signature
|
|
21836
|
+
},
|
|
21837
|
+
publicKey,
|
|
21838
|
+
pkpId: chipotlePkpAddress,
|
|
21839
|
+
chain: chainName,
|
|
21840
|
+
rpcUrl,
|
|
21841
|
+
...this.config.bitcoinProviders?.[0]?.url && (chainId === 1337 || chainId === 31337) ? { devBitcoinProviderUrl: this.config.bitcoinProviders[0].url } : {},
|
|
21842
|
+
contractAddresses: {
|
|
21843
|
+
PositionManager: dc.positionManager ?? "",
|
|
21844
|
+
BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
|
|
21845
|
+
...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {},
|
|
21846
|
+
...dc.positionDelegateRegistry ? { PositionDelegateRegistry: dc.positionDelegateRegistry } : {}
|
|
21847
|
+
}
|
|
21848
|
+
});
|
|
21849
|
+
if (!signResult.success) {
|
|
21850
|
+
return {
|
|
21851
|
+
success: false,
|
|
21852
|
+
error: signResult.error ?? "btc-utxo-invalidator signing failed",
|
|
21853
|
+
failedStep: signResult.failedStep
|
|
21854
|
+
};
|
|
21855
|
+
}
|
|
21856
|
+
if (signResult.alreadySpent && typeof signResult.alreadySpent === "object") {
|
|
21857
|
+
return {
|
|
21858
|
+
success: true,
|
|
21859
|
+
alreadySpent: { spendingTxid: signResult.alreadySpent.spendingTxid }
|
|
21860
|
+
};
|
|
21861
|
+
}
|
|
21862
|
+
if (!signResult.signatures?.length || !signResult.utxo || !signResult.pkpBtcAddress || typeof signResult.invalidatorAmount !== "number") {
|
|
21863
|
+
return {
|
|
21864
|
+
success: false,
|
|
21865
|
+
error: "btc-utxo-invalidator returned incomplete data (signatures/utxo/pkpBtcAddress/invalidatorAmount)"
|
|
21866
|
+
};
|
|
21867
|
+
}
|
|
21868
|
+
const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
|
|
21869
|
+
if (!broadcastUrl) {
|
|
21870
|
+
return {
|
|
21871
|
+
success: false,
|
|
21872
|
+
error: "invalidateStaleSpend broadcast requires config.bitcoinProviders[0].url (esplora base)"
|
|
21873
|
+
};
|
|
21874
|
+
}
|
|
21875
|
+
const { txid: invalidatorTxid } = await broadcastSignedBitcoinTransaction({
|
|
21876
|
+
signatures: signResult.signatures,
|
|
21877
|
+
utxos: [signResult.utxo],
|
|
21878
|
+
pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
|
|
21879
|
+
destination: signResult.pkpBtcAddress,
|
|
21880
|
+
userReceivesAmount: signResult.invalidatorAmount,
|
|
21881
|
+
changeAmount: 0,
|
|
21882
|
+
pkpBtcAddress: signResult.pkpBtcAddress,
|
|
21883
|
+
bitcoinNetwork: chainId === 1 ? "mainnet" : "regtest",
|
|
21884
|
+
bitcoinRpcUrl: broadcastUrl
|
|
21885
|
+
});
|
|
21886
|
+
return {
|
|
21887
|
+
success: true,
|
|
21888
|
+
invalidatorTxid,
|
|
21889
|
+
invalidatorAmount: signResult.invalidatorAmount,
|
|
21890
|
+
networkFee: signResult.networkFee,
|
|
21891
|
+
valueMismatch: signResult.valueMismatch
|
|
21892
|
+
};
|
|
21893
|
+
}
|
|
21894
|
+
/**
|
|
21895
|
+
* Resumable, stateless clearing state machine for one stuck reservation —
|
|
21896
|
+
* Bitcoin itself is the state store, so every call either advances the flow
|
|
21897
|
+
* one step or reports where it stands (survives reloads and the multi-day
|
|
21898
|
+
* 6-confirmation wait; no internal polling).
|
|
21899
|
+
*
|
|
21900
|
+
* (a) reservation gone → phase "cleared"
|
|
21901
|
+
* (b) outpoint spent, >=6 confs → recoverStaleSpend → "cleared"/"failed"
|
|
21902
|
+
* outpoint spent, <6 confs → "awaiting-confirmations" (x/6)
|
|
21903
|
+
* (c) outpoint unspent → invalidateStaleSpend → "invalidator-broadcast"
|
|
21904
|
+
*/
|
|
21905
|
+
async clearStuckReservation(params) {
|
|
21906
|
+
this.ensureInitialized();
|
|
21907
|
+
const REQUIRED_CONFS = 6;
|
|
21908
|
+
const esploraBaseUrl = params.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
|
|
21909
|
+
const reconciled = await this.reconcilePendingWithdrawals(
|
|
21910
|
+
params.positionId,
|
|
21911
|
+
esploraBaseUrl ? { esploraBaseUrl } : void 0
|
|
21912
|
+
);
|
|
21913
|
+
const row = reconciled.find(
|
|
21914
|
+
(r) => r.spend.txid.toLowerCase() === params.utxoTxid.toLowerCase() && r.spend.vout === params.utxoVout
|
|
21915
|
+
);
|
|
21916
|
+
if (!row) {
|
|
21917
|
+
return { phase: "cleared" };
|
|
21918
|
+
}
|
|
21919
|
+
if (row.spendingTxid && esploraBaseUrl) {
|
|
21920
|
+
const base = esploraBaseUrl.replace(/\/+$/, "");
|
|
21921
|
+
const txRes = await fetch(`${base}/tx/${row.spendingTxid}`);
|
|
21922
|
+
const tipRes = await fetch(`${base}/blocks/tip/height`);
|
|
21923
|
+
let confirmations = 0;
|
|
21924
|
+
if (txRes.ok && tipRes.ok) {
|
|
21925
|
+
const tx = await txRes.json();
|
|
21926
|
+
const tip = Number(await tipRes.text());
|
|
21927
|
+
if (tx.status?.confirmed && typeof tx.status.block_height === "number" && Number.isFinite(tip)) {
|
|
21928
|
+
confirmations = Math.max(0, tip - tx.status.block_height + 1);
|
|
21929
|
+
}
|
|
21930
|
+
}
|
|
21931
|
+
if (confirmations < REQUIRED_CONFS) {
|
|
21932
|
+
return {
|
|
21933
|
+
phase: "awaiting-confirmations",
|
|
21934
|
+
invalidatorTxid: row.spendingTxid,
|
|
21935
|
+
confirmations,
|
|
21936
|
+
required: REQUIRED_CONFS
|
|
21937
|
+
};
|
|
21938
|
+
}
|
|
21939
|
+
const recovery = await this.recoverStaleSpend({
|
|
21940
|
+
positionId: params.positionId,
|
|
21941
|
+
utxoTxid: params.utxoTxid,
|
|
21942
|
+
utxoVout: params.utxoVout,
|
|
21943
|
+
invalidatorTxid: row.spendingTxid
|
|
21944
|
+
});
|
|
21945
|
+
if (recovery.success) {
|
|
21946
|
+
return {
|
|
21947
|
+
phase: "cleared",
|
|
21948
|
+
invalidatorTxid: row.spendingTxid,
|
|
21949
|
+
transactionHash: recovery.transactionHash,
|
|
21950
|
+
classification: recovery.classification
|
|
21951
|
+
};
|
|
21952
|
+
}
|
|
21953
|
+
return {
|
|
21954
|
+
phase: "failed",
|
|
21955
|
+
invalidatorTxid: row.spendingTxid,
|
|
21956
|
+
error: recovery.error ?? "stale-spend recovery failed"
|
|
21957
|
+
};
|
|
21958
|
+
}
|
|
21959
|
+
const invalidation = await this.invalidateStaleSpend({
|
|
21960
|
+
positionId: params.positionId,
|
|
21961
|
+
utxoTxid: params.utxoTxid,
|
|
21962
|
+
utxoVout: params.utxoVout
|
|
21963
|
+
});
|
|
21964
|
+
if (!invalidation.success) {
|
|
21965
|
+
return {
|
|
21966
|
+
phase: "failed",
|
|
21967
|
+
error: invalidation.error ?? "invalidator signing/broadcast failed"
|
|
21968
|
+
};
|
|
21969
|
+
}
|
|
21970
|
+
if (invalidation.alreadySpent) {
|
|
21971
|
+
return {
|
|
21972
|
+
phase: "awaiting-confirmations",
|
|
21973
|
+
invalidatorTxid: invalidation.alreadySpent.spendingTxid,
|
|
21974
|
+
confirmations: 0,
|
|
21975
|
+
required: REQUIRED_CONFS
|
|
21976
|
+
};
|
|
21977
|
+
}
|
|
21978
|
+
return {
|
|
21979
|
+
phase: "invalidator-broadcast",
|
|
21980
|
+
invalidatorTxid: invalidation.invalidatorTxid,
|
|
21981
|
+
valueMismatch: invalidation.valueMismatch
|
|
21982
|
+
};
|
|
21983
|
+
}
|
|
21645
21984
|
/**
|
|
21646
21985
|
* Withdraw BTC and execute transfer (Complete Flow)
|
|
21647
21986
|
*
|
|
@@ -22732,7 +23071,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22732
23071
|
* Convert decimal position ID to bytes32 format
|
|
22733
23072
|
*/
|
|
22734
23073
|
toBytes32(value) {
|
|
22735
|
-
return
|
|
23074
|
+
return zeroPadValue5(toBeHex2(BigInt(value)), 32);
|
|
22736
23075
|
}
|
|
22737
23076
|
/**
|
|
22738
23077
|
* Check if an error indicates a technical failure vs business logic rejection
|
|
@@ -22950,8 +23289,8 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22950
23289
|
const MIN_REVEAL_DELAY = 60;
|
|
22951
23290
|
const MAX_RANDOM_DELAY = 240;
|
|
22952
23291
|
const entropyInput = positionId + quantumTimestamp.toString();
|
|
22953
|
-
const hash =
|
|
22954
|
-
|
|
23292
|
+
const hash = keccak2566(
|
|
23293
|
+
toUtf8Bytes6(entropyInput)
|
|
22955
23294
|
);
|
|
22956
23295
|
const randomValue = BigInt(hash);
|
|
22957
23296
|
const randomDelay = Number(randomValue % BigInt(MAX_RANDOM_DELAY));
|
|
@@ -22976,12 +23315,12 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22976
23315
|
const rawPositionId = /^0x/i.test(params.positionId.trim()) ? params.positionId.trim().slice(2) : params.positionId.trim();
|
|
22977
23316
|
const canonicalPositionId = "0x" + rawPositionId.padStart(64, "0").toLowerCase();
|
|
22978
23317
|
const intentTimestamp = Math.floor(Date.now() / 1e3);
|
|
22979
|
-
const intentActionHash =
|
|
22980
|
-
const intentHash =
|
|
23318
|
+
const intentActionHash = keccak2566(toUtf8Bytes6("liquidate-position"));
|
|
23319
|
+
const intentHash = solidityPackedKeccak2565(
|
|
22981
23320
|
["bytes32", "uint256", "uint256", "address", "bytes32"],
|
|
22982
23321
|
[canonicalPositionId, intentTimestamp, chainId, intentSigner, intentActionHash]
|
|
22983
23322
|
);
|
|
22984
|
-
const intentSignature = await signer.signMessage(
|
|
23323
|
+
const intentSignature = await signer.signMessage(getBytes5(intentHash));
|
|
22985
23324
|
const response = await fetch(endpoint, {
|
|
22986
23325
|
method: "POST",
|
|
22987
23326
|
headers: { "Content-Type": "application/json" },
|
|
@@ -23033,7 +23372,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
23033
23372
|
const vrfSeedRaw = await lmResult.value["vrfSeeds"](positionId);
|
|
23034
23373
|
const vrfSeed = BigInt(vrfSeedRaw.toString());
|
|
23035
23374
|
if (vrfSeed !== 0n) {
|
|
23036
|
-
const finalEntropy =
|
|
23375
|
+
const finalEntropy = solidityPackedKeccak2565(
|
|
23037
23376
|
["uint256", "uint256", "bytes32"],
|
|
23038
23377
|
[vrfSeed, BigInt(quantumTimestamp.toString()), positionId]
|
|
23039
23378
|
);
|
|
@@ -23354,6 +23693,7 @@ export {
|
|
|
23354
23693
|
PKPManager,
|
|
23355
23694
|
PaymentType,
|
|
23356
23695
|
PositionStatus,
|
|
23696
|
+
RECOVERY_INVALIDATE_ACTION,
|
|
23357
23697
|
SDKError,
|
|
23358
23698
|
SDK_DEFAULTS,
|
|
23359
23699
|
SEPOLIA_CONTRACTS,
|
|
@@ -23366,6 +23706,8 @@ export {
|
|
|
23366
23706
|
assertProtocolNotPaused,
|
|
23367
23707
|
assertSafeServiceEndpoint,
|
|
23368
23708
|
buildBtcExecuteEnvelope,
|
|
23709
|
+
buildInvalidateMessageHash,
|
|
23710
|
+
buildInvalidateStaleSpendEnvelope,
|
|
23369
23711
|
collectFailures,
|
|
23370
23712
|
collectSuccesses,
|
|
23371
23713
|
combine,
|