@gvnrdao/dh-sdk 0.0.306 → 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/index.d.ts +2 -0
- package/dist/index.js +155 -0
- package/dist/index.mjs +153 -0
- package/dist/modules/diamond-hands-sdk.d.ts +22 -0
- package/dist/utils/withdrawal-reconciliation.utils.d.ts +64 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -52,3 +52,5 @@ export { SDKError, ErrorCategory, ErrorSeverity } from './utils/error-handler';
|
|
|
52
52
|
export { setPositionDelegate, getPositionDelegate } from './utils/position-delegate.utils';
|
|
53
53
|
export { buildBtcExecuteEnvelope, } from './utils/btc-withdrawal-message';
|
|
54
54
|
export type { BtcExecuteSignParams, BtcExecuteSignedEnvelope, } from './utils/btc-withdrawal-message';
|
|
55
|
+
export { reconcileAuthorizedSpend, defaultHttpGetJson, } from './utils/withdrawal-reconciliation.utils';
|
|
56
|
+
export type { PendingWithdrawalStatus, AuthorizedSpendLike, ReconciledWithdrawal, HttpGetJson, } from './utils/withdrawal-reconciliation.utils';
|
package/dist/index.js
CHANGED
|
@@ -5591,6 +5591,7 @@ __export(src_exports, {
|
|
|
5591
5591
|
createPKPManager: () => createPKPManager,
|
|
5592
5592
|
createWithdrawalAddressModule: () => createWithdrawalAddressModule,
|
|
5593
5593
|
default: () => DiamondHandsSDK,
|
|
5594
|
+
defaultHttpGetJson: () => defaultHttpGetJson,
|
|
5594
5595
|
failure: () => failure,
|
|
5595
5596
|
fetchProtocolPauseStatus: () => fetchProtocolPauseStatus,
|
|
5596
5597
|
firstSuccess: () => firstSuccess,
|
|
@@ -5614,6 +5615,7 @@ __export(src_exports, {
|
|
|
5614
5615
|
mapError: () => mapError,
|
|
5615
5616
|
match: () => match,
|
|
5616
5617
|
numericToPositionStatus: () => numericToPositionStatus,
|
|
5618
|
+
reconcileAuthorizedSpend: () => reconcileAuthorizedSpend,
|
|
5617
5619
|
setPositionDelegate: () => setPositionDelegate,
|
|
5618
5620
|
success: () => success,
|
|
5619
5621
|
toPromise: () => toPromise,
|
|
@@ -7028,6 +7030,122 @@ async function isLaneGuardActiveOnProvider(provider) {
|
|
|
7028
7030
|
}
|
|
7029
7031
|
}
|
|
7030
7032
|
|
|
7033
|
+
// src/utils/withdrawal-reconciliation.utils.ts
|
|
7034
|
+
var defaultHttpGetJson = async (url) => {
|
|
7035
|
+
const res = await fetch(url, { headers: { accept: "application/json" } });
|
|
7036
|
+
let body = null;
|
|
7037
|
+
try {
|
|
7038
|
+
body = await res.json();
|
|
7039
|
+
} catch {
|
|
7040
|
+
body = null;
|
|
7041
|
+
}
|
|
7042
|
+
return { status: res.status, body };
|
|
7043
|
+
};
|
|
7044
|
+
async function reconcileAuthorizedSpend(spend, esploraBaseUrl, httpGetJson = defaultHttpGetJson) {
|
|
7045
|
+
const base = esploraBaseUrl.replace(/\/+$/, "");
|
|
7046
|
+
const fundingRes = await httpGetJson(`${base}/tx/${spend.txid}`);
|
|
7047
|
+
if (fundingRes.status === 404) {
|
|
7048
|
+
return {
|
|
7049
|
+
spend,
|
|
7050
|
+
status: "UNFUNDED",
|
|
7051
|
+
reason: `Funding tx ${spend.txid} not found on the network`
|
|
7052
|
+
};
|
|
7053
|
+
}
|
|
7054
|
+
if (fundingRes.status !== 200) {
|
|
7055
|
+
throw new Error(
|
|
7056
|
+
`Withdrawal reconciliation: esplora /tx/${spend.txid} returned HTTP ${fundingRes.status}`
|
|
7057
|
+
);
|
|
7058
|
+
}
|
|
7059
|
+
const funding = fundingRes.body;
|
|
7060
|
+
if (!funding?.status?.confirmed) {
|
|
7061
|
+
return {
|
|
7062
|
+
spend,
|
|
7063
|
+
status: "UNFUNDED",
|
|
7064
|
+
reason: `Funding tx ${spend.txid} is not confirmed yet`
|
|
7065
|
+
};
|
|
7066
|
+
}
|
|
7067
|
+
const output = funding.vout?.[spend.vout];
|
|
7068
|
+
if (!output || typeof output.value !== "number") {
|
|
7069
|
+
return {
|
|
7070
|
+
spend,
|
|
7071
|
+
status: "CORRUPT",
|
|
7072
|
+
reason: `Authorized vout ${spend.vout} does not exist on tx ${spend.txid} (${funding.vout?.length ?? 0} outputs)`
|
|
7073
|
+
};
|
|
7074
|
+
}
|
|
7075
|
+
const onChainOutputValue = output.value;
|
|
7076
|
+
if (onChainOutputValue !== spend.satoshis) {
|
|
7077
|
+
return {
|
|
7078
|
+
spend,
|
|
7079
|
+
status: "CORRUPT",
|
|
7080
|
+
onChainOutputValue,
|
|
7081
|
+
reason: `Authorization declares ${spend.satoshis} sats for ${spend.txid}:${spend.vout} but the on-chain output is ${onChainOutputValue} sats`
|
|
7082
|
+
};
|
|
7083
|
+
}
|
|
7084
|
+
if (spend.targetAmount > onChainOutputValue) {
|
|
7085
|
+
return {
|
|
7086
|
+
spend,
|
|
7087
|
+
status: "CORRUPT",
|
|
7088
|
+
onChainOutputValue,
|
|
7089
|
+
reason: `Authorized targetAmount ${spend.targetAmount} sats exceeds the ${onChainOutputValue}-sat output it is authorized against`
|
|
7090
|
+
};
|
|
7091
|
+
}
|
|
7092
|
+
const outspendRes = await httpGetJson(
|
|
7093
|
+
`${base}/tx/${spend.txid}/outspend/${spend.vout}`
|
|
7094
|
+
);
|
|
7095
|
+
if (outspendRes.status !== 200) {
|
|
7096
|
+
throw new Error(
|
|
7097
|
+
`Withdrawal reconciliation: esplora outspend for ${spend.txid}:${spend.vout} returned HTTP ${outspendRes.status}`
|
|
7098
|
+
);
|
|
7099
|
+
}
|
|
7100
|
+
const outspend = outspendRes.body;
|
|
7101
|
+
if (outspend?.spent) {
|
|
7102
|
+
const spendingTxid = typeof outspend.txid === "string" && outspend.txid ? outspend.txid : null;
|
|
7103
|
+
if (spendingTxid) {
|
|
7104
|
+
const spendingRes = await httpGetJson(`${base}/tx/${spendingTxid}`);
|
|
7105
|
+
if (spendingRes.status === 200) {
|
|
7106
|
+
const spendingTx = spendingRes.body;
|
|
7107
|
+
const includesOutpoint = (spendingTx.vin ?? []).some(
|
|
7108
|
+
(v) => v.txid === spend.txid && Number(v.vout) === spend.vout
|
|
7109
|
+
);
|
|
7110
|
+
if (includesOutpoint) {
|
|
7111
|
+
const paidToTargetSats = (spendingTx.vout ?? []).filter(
|
|
7112
|
+
(o) => (o.scriptpubkey_address ?? "").toLowerCase() === spend.targetAddress.toLowerCase()
|
|
7113
|
+
).reduce((sum, o) => sum + (o.value ?? 0), 0);
|
|
7114
|
+
if (paidToTargetSats > 0) {
|
|
7115
|
+
return {
|
|
7116
|
+
spend,
|
|
7117
|
+
status: "EXECUTED",
|
|
7118
|
+
spendingTxid,
|
|
7119
|
+
paidToTargetSats,
|
|
7120
|
+
onChainOutputValue,
|
|
7121
|
+
reason: `Outpoint spent by ${spendingTxid}, paying ${paidToTargetSats} sats to the authorized target \u2014 withdrawal complete`
|
|
7122
|
+
};
|
|
7123
|
+
}
|
|
7124
|
+
return {
|
|
7125
|
+
spend,
|
|
7126
|
+
status: "SPENT_MISMATCH",
|
|
7127
|
+
spendingTxid,
|
|
7128
|
+
onChainOutputValue,
|
|
7129
|
+
reason: `Outpoint spent by ${spendingTxid} which pays the authorized target nothing \u2014 authorization is unexecutable`
|
|
7130
|
+
};
|
|
7131
|
+
}
|
|
7132
|
+
}
|
|
7133
|
+
}
|
|
7134
|
+
return {
|
|
7135
|
+
spend,
|
|
7136
|
+
status: "EXECUTABLE",
|
|
7137
|
+
onChainOutputValue,
|
|
7138
|
+
reason: "Esplora claims the outpoint is spent but no spending tx provably includes it (known regtest-faucet artifact) \u2014 treated as unspent"
|
|
7139
|
+
};
|
|
7140
|
+
}
|
|
7141
|
+
return {
|
|
7142
|
+
spend,
|
|
7143
|
+
status: "EXECUTABLE",
|
|
7144
|
+
onChainOutputValue,
|
|
7145
|
+
reason: "Outpoint confirmed, unspent, and coherent with the authorization"
|
|
7146
|
+
};
|
|
7147
|
+
}
|
|
7148
|
+
|
|
7031
7149
|
// src/utils/btc-withdrawal-message.ts
|
|
7032
7150
|
var import_ethers6 = require("ethers");
|
|
7033
7151
|
var QUANTUM_SECONDS = 60;
|
|
@@ -21400,6 +21518,41 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21400
21518
|
)
|
|
21401
21519
|
}));
|
|
21402
21520
|
}
|
|
21521
|
+
/**
|
|
21522
|
+
* Reconcile every pending withdrawal against BITCOIN truth (incident
|
|
21523
|
+
* 2026-07-22): `getPendingWithdrawals` only reflects on-chain
|
|
21524
|
+
* authorizations, and the contract can never know whether the Phase-2 BTC
|
|
21525
|
+
* broadcast happened. Callers MUST use the returned `status` to decide what
|
|
21526
|
+
* to offer:
|
|
21527
|
+
* - EXECUTABLE → offer Execute (the only status that may).
|
|
21528
|
+
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof.
|
|
21529
|
+
* - SPENT_MISMATCH → unexecutable; offer cancelPendingWithdrawal.
|
|
21530
|
+
* - CORRUPT → authorization contradicts the chain (e.g. declared
|
|
21531
|
+
* satoshis ≠ real output value); offer Cancel &
|
|
21532
|
+
* re-request — Execute can only die at the signer guard.
|
|
21533
|
+
* - UNFUNDED → funding tx unknown/unconfirmed; wait.
|
|
21534
|
+
*
|
|
21535
|
+
* @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
|
|
21536
|
+
* `/v1/proxy/esplora/<network>`). Falls back to
|
|
21537
|
+
* `config.bitcoinProviders[0].url`; throws when neither is configured.
|
|
21538
|
+
*/
|
|
21539
|
+
async reconcilePendingWithdrawals(positionId, opts) {
|
|
21540
|
+
this.ensureInitialized();
|
|
21541
|
+
const esploraBaseUrl = opts?.esploraBaseUrl ?? this.config.bitcoinProviders?.[0]?.url;
|
|
21542
|
+
if (!esploraBaseUrl) {
|
|
21543
|
+
throw new SDKError({
|
|
21544
|
+
code: "SDK_ESPLORA_NOT_CONFIGURED",
|
|
21545
|
+
message: "reconcilePendingWithdrawals requires opts.esploraBaseUrl or config.bitcoinProviders[0].url",
|
|
21546
|
+
category: "CONFIGURATION" /* CONFIGURATION */,
|
|
21547
|
+
severity: "HIGH" /* HIGH */,
|
|
21548
|
+
originalError: new Error("esplora base URL not configured")
|
|
21549
|
+
});
|
|
21550
|
+
}
|
|
21551
|
+
const spends = await this.getPendingWithdrawals(positionId);
|
|
21552
|
+
return Promise.all(
|
|
21553
|
+
spends.map((spend) => reconcileAuthorizedSpend(spend, esploraBaseUrl))
|
|
21554
|
+
);
|
|
21555
|
+
}
|
|
21403
21556
|
/**
|
|
21404
21557
|
* Cancel a pending BTC withdrawal
|
|
21405
21558
|
*
|
|
@@ -23298,6 +23451,7 @@ async function getPositionDelegate(positionId, provider, registryAddress) {
|
|
|
23298
23451
|
createLoanQuery,
|
|
23299
23452
|
createPKPManager,
|
|
23300
23453
|
createWithdrawalAddressModule,
|
|
23454
|
+
defaultHttpGetJson,
|
|
23301
23455
|
failure,
|
|
23302
23456
|
fetchProtocolPauseStatus,
|
|
23303
23457
|
firstSuccess,
|
|
@@ -23321,6 +23475,7 @@ async function getPositionDelegate(positionId, provider, registryAddress) {
|
|
|
23321
23475
|
mapError,
|
|
23322
23476
|
match,
|
|
23323
23477
|
numericToPositionStatus,
|
|
23478
|
+
reconcileAuthorizedSpend,
|
|
23324
23479
|
setPositionDelegate,
|
|
23325
23480
|
success,
|
|
23326
23481
|
toPromise,
|
package/dist/index.mjs
CHANGED
|
@@ -6952,6 +6952,122 @@ async function isLaneGuardActiveOnProvider(provider) {
|
|
|
6952
6952
|
}
|
|
6953
6953
|
}
|
|
6954
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
|
+
|
|
6955
7071
|
// src/utils/btc-withdrawal-message.ts
|
|
6956
7072
|
import { keccak256 as keccak2563, toUtf8Bytes as toUtf8Bytes3, solidityPackedKeccak256 as solidityPackedKeccak2563, getBytes as getBytes3, zeroPadValue } from "ethers";
|
|
6957
7073
|
var QUANTUM_SECONDS = 60;
|
|
@@ -21328,6 +21444,41 @@ Message: ${causeMessage}${quantumContext}${mintDebtDiagnostics}`
|
|
|
21328
21444
|
)
|
|
21329
21445
|
}));
|
|
21330
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
|
+
}
|
|
21331
21482
|
/**
|
|
21332
21483
|
* Cancel a pending BTC withdrawal
|
|
21333
21484
|
*
|
|
@@ -23226,6 +23377,7 @@ export {
|
|
|
23226
23377
|
createPKPManager,
|
|
23227
23378
|
createWithdrawalAddressModule,
|
|
23228
23379
|
DiamondHandsSDK as default,
|
|
23380
|
+
defaultHttpGetJson,
|
|
23229
23381
|
failure,
|
|
23230
23382
|
fetchProtocolPauseStatus,
|
|
23231
23383
|
firstSuccess,
|
|
@@ -23249,6 +23401,7 @@ export {
|
|
|
23249
23401
|
mapError,
|
|
23250
23402
|
match,
|
|
23251
23403
|
numericToPositionStatus,
|
|
23404
|
+
reconcileAuthorizedSpend,
|
|
23252
23405
|
setPositionDelegate,
|
|
23253
23406
|
success,
|
|
23254
23407
|
toPromise,
|
|
@@ -18,6 +18,7 @@ import { SDKError } from "../utils/error-handler";
|
|
|
18
18
|
import type { CreateLoanRequest, CreateLoanResult, LoanDataDetail, UCDMintRequest, UCDMintResult, PartialPaymentRequest, PartialPaymentResult, BTCWithdrawalResult, RenewPositionRequest, RenewPositionResult, LiquidationRequest, LiquidationResult, ConfirmBalanceRequest, ConfirmBalanceResult, TermsWithFeesResult } from "../interfaces/chunks/loan-operations.i";
|
|
19
19
|
import type { DiamondHandsSDKConfig } from "../interfaces/chunks/config.i";
|
|
20
20
|
import type { PKPData } from "../interfaces/chunks/pkp-integration.i";
|
|
21
|
+
import { type ReconciledWithdrawal } from "../utils/withdrawal-reconciliation.utils";
|
|
21
22
|
import { ContractManager } from "./contract/contract-manager.module";
|
|
22
23
|
import { WithdrawalAddressModule } from "./withdrawal-address/withdrawal-address.module";
|
|
23
24
|
import { BitcoinOperations } from "./bitcoin/bitcoin-operations.module";
|
|
@@ -556,6 +557,27 @@ export declare class DiamondHandsSDK {
|
|
|
556
557
|
authorizedAt: number;
|
|
557
558
|
utxoKey: string;
|
|
558
559
|
}>>;
|
|
560
|
+
/**
|
|
561
|
+
* Reconcile every pending withdrawal against BITCOIN truth (incident
|
|
562
|
+
* 2026-07-22): `getPendingWithdrawals` only reflects on-chain
|
|
563
|
+
* authorizations, and the contract can never know whether the Phase-2 BTC
|
|
564
|
+
* broadcast happened. Callers MUST use the returned `status` to decide what
|
|
565
|
+
* to offer:
|
|
566
|
+
* - EXECUTABLE → offer Execute (the only status that may).
|
|
567
|
+
* - EXECUTED → auto-clear; `spendingTxid` is the completion proof.
|
|
568
|
+
* - SPENT_MISMATCH → unexecutable; offer cancelPendingWithdrawal.
|
|
569
|
+
* - CORRUPT → authorization contradicts the chain (e.g. declared
|
|
570
|
+
* satoshis ≠ real output value); offer Cancel &
|
|
571
|
+
* re-request — Execute can only die at the signer guard.
|
|
572
|
+
* - UNFUNDED → funding tx unknown/unconfirmed; wait.
|
|
573
|
+
*
|
|
574
|
+
* @param opts.esploraBaseUrl Esplora API base (e.g. the api proxy
|
|
575
|
+
* `/v1/proxy/esplora/<network>`). Falls back to
|
|
576
|
+
* `config.bitcoinProviders[0].url`; throws when neither is configured.
|
|
577
|
+
*/
|
|
578
|
+
reconcilePendingWithdrawals(positionId: string, opts?: {
|
|
579
|
+
esploraBaseUrl?: string;
|
|
580
|
+
}): Promise<Array<ReconciledWithdrawal<Awaited<ReturnType<DiamondHandsSDK["getPendingWithdrawals"]>>[number]>>>;
|
|
559
581
|
/**
|
|
560
582
|
* Cancel a pending BTC withdrawal
|
|
561
583
|
*
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending-withdrawal reconciliation — classify each on-chain authorized spend
|
|
3
|
+
* (`BTCSpendAuthorizer.getAuthorizedSpends`) against BITCOIN truth before any
|
|
4
|
+
* UI offers "Execute" or any server invokes the TEE signer.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (incident 2026-07-22, position 0x992d5c…): the contract can
|
|
7
|
+
* never know whether a Phase-2 BTC broadcast happened, and a pre-P6/#8
|
|
8
|
+
* authorization could record a `(vout, satoshis)` pair that never matched the
|
|
9
|
+
* chain (declared 96,049 vs on-chain 33,000). Executing such an entry can only
|
|
10
|
+
* die at the signer's parent-fetch guard; and an entry whose outpoint was
|
|
11
|
+
* already spent paying the target is DONE and must auto-clear — while an
|
|
12
|
+
* entry whose funding tx merely confirmed must NOT be cleared as "complete".
|
|
13
|
+
*
|
|
14
|
+
* Statuses:
|
|
15
|
+
* - EXECUTABLE — outpoint confirmed, unspent, values coherent → offer Execute.
|
|
16
|
+
* - EXECUTED — outpoint spent by a tx that pays the authorized target →
|
|
17
|
+
* auto-clear, show `spendingTxid` as the completion proof.
|
|
18
|
+
* - SPENT_MISMATCH — outpoint spent but the spending tx pays the target
|
|
19
|
+
* nothing → unexecutable; surface Cancel.
|
|
20
|
+
* - CORRUPT — authorization contradicts the chain (declared value ≠
|
|
21
|
+
* real output value, targetAmount > real value, or vout
|
|
22
|
+
* out of range) → surface Cancel & re-request; never Execute.
|
|
23
|
+
* - UNFUNDED — funding tx unknown/unconfirmed → wait; no Execute yet.
|
|
24
|
+
*
|
|
25
|
+
* Esplora `spent: true` claims are only trusted when the spending tx can be
|
|
26
|
+
* fetched AND provably includes this outpoint among its inputs — the regtest
|
|
27
|
+
* faucet's esplora answers `spent: true` for arbitrary txids, and a false
|
|
28
|
+
* "executed" here would silently dismiss a withdrawal the user was never paid
|
|
29
|
+
* for. Unverifiable claims classify as EXECUTABLE (chain truth: no proven
|
|
30
|
+
* spend); a genuinely-spent outpoint then simply fails downstream, safely.
|
|
31
|
+
*/
|
|
32
|
+
export type PendingWithdrawalStatus = "EXECUTABLE" | "EXECUTED" | "SPENT_MISMATCH" | "CORRUPT" | "UNFUNDED";
|
|
33
|
+
export interface AuthorizedSpendLike {
|
|
34
|
+
txid: string;
|
|
35
|
+
vout: number;
|
|
36
|
+
satoshis: number;
|
|
37
|
+
targetAddress: string;
|
|
38
|
+
targetAmount: number;
|
|
39
|
+
}
|
|
40
|
+
export interface ReconciledWithdrawal<T extends AuthorizedSpendLike = AuthorizedSpendLike> {
|
|
41
|
+
spend: T;
|
|
42
|
+
status: PendingWithdrawalStatus;
|
|
43
|
+
/** Human-readable, single-sentence explanation of the classification. */
|
|
44
|
+
reason: string;
|
|
45
|
+
/** Real value of the referenced outpoint, when the funding tx is known. */
|
|
46
|
+
onChainOutputValue?: number;
|
|
47
|
+
/** The verified spending tx, for EXECUTED / SPENT_MISMATCH. */
|
|
48
|
+
spendingTxid?: string;
|
|
49
|
+
/** Sats the verified spending tx pays to `targetAddress` (EXECUTED only). */
|
|
50
|
+
paidToTargetSats?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* HTTP seam: GET `url`, resolve `{ status, body }` (body null on non-JSON).
|
|
54
|
+
* Injectable for tests; the default uses global `fetch` (node ≥18 + browsers).
|
|
55
|
+
* Transport failures throw — reconciliation must fail LOUD, never classify on
|
|
56
|
+
* missing data.
|
|
57
|
+
*/
|
|
58
|
+
export type HttpGetJson = (url: string) => Promise<{
|
|
59
|
+
status: number;
|
|
60
|
+
body: unknown;
|
|
61
|
+
}>;
|
|
62
|
+
export declare const defaultHttpGetJson: HttpGetJson;
|
|
63
|
+
/** Classify one authorized spend against the esplora at `esploraBaseUrl`. */
|
|
64
|
+
export declare function reconcileAuthorizedSpend<T extends AuthorizedSpendLike>(spend: T, esploraBaseUrl: string, httpGetJson?: HttpGetJson): Promise<ReconciledWithdrawal<T>>;
|
package/package.json
CHANGED