@gvnrdao/dh-sdk 0.0.305 → 0.0.307
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/deployments.js +32 -30
- package/dist/deployments.mjs +32 -30
- package/dist/index.d.ts +2 -0
- package/dist/index.js +442 -36
- package/dist/index.mjs +441 -37
- package/dist/interfaces/chunks/config.i.d.ts +12 -0
- package/dist/modules/diamond-hands-sdk.d.ts +41 -0
- package/dist/utils/lane-guard.utils.d.ts +36 -0
- package/dist/utils/quantum-submission.utils.d.ts +8 -0
- package/dist/utils/withdrawal-reconciliation.utils.d.ts +64 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -577,45 +577,47 @@ var init_deployment_addresses = __esm({
|
|
|
577
577
|
};
|
|
578
578
|
LOCALHOST_DEPLOYMENT = {
|
|
579
579
|
network: "localhost",
|
|
580
|
-
chainId:
|
|
581
|
-
timestamp: "2026-
|
|
580
|
+
chainId: 1337,
|
|
581
|
+
timestamp: "2026-07-20T19:45:58.514Z",
|
|
582
582
|
deployer: "",
|
|
583
583
|
contracts: {
|
|
584
|
-
MessageHashBuilder: "
|
|
585
|
-
|
|
584
|
+
MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
|
|
585
|
+
LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
|
|
586
|
+
UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
|
|
586
587
|
UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
587
588
|
UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
588
|
-
PositionManagerCoreModule: "
|
|
589
|
-
TermManagerModule: "
|
|
590
|
-
LoanOperationsManagerModule: "
|
|
591
|
-
BTCSpendAuthorizer: "
|
|
592
|
-
CollateralManagerModule: "
|
|
593
|
-
LiquidationManagerModule: "
|
|
594
|
-
CircuitBreakerModule: "
|
|
595
|
-
AdminModule: "
|
|
596
|
-
PositionManagerViews: "
|
|
597
|
-
PositionManager: "
|
|
598
|
-
OperationAuthorizationRegistry: "
|
|
599
|
-
PKPValidation: "
|
|
589
|
+
PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
|
|
590
|
+
TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
|
|
591
|
+
LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
|
|
592
|
+
BTCSpendAuthorizer: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d",
|
|
593
|
+
CollateralManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
|
|
594
|
+
LiquidationManagerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
|
|
595
|
+
CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
|
|
596
|
+
AdminModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
|
597
|
+
PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
|
598
|
+
PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
|
|
599
|
+
OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
|
|
600
|
+
PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
|
|
600
601
|
}
|
|
601
602
|
};
|
|
602
603
|
LOCALHOST_CONTRACTS = {
|
|
603
|
-
MessageHashBuilder: "
|
|
604
|
-
|
|
604
|
+
MessageHashBuilder: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9",
|
|
605
|
+
LITActionValidator: "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853",
|
|
606
|
+
UpgradeValidator: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
|
|
605
607
|
UCDToken: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
|
|
606
608
|
UCDController: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
|
|
607
|
-
PositionManagerCoreModule: "
|
|
608
|
-
TermManagerModule: "
|
|
609
|
-
LoanOperationsManagerModule: "
|
|
610
|
-
BTCSpendAuthorizer: "
|
|
611
|
-
CollateralManagerModule: "
|
|
612
|
-
LiquidationManagerModule: "
|
|
613
|
-
CircuitBreakerModule: "
|
|
614
|
-
AdminModule: "
|
|
615
|
-
PositionManagerViews: "
|
|
616
|
-
PositionManager: "
|
|
617
|
-
OperationAuthorizationRegistry: "
|
|
618
|
-
PKPValidation: "
|
|
609
|
+
PositionManagerCoreModule: "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e",
|
|
610
|
+
TermManagerModule: "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82",
|
|
611
|
+
LoanOperationsManagerModule: "0x68B1D87F95878fE05B998F19b66F4baba5De1aed",
|
|
612
|
+
BTCSpendAuthorizer: "0xc6e7DF5E7b4f2A278906862b61205850344D4e7d",
|
|
613
|
+
CollateralManagerModule: "0x322813Fd9A801c5507c9de605d63CEA4f2CE6c44",
|
|
614
|
+
LiquidationManagerModule: "0x4A679253410272dd5232B3Ff7cF5dbB88f295319",
|
|
615
|
+
CircuitBreakerModule: "0x0B306BF915C4d645ff596e518fAf3F9669b97016",
|
|
616
|
+
AdminModule: "0x09635F643e140090A9A8Dcd712eD6285858ceBef",
|
|
617
|
+
PositionManagerViews: "0x959922bE3CAee4b8Cd9a407cc3ac1C251C2007B1",
|
|
618
|
+
PositionManager: "0x851356ae760d987E095750cCeb3bC6014560891C",
|
|
619
|
+
OperationAuthorizationRegistry: "0xc96304e3c037f81dA488ed9dEa1D8F2a48278a75",
|
|
620
|
+
PKPValidation: "0xD0141E899a65C95a556fE2B27e5982A6DE7fDD7A"
|
|
619
621
|
};
|
|
620
622
|
ALL_DEPLOYMENTS = {
|
|
621
623
|
sepolia: SEPOLIA_DEPLOYMENT,
|
|
@@ -5534,7 +5536,12 @@ var init_network_configs = __esm({
|
|
|
5534
5536
|
NETWORK_CONFIGS = {
|
|
5535
5537
|
1: getMainnetConfig(),
|
|
5536
5538
|
11155111: getSepoliaConfig(),
|
|
5537
|
-
1337: getLocalhostConfig()
|
|
5539
|
+
1337: getLocalhostConfig(),
|
|
5540
|
+
// In-process hardhat default chainId — same localhost deployment as 1337
|
|
5541
|
+
// (`npx hardhat node`), so consumers on either local chain resolve config.
|
|
5542
|
+
// chainId overridden so the resolved config self-describes the chain that
|
|
5543
|
+
// was actually looked up (downstream bind/log/validate reads it).
|
|
5544
|
+
31337: { ...getLocalhostConfig(), chainId: 31337 }
|
|
5538
5545
|
};
|
|
5539
5546
|
DEFAULT_NETWORK_CONFIG = {
|
|
5540
5547
|
/** Fallback when chain is unknown — production Chipotle. */
|
|
@@ -6874,12 +6881,20 @@ async function assertSafeQuantumSubmission(opts) {
|
|
|
6874
6881
|
...opts.sleep ? { sleep: opts.sleep } : {}
|
|
6875
6882
|
};
|
|
6876
6883
|
await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
|
|
6884
|
+
const SAME_BLOCK_INTERACTION_SELECTOR = "0x3b53d1db";
|
|
6885
|
+
const laneGuardActive = opts.laneGuardActive === true;
|
|
6877
6886
|
for (let sim = 1; ; sim++) {
|
|
6878
6887
|
try {
|
|
6879
6888
|
await provider.call({ to, from, data });
|
|
6880
6889
|
break;
|
|
6881
6890
|
} catch (simError) {
|
|
6882
6891
|
const decoded = decodeQuantumRevert(simError);
|
|
6892
|
+
if (laneGuardActive && decoded.selector === SAME_BLOCK_INTERACTION_SELECTOR) {
|
|
6893
|
+
onDebug(
|
|
6894
|
+
"\u21AA\uFE0F [lane-only] Simulated SameBlockInteraction is a stale-latest-block artifact (the simulated block contains this flow's own previous tx) \u2014 proceeding to broadcast; the on-chain guard stays authoritative"
|
|
6895
|
+
);
|
|
6896
|
+
break;
|
|
6897
|
+
}
|
|
6883
6898
|
if (decoded.selector === DEAD_ZONE_VIOLATION_SELECTOR && sim <= maxResimulations) {
|
|
6884
6899
|
const sigQuantum = Math.floor(quantumTimestamp / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
|
|
6885
6900
|
const realCurrentQuantum = Math.floor(now() / QUANTUM_WINDOW_SECONDS) * QUANTUM_WINDOW_SECONDS;
|
|
@@ -6915,6 +6930,144 @@ async function assertSafeQuantumSubmission(opts) {
|
|
|
6915
6930
|
await awaitSafeSubmissionWindow(quantumTimestamp, gateOpts);
|
|
6916
6931
|
}
|
|
6917
6932
|
|
|
6933
|
+
// src/utils/lane-guard.utils.ts
|
|
6934
|
+
var HARDHAT_CHAIN_IDS = /* @__PURE__ */ new Set([1337, 31337]);
|
|
6935
|
+
function laneGuardEnvEnabled() {
|
|
6936
|
+
return typeof process !== "undefined" && process.env?.LIT_LANE_GUARD === "1";
|
|
6937
|
+
}
|
|
6938
|
+
async function isLaneGuardActiveOnProvider(provider) {
|
|
6939
|
+
if (!laneGuardEnvEnabled())
|
|
6940
|
+
return false;
|
|
6941
|
+
if (!provider || typeof provider.send !== "function")
|
|
6942
|
+
return false;
|
|
6943
|
+
try {
|
|
6944
|
+
const [chainIdHex, clientVersion] = await Promise.all([
|
|
6945
|
+
provider.send("eth_chainId", []),
|
|
6946
|
+
provider.send("web3_clientVersion", [])
|
|
6947
|
+
]);
|
|
6948
|
+
const chainId = Number(chainIdHex);
|
|
6949
|
+
return HARDHAT_CHAIN_IDS.has(chainId) && typeof clientVersion === "string" && clientVersion.toLowerCase().includes("hardhat");
|
|
6950
|
+
} catch {
|
|
6951
|
+
return false;
|
|
6952
|
+
}
|
|
6953
|
+
}
|
|
6954
|
+
|
|
6955
|
+
// src/utils/withdrawal-reconciliation.utils.ts
|
|
6956
|
+
var defaultHttpGetJson = async (url) => {
|
|
6957
|
+
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
6958
|
+
let body = null;
|
|
6959
|
+
try {
|
|
6960
|
+
body = await res.json();
|
|
6961
|
+
} catch {
|
|
6962
|
+
body = null;
|
|
6963
|
+
}
|
|
6964
|
+
return { status: res.status, body };
|
|
6965
|
+
};
|
|
6966
|
+
async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = defaultHttpGetJson) {
|
|
6967
|
+
const base = esploraBaseUrl.replace(/\/+$/, "");
|
|
6968
|
+
const fundingRes = await httpGetJson(`${base}/tx/${spend.txid}`);
|
|
6969
|
+
if (fundingRes.status === 404) {
|
|
6970
|
+
return {
|
|
6971
|
+
spend,
|
|
6972
|
+
status: "UNFUNDED",
|
|
6973
|
+
reason: `Funding tx ${spend.txid} not found on the network`
|
|
6974
|
+
};
|
|
6975
|
+
}
|
|
6976
|
+
if (fundingRes.status !== 200) {
|
|
6977
|
+
throw new Error(
|
|
6978
|
+
`Withdrawal reconciliation: esplora /tx/${spend.txid} returned HTTP ${fundingRes.status}`
|
|
6979
|
+
);
|
|
6980
|
+
}
|
|
6981
|
+
const funding = fundingRes.body;
|
|
6982
|
+
if (!funding?.status?.confirmed) {
|
|
6983
|
+
return {
|
|
6984
|
+
spend,
|
|
6985
|
+
status: "UNFUNDED",
|
|
6986
|
+
reason: `Funding tx ${spend.txid} is not confirmed yet`
|
|
6987
|
+
};
|
|
6988
|
+
}
|
|
6989
|
+
const output = funding.vout?.[spend.vout];
|
|
6990
|
+
if (!output || typeof output.value !== "number") {
|
|
6991
|
+
return {
|
|
6992
|
+
spend,
|
|
6993
|
+
status: "CORRUPT",
|
|
6994
|
+
reason: `Authorized vout ${spend.vout} does not exist on tx ${spend.txid} (${funding.vout?.length ?? 0} outputs)`
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6997
|
+
const onChainOutputValue = output.value;
|
|
6998
|
+
if (onChainOutputValue !== spend.satoshis) {
|
|
6999
|
+
return {
|
|
7000
|
+
spend,
|
|
7001
|
+
status: "CORRUPT",
|
|
7002
|
+
onChainOutputValue,
|
|
7003
|
+
reason: `Authorization declares ${spend.satoshis} sats for ${spend.txid}:${spend.vout} but the on-chain output is ${onChainOutputValue} sats`
|
|
7004
|
+
};
|
|
7005
|
+
}
|
|
7006
|
+
if (spend.targetAmount > onChainOutputValue) {
|
|
7007
|
+
return {
|
|
7008
|
+
spend,
|
|
7009
|
+
status: "CORRUPT",
|
|
7010
|
+
onChainOutputValue,
|
|
7011
|
+
reason: `Authorized targetAmount ${spend.targetAmount} sats exceeds the ${onChainOutputValue}-sat output it is authorized against`
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
7014
|
+
const outspendRes = await httpGetJson(
|
|
7015
|
+
`${base}/tx/${spend.txid}/outspend/${spend.vout}`
|
|
7016
|
+
);
|
|
7017
|
+
if (outspendRes.status !== 200) {
|
|
7018
|
+
throw new Error(
|
|
7019
|
+
`Withdrawal reconciliation: esplora outspend for ${spend.txid}:${spend.vout} returned HTTP ${outspendRes.status}`
|
|
7020
|
+
);
|
|
7021
|
+
}
|
|
7022
|
+
const outspend = outspendRes.body;
|
|
7023
|
+
if (outspend?.spent) {
|
|
7024
|
+
const spendingTxid = typeof outspend.txid === "string" && outspend.txid ? outspend.txid : null;
|
|
7025
|
+
if (spendingTxid) {
|
|
7026
|
+
const spendingRes = await httpGetJson(`${base}/tx/${spendingTxid}`);
|
|
7027
|
+
if (spendingRes.status === 200) {
|
|
7028
|
+
const spendingTx = spendingRes.body;
|
|
7029
|
+
const includesOutpoint = (spendingTx.vin ?? []).some(
|
|
7030
|
+
(v) => v.txid === spend.txid && Number(v.vout) === spend.vout
|
|
7031
|
+
);
|
|
7032
|
+
if (includesOutpoint) {
|
|
7033
|
+
const paidToTargetSats = (spendingTx.vout ?? []).filter(
|
|
7034
|
+
(o) => (o.scriptpubkey_address ?? "").toLowerCase() === spend.targetAddress.toLowerCase()
|
|
7035
|
+
).reduce((sum, o) => sum + (o.value ?? 0), 0);
|
|
7036
|
+
if (paidToTargetSats > 0) {
|
|
7037
|
+
return {
|
|
7038
|
+
spend,
|
|
7039
|
+
status: "EXECUTED",
|
|
7040
|
+
spendingTxid,
|
|
7041
|
+
paidToTargetSats,
|
|
7042
|
+
onChainOutputValue,
|
|
7043
|
+
reason: `Outpoint spent by ${spendingTxid}, paying ${paidToTargetSats} sats to the authorized target \u2014 withdrawal complete`
|
|
7044
|
+
};
|
|
7045
|
+
}
|
|
7046
|
+
return {
|
|
7047
|
+
spend,
|
|
7048
|
+
status: "SPENT_MISMATCH",
|
|
7049
|
+
spendingTxid,
|
|
7050
|
+
onChainOutputValue,
|
|
7051
|
+
reason: `Outpoint spent by ${spendingTxid} which pays the authorized target nothing \u2014 authorization is unexecutable`
|
|
7052
|
+
};
|
|
7053
|
+
}
|
|
7054
|
+
}
|
|
7055
|
+
}
|
|
7056
|
+
return {
|
|
7057
|
+
spend,
|
|
7058
|
+
status: "EXECUTABLE",
|
|
7059
|
+
onChainOutputValue,
|
|
7060
|
+
reason: "Esplora claims the outpoint is spent but no spending tx provably includes it (known regtest-faucet artifact) \u2014 treated as unspent"
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
return {
|
|
7064
|
+
spend,
|
|
7065
|
+
status: "EXECUTABLE",
|
|
7066
|
+
onChainOutputValue,
|
|
7067
|
+
reason: "Outpoint confirmed, unspent, and coherent with the authorization"
|
|
7068
|
+
};
|
|
7069
|
+
}
|
|
7070
|
+
|
|
6918
7071
|
// src/utils/btc-withdrawal-message.ts
|
|
6919
7072
|
import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
|
|
6920
7073
|
var QUANTUM_SECONDS = 60;
|
|
@@ -15776,7 +15929,7 @@ function createMockTokenManager(config) {
|
|
|
15776
15929
|
|
|
15777
15930
|
// src/modules/diamond-hands-sdk.ts
|
|
15778
15931
|
var import_dh_lit_actions2 = __toESM(require_pkg_src());
|
|
15779
|
-
import { LitOps } from "@gvnrdao/dh-lit-ops";
|
|
15932
|
+
import { LitOps, broadcastSignedBitcoinTransaction } from "@gvnrdao/dh-lit-ops";
|
|
15780
15933
|
|
|
15781
15934
|
// src/utils/telegram-messaging.utils.ts
|
|
15782
15935
|
var TelegramBot = null;
|
|
@@ -16261,6 +16414,10 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
16261
16414
|
this.litOps = new LitOps({
|
|
16262
16415
|
mode: config.mode || "standalone",
|
|
16263
16416
|
network: config.litNetwork || DEFAULT_LIT_NETWORK,
|
|
16417
|
+
// Completes the module's chainId resolution chain (jsParams shapes →
|
|
16418
|
+
// config.chainId) — e.g. the lane validator-override chain binding
|
|
16419
|
+
// reads it when a jsParams shape lacks chainId.
|
|
16420
|
+
chainId: config.chainId,
|
|
16264
16421
|
signer: config.mode === "standalone" ? config.litOpsSigner : void 0,
|
|
16265
16422
|
serviceEndpoint: isServiceModeConfig(config) ? config.serviceEndpoint : void 0,
|
|
16266
16423
|
// Hand lit-ops a session-aware auth getter so every request it makes
|
|
@@ -16938,6 +17095,19 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
16938
17095
|
}
|
|
16939
17096
|
const MAX_FULL_RETRIES = 3;
|
|
16940
17097
|
for (let fullRetry = 1; fullRetry <= MAX_FULL_RETRIES; fullRetry++) {
|
|
17098
|
+
if (await this.isLocalLaneGuardActive()) {
|
|
17099
|
+
const phase = Math.floor(Date.now() / 1e3) % 60;
|
|
17100
|
+
if (phase > 30) {
|
|
17101
|
+
const waitMs = (60 - phase + 1) * 1e3;
|
|
17102
|
+
if (this.config.debug) {
|
|
17103
|
+
log.info(
|
|
17104
|
+
`\u23F3 Quantum-phase alignment (lane-only): ${phase}s into the quantum \u2014 waiting ${Math.ceil(waitMs / 1e3)}s for the next boundary before authorization`,
|
|
17105
|
+
{}
|
|
17106
|
+
);
|
|
17107
|
+
}
|
|
17108
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
17109
|
+
}
|
|
17110
|
+
}
|
|
16941
17111
|
if (fullRetry > 1 && this.config.debug) {
|
|
16942
17112
|
log.info(
|
|
16943
17113
|
`\u{1F504} Full mint retry ${fullRetry}/${MAX_FULL_RETRIES} due to timestamp staleness...`,
|
|
@@ -17686,6 +17856,7 @@ var DiamondHandsSDK = class _DiamondHandsSDK {
|
|
|
17686
17856
|
from: fromAddress,
|
|
17687
17857
|
data: mintCalldata,
|
|
17688
17858
|
quantumTimestamp: Number(validationResponse.timestamp),
|
|
17859
|
+
laneGuardActive: await this.isLocalLaneGuardActive(),
|
|
17689
17860
|
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
17690
17861
|
});
|
|
17691
17862
|
} catch (simError) {
|
|
@@ -18981,6 +19152,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
18981
19152
|
from: await extendSigner.getAddress(),
|
|
18982
19153
|
data: extendCalldata,
|
|
18983
19154
|
quantumTimestamp: Number(quantumTimestamp),
|
|
19155
|
+
laneGuardActive: await this.isLocalLaneGuardActive(),
|
|
18984
19156
|
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
18985
19157
|
});
|
|
18986
19158
|
} catch (simError) {
|
|
@@ -19881,6 +20053,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
19881
20053
|
from: paymentFrom,
|
|
19882
20054
|
data: paymentCalldata,
|
|
19883
20055
|
quantumTimestamp: Number(quantumTimestamp),
|
|
20056
|
+
laneGuardActive: await this.isLocalLaneGuardActive(),
|
|
19884
20057
|
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
19885
20058
|
});
|
|
19886
20059
|
} catch (simError) {
|
|
@@ -20620,6 +20793,7 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
20620
20793
|
from: await withdrawSigner.getAddress(),
|
|
20621
20794
|
data: withdrawCalldata,
|
|
20622
20795
|
quantumTimestamp: Number(withdrawalParams.quantumTimestamp),
|
|
20796
|
+
laneGuardActive: await this.isLocalLaneGuardActive(),
|
|
20623
20797
|
onDebug: this.config.debug ? (msg) => log.info(msg, {}) : void 0
|
|
20624
20798
|
});
|
|
20625
20799
|
tx = await sendEip1559Transaction({
|
|
@@ -20774,15 +20948,180 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
20774
20948
|
* @param request - Execution request with positionId, utxoIdentifier, and networkFee
|
|
20775
20949
|
* @returns Execution result with Bitcoin txid
|
|
20776
20950
|
*/
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20951
|
+
/**
|
|
20952
|
+
* STANDALONE Phase 2: sign + broadcast the withdrawal Bitcoin transaction
|
|
20953
|
+
* in-process — the same orchestration lit-ops-server performs, composed
|
|
20954
|
+
* from the same primitives (`litOps.signBTCTransaction`, which has always
|
|
20955
|
+
* had a standalone path, and `broadcastSignedBitcoinTransaction`).
|
|
20956
|
+
* Both modes must support withdrawal execution (Master mandate 2026-07-21).
|
|
20957
|
+
*
|
|
20958
|
+
* The service path's post-broadcast CRIT-2 re-verification exists to catch
|
|
20959
|
+
* a LYING SERVER; here the broadcast happens locally from the signatures we
|
|
20960
|
+
* just obtained, so the esplora-returned txid is already first-hand.
|
|
20961
|
+
*/
|
|
20962
|
+
async executeBTCWithdrawalStandalonePhase2(request, envelope, txid, vout, chainId) {
|
|
20963
|
+
const positionDetails = await this.getPositionDetailsView(
|
|
20964
|
+
request.positionId
|
|
20965
|
+
);
|
|
20966
|
+
if (!positionDetails) {
|
|
20967
|
+
return { success: false, error: `Position ${request.positionId} not found` };
|
|
20968
|
+
}
|
|
20969
|
+
const pkpId = positionDetails.pkpId;
|
|
20970
|
+
const vaultAddress = positionDetails.vaultAddress;
|
|
20971
|
+
if (!pkpId || pkpId === ZeroHash) {
|
|
20972
|
+
return { success: false, error: "Position has no PKP" };
|
|
20973
|
+
}
|
|
20974
|
+
if (!vaultAddress) {
|
|
20975
|
+
return { success: false, error: "Position has no vault address" };
|
|
20976
|
+
}
|
|
20977
|
+
const pkpCache = this.cacheManager.getCache("pkp-data", {
|
|
20978
|
+
maxSize: 500,
|
|
20979
|
+
ttlMs: 24 * 60 * 60 * 1e3
|
|
20980
|
+
});
|
|
20981
|
+
let publicKey = pkpCache.get(request.positionId)?.publicKey;
|
|
20982
|
+
if (!publicKey) {
|
|
20983
|
+
const derived = await this.litOps.deriveDiamondHandsLoanPkpPublicKey({
|
|
20984
|
+
pkpId
|
|
20985
|
+
});
|
|
20986
|
+
publicKey = derived.publicKey;
|
|
20987
|
+
}
|
|
20988
|
+
if (!publicKey) {
|
|
20989
|
+
return { success: false, error: "Failed to resolve PKP public key" };
|
|
20990
|
+
}
|
|
20991
|
+
const chipotlePkpAddress = computeAddress(publicKey);
|
|
20992
|
+
const STANDALONE_CHAIN_NAMES = {
|
|
20993
|
+
1: "ethereum",
|
|
20994
|
+
11155111: "sepolia",
|
|
20995
|
+
1337: "hardhat",
|
|
20996
|
+
31337: "hardhat"
|
|
20997
|
+
};
|
|
20998
|
+
const chainName = STANDALONE_CHAIN_NAMES[chainId];
|
|
20999
|
+
if (!chainName) {
|
|
20780
21000
|
return {
|
|
20781
21001
|
success: false,
|
|
20782
|
-
error:
|
|
21002
|
+
error: `Unsupported chainId ${chainId} for standalone BTC withdrawal (supported: 1, 11155111, 1337, 31337)`
|
|
20783
21003
|
};
|
|
20784
21004
|
}
|
|
20785
|
-
|
|
21005
|
+
const bitcoinNetworkName = chainId === 1 ? "mainnet" : "regtest";
|
|
21006
|
+
const bitcoin = await import("bitcoinjs-lib");
|
|
21007
|
+
const bitcoinNetwork = chainId === 1 ? bitcoin.networks.bitcoin : chainId === 1337 || chainId === 31337 ? bitcoin.networks.regtest : bitcoin.networks.testnet;
|
|
21008
|
+
let scriptPubKeyBuffer;
|
|
21009
|
+
if (vaultAddress.startsWith("bcrt1q")) {
|
|
21010
|
+
const bech322 = await import("bech32");
|
|
21011
|
+
const decoded = bech322.bech32.decode(vaultAddress);
|
|
21012
|
+
const witnessProgram = Buffer.from(
|
|
21013
|
+
bech322.bech32.fromWords(decoded.words.slice(1))
|
|
21014
|
+
);
|
|
21015
|
+
if (witnessProgram.length !== 20) {
|
|
21016
|
+
return {
|
|
21017
|
+
success: false,
|
|
21018
|
+
error: `Vault address ${vaultAddress} decodes to a ${witnessProgram.length}-byte witness program \u2014 expected 20-byte P2WPKH`
|
|
21019
|
+
};
|
|
21020
|
+
}
|
|
21021
|
+
scriptPubKeyBuffer = Buffer.concat([
|
|
21022
|
+
Buffer.from([0, witnessProgram.length]),
|
|
21023
|
+
witnessProgram
|
|
21024
|
+
]);
|
|
21025
|
+
} else {
|
|
21026
|
+
scriptPubKeyBuffer = bitcoin.address.toOutputScript(
|
|
21027
|
+
vaultAddress,
|
|
21028
|
+
bitcoinNetwork
|
|
21029
|
+
);
|
|
21030
|
+
}
|
|
21031
|
+
const scriptPubKey = scriptPubKeyBuffer.toString("hex");
|
|
21032
|
+
const executionUtxos = (envelope.utxos && envelope.utxos.length ? envelope.utxos : [{ txid, vout, value: request.utxoSatoshis }]).map((u) => ({ ...u, scriptPubKey }));
|
|
21033
|
+
const dc = this.config.contractAddresses || {};
|
|
21034
|
+
const signerContractAddresses = {
|
|
21035
|
+
LoanOperationsManagerModule: dc.loanOperationsManager ?? "",
|
|
21036
|
+
PositionManager: dc.positionManager ?? "",
|
|
21037
|
+
BTCSpendAuthorizer: dc.btcSpendAuthorizer ?? "",
|
|
21038
|
+
PositionDelegateRegistry: dc.positionDelegateRegistry ?? "",
|
|
21039
|
+
OperationAuthorizationRegistry: dc.operationAuthorizationRegistry ?? "",
|
|
21040
|
+
...dc.bitcoinProviderRegistry ? { BitcoinProviderRegistry: dc.bitcoinProviderRegistry } : {}
|
|
21041
|
+
};
|
|
21042
|
+
const signResult = await this.litOps.signBTCTransaction({
|
|
21043
|
+
auth: {
|
|
21044
|
+
positionId: envelope.positionId,
|
|
21045
|
+
utxo: { txid, vout, value: request.utxoSatoshis, scriptPubKey },
|
|
21046
|
+
utxos: executionUtxos,
|
|
21047
|
+
networkFee: envelope.networkFee,
|
|
21048
|
+
targetAddress: envelope.targetAddress,
|
|
21049
|
+
targetAmount: envelope.targetAmount,
|
|
21050
|
+
timestamp: envelope.timestamp,
|
|
21051
|
+
signature: envelope.userSignature,
|
|
21052
|
+
chainId
|
|
21053
|
+
},
|
|
21054
|
+
publicKey,
|
|
21055
|
+
pkpId: chipotlePkpAddress,
|
|
21056
|
+
chain: chainName,
|
|
21057
|
+
bitcoinNetwork: bitcoinNetworkName,
|
|
21058
|
+
rpcUrl: this.config.ethRpcUrl,
|
|
21059
|
+
contractAddresses: signerContractAddresses
|
|
21060
|
+
});
|
|
21061
|
+
if (!signResult.success) {
|
|
21062
|
+
return {
|
|
21063
|
+
success: false,
|
|
21064
|
+
error: signResult.error ?? "BTC transaction signing failed"
|
|
21065
|
+
};
|
|
21066
|
+
}
|
|
21067
|
+
if (!signResult.unsignedTxHex || !signResult.signatures?.length) {
|
|
21068
|
+
return {
|
|
21069
|
+
success: false,
|
|
21070
|
+
error: "BTC signing returned incomplete data (missing unsignedTxHex/signatures)"
|
|
21071
|
+
};
|
|
21072
|
+
}
|
|
21073
|
+
if (!signResult.userReceivesAmount) {
|
|
21074
|
+
return { success: false, error: "BTC signing did not return userReceivesAmount" };
|
|
21075
|
+
}
|
|
21076
|
+
const broadcastUrl = this.config.bitcoinProviders?.[0]?.url;
|
|
21077
|
+
if (!broadcastUrl) {
|
|
21078
|
+
return {
|
|
21079
|
+
success: false,
|
|
21080
|
+
error: "Standalone Phase 2 broadcast requires config.bitcoinProviders[0].url (esplora base)"
|
|
21081
|
+
};
|
|
21082
|
+
}
|
|
21083
|
+
try {
|
|
21084
|
+
const { txid: btcTxid } = await broadcastSignedBitcoinTransaction({
|
|
21085
|
+
signatures: signResult.signatures,
|
|
21086
|
+
utxos: executionUtxos,
|
|
21087
|
+
pkpPublicKey: signResult.pkpPublicKey ?? publicKey,
|
|
21088
|
+
destination: envelope.targetAddress,
|
|
21089
|
+
userReceivesAmount: signResult.userReceivesAmount,
|
|
21090
|
+
changeAmount: signResult.changeAmount,
|
|
21091
|
+
pkpBtcAddress: signResult.pkpBtcAddress,
|
|
21092
|
+
bitcoinNetwork: bitcoinNetworkName,
|
|
21093
|
+
bitcoinRpcUrl: broadcastUrl,
|
|
21094
|
+
...this.config.debug ? {
|
|
21095
|
+
debug: (level, msg, data) => {
|
|
21096
|
+
if (level === "warn")
|
|
21097
|
+
log.warn(msg, { data });
|
|
21098
|
+
else if (level === "error")
|
|
21099
|
+
log.error(msg, { data });
|
|
21100
|
+
else
|
|
21101
|
+
log.info(msg, { data });
|
|
21102
|
+
}
|
|
21103
|
+
} : {}
|
|
21104
|
+
});
|
|
21105
|
+
if (this.config.debug) {
|
|
21106
|
+
log.info("\u2705 Standalone Phase 2 broadcast successful", { txid: btcTxid });
|
|
21107
|
+
}
|
|
21108
|
+
return {
|
|
21109
|
+
success: true,
|
|
21110
|
+
txid: btcTxid,
|
|
21111
|
+
amountSent: signResult.userReceivesAmount,
|
|
21112
|
+
networkFee: signResult.networkFee,
|
|
21113
|
+
destination: envelope.targetAddress
|
|
21114
|
+
};
|
|
21115
|
+
} catch (broadcastError) {
|
|
21116
|
+
return {
|
|
21117
|
+
success: false,
|
|
21118
|
+
error: `BTC broadcast failed: ${broadcastError instanceof Error ? broadcastError.message : String(broadcastError)}`
|
|
21119
|
+
};
|
|
21120
|
+
}
|
|
21121
|
+
}
|
|
21122
|
+
async executeBTCWithdrawal(request) {
|
|
21123
|
+
this.ensureInitialized();
|
|
21124
|
+
if (this.config.mode === "service" && !this.config.serviceEndpoint) {
|
|
20786
21125
|
return { success: false, error: "serviceEndpoint required for BTC withdrawal" };
|
|
20787
21126
|
}
|
|
20788
21127
|
if (!request.positionId) {
|
|
@@ -20837,6 +21176,15 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
20837
21176
|
signer,
|
|
20838
21177
|
...request.utxos ? { utxos: request.utxos } : {}
|
|
20839
21178
|
});
|
|
21179
|
+
if (this.config.mode !== "service") {
|
|
21180
|
+
return await this.executeBTCWithdrawalStandalonePhase2(
|
|
21181
|
+
request,
|
|
21182
|
+
envelope,
|
|
21183
|
+
txid,
|
|
21184
|
+
vout,
|
|
21185
|
+
chainId
|
|
21186
|
+
);
|
|
21187
|
+
}
|
|
20840
21188
|
const headers = {
|
|
20841
21189
|
"Content-Type": "application/json",
|
|
20842
21190
|
...await this.getAuthHeader()
|
|
@@ -21096,6 +21444,41 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21096
21444
|
)
|
|
21097
21445
|
}));
|
|
21098
21446
|
}
|
|
21447
|
+
/**
|
|
21448
|
+
* Reconcile every pending withdrawal against BITCOIN truth (incident
|
|
21449
|
+
* 2026-07-22): `getPendingWithdrawals` only reflects on-chain
|
|
21450
|
+
* authorizations, and the contract can never know whether the Phase-2 BTC
|
|
21451
|
+
* broadcast happened. Callers MUST use the returned `status` to decide what
|
|
21452
|
+
* to offer:
|
|
21453
|
+
* - EXECUTABLE → offer Execute (the only status that may).
|
|
21454
|
+
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof.
|
|
21455
|
+
* - SPENT_MISMATCH → unexecutable; offer cancelPendingWithdrawal.
|
|
21456
|
+
* - CORRUPT → authorization contradicts the chain (e.g. declared
|
|
21457
|
+
* satoshis ≠ real output value); offer Cancel &
|
|
21458
|
+
* re-request — Execute can only die at the signer guard.
|
|
21459
|
+
* - UNFUNDED → funding tx unknown/unconfirmed; wait.
|
|
21460
|
+
*
|
|
21461
|
+
* @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
|
|
21462
|
+
* `/v1/proxy/esplora/<network>`). Falls back to
|
|
21463
|
+
* `config.bitcoinProviders[0].url`; throws when neither is configured.
|
|
21464
|
+
*/
|
|
21465
|
+
async reconcilePendingWithdrawals(positionId, opts) {
|
|
21466
|
+
this.ensureInitialized();
|
|
21467
|
+
const esploraBaseUrl = opts?.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
|
|
21468
|
+
if (!esploraBaseUrl) {
|
|
21469
|
+
throw new SDKError({
|
|
21470
|
+
code: "SDK_ESPLORA_NOT_CONFIGURED",
|
|
21471
|
+
message: "reconcilePendingWithdrawals requires opts.esploraBaseUrl or config.bitcoinProviders[0].url",
|
|
21472
|
+
category: "CONFIGURATION" /* CONFIGURATION */,
|
|
21473
|
+
severity: "HIGH" /* HIGH */,
|
|
21474
|
+
originalError: new Error("esplora base URL not configured")
|
|
21475
|
+
});
|
|
21476
|
+
}
|
|
21477
|
+
const spends = await this.getPendingWithdrawals(positionId);
|
|
21478
|
+
return Promise.all(
|
|
21479
|
+
spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
|
|
21480
|
+
);
|
|
21481
|
+
}
|
|
21099
21482
|
/**
|
|
21100
21483
|
* Cancel a pending BTC withdrawal
|
|
21101
21484
|
*
|
|
@@ -22154,6 +22537,25 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
22154
22537
|
const chain = this.config.chain?.toLowerCase() || "";
|
|
22155
22538
|
return chain.includes("mainnet");
|
|
22156
22539
|
}
|
|
22540
|
+
/**
|
|
22541
|
+
* Local-lane activation, verified against the LIVE node at call time (not
|
|
22542
|
+
* the chainId cached at init — a provider that switched networks after
|
|
22543
|
+
* init fails the fresh probe). Zero RPC cost outside a lane process tree:
|
|
22544
|
+
* without LIT_LANE_GUARD=1 this short-circuits to false before any probe.
|
|
22545
|
+
*/
|
|
22546
|
+
async isLocalLaneGuardActive() {
|
|
22547
|
+
if (!laneGuardEnvEnabled())
|
|
22548
|
+
return false;
|
|
22549
|
+
let provider;
|
|
22550
|
+
try {
|
|
22551
|
+
provider = this.config.provider ?? this.getProviderOrThrow();
|
|
22552
|
+
} catch {
|
|
22553
|
+
return false;
|
|
22554
|
+
}
|
|
22555
|
+
return isLaneGuardActiveOnProvider(
|
|
22556
|
+
provider
|
|
22557
|
+
);
|
|
22558
|
+
}
|
|
22157
22559
|
/**
|
|
22158
22560
|
* Get Bitcoin network based on chain.
|
|
22159
22561
|
*
|
|
@@ -22975,6 +23377,7 @@ export {
|
|
|
22975
23377
|
createPKPManager,
|
|
22976
23378
|
createWithdrawalAddressModule,
|
|
22977
23379
|
DiamondHandsSDK as default,
|
|
23380
|
+
defaultHttpGetJson,
|
|
22978
23381
|
failure,
|
|
22979
23382
|
fetchProtocolPauseStatus,
|
|
22980
23383
|
firstSuccess,
|
|
@@ -22998,6 +23401,7 @@ export {
|
|
|
22998
23401
|
mapError,
|
|
22999
23402
|
match,
|
|
23000
23403
|
numericToPositionStatus,
|
|
23404
|
+
reconcileAuthorizedSpend,
|
|
23001
23405
|
setPositionDelegate,
|
|
23002
23406
|
success,
|
|
23003
23407
|
toPromise,
|
|
@@ -89,6 +89,18 @@ interface BaseSDKConfig {
|
|
|
89
89
|
litActionCid?: string;
|
|
90
90
|
litNodeConnectTimeoutMs?: number;
|
|
91
91
|
subgraphUrl?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Standalone/local-lane Bitcoin providers (esplora bases). LIT Actions pull
|
|
94
|
+
* every registered provider from the on-chain BitcoinProviderRegistry and
|
|
95
|
+
* the server's executePendingWithdrawal handles service-mode broadcast —
|
|
96
|
+
* this field never feeds those paths. It drives standalone-mode flows only:
|
|
97
|
+
* the standalone Phase 2 withdrawal broadcast, hardhat-lane per-call
|
|
98
|
+
* overrides, and `network` as a bitcoinNetwork inference fallback.
|
|
99
|
+
*/
|
|
100
|
+
bitcoinProviders?: Array<{
|
|
101
|
+
url: string;
|
|
102
|
+
network?: "mainnet" | "testnet" | "regtest";
|
|
103
|
+
}>;
|
|
92
104
|
priceProviders?: PriceProviderConfig[];
|
|
93
105
|
/** PKP Ethereum address for signed price oracle (Lit jsParams pkpId); optional */
|
|
94
106
|
priceOraclePkpId?: string;
|