@arkade-os/sdk 0.4.34 → 0.4.35
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/dist/adapters/expo.cjs +4 -4
- package/dist/adapters/expo.d.cts +2 -2
- package/dist/adapters/expo.d.ts +2 -2
- package/dist/adapters/expo.js +2 -2
- package/dist/adapters/indexedDB.cjs +3 -3
- package/dist/adapters/indexedDB.js +2 -2
- package/dist/{ark-Dsv5Jq4E.d.cts → ark-D6sau_6-.d.cts} +458 -3
- package/dist/{ark-Dsv5Jq4E.d.ts → ark-D6sau_6-.d.ts} +458 -3
- package/dist/{asyncStorageTaskQueue-D92ch8yI.d.cts → asyncStorageTaskQueue-CpC027t_.d.cts} +2 -2
- package/dist/{asyncStorageTaskQueue-BH-zuth5.d.ts → asyncStorageTaskQueue-GT8fmPUG.d.ts} +2 -2
- package/dist/{chunk-FXFBPXV3.js → chunk-3JR77WQ4.js} +139 -41
- package/dist/chunk-3JR77WQ4.js.map +1 -0
- package/dist/{chunk-ZS3OZHC7.cjs → chunk-FM7T7JVL.cjs} +7 -7
- package/dist/{chunk-ZS3OZHC7.cjs.map → chunk-FM7T7JVL.cjs.map} +1 -1
- package/dist/{chunk-XCHBQVMK.cjs → chunk-H2LX2KKY.cjs} +1540 -265
- package/dist/chunk-H2LX2KKY.cjs.map +1 -0
- package/dist/{chunk-HFXEUW55.js → chunk-NOR7XOKN.js} +1472 -202
- package/dist/chunk-NOR7XOKN.js.map +1 -0
- package/dist/{chunk-VVGD3JIP.js → chunk-OURFR4UR.js} +3 -3
- package/dist/{chunk-VVGD3JIP.js.map → chunk-OURFR4UR.js.map} +1 -1
- package/dist/{chunk-CCLNFHJ5.cjs → chunk-VYS3KGRI.cjs} +16 -10
- package/dist/chunk-VYS3KGRI.cjs.map +1 -0
- package/dist/{chunk-FSAXPBGP.cjs → chunk-X2EQLK4O.cjs} +143 -40
- package/dist/chunk-X2EQLK4O.cjs.map +1 -0
- package/dist/{chunk-5WDBHWX3.js → chunk-XQS2HW4Q.js} +10 -4
- package/dist/chunk-XQS2HW4Q.js.map +1 -0
- package/dist/contracts/handlers/index.d.cts +3 -3
- package/dist/contracts/handlers/index.d.ts +3 -3
- package/dist/{delegate-BaS5SCIW.d.cts → delegate-C-L6gSZx.d.cts} +1 -1
- package/dist/{delegate-Baz_hb83.d.ts → delegate-De5__fpZ.d.ts} +1 -1
- package/dist/{index-FwXZveaX.d.ts → index-BETdjE_o.d.ts} +2 -2
- package/dist/{index-lNZ6qaO3.d.cts → index-jwQfHP6D.d.cts} +2 -2
- package/dist/index.cjs +129 -105
- package/dist/index.d.cts +69 -9
- package/dist/index.d.ts +69 -9
- package/dist/index.js +2 -2
- package/dist/repositories/realm/index.cjs +12 -12
- package/dist/repositories/realm/index.cjs.map +1 -1
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/realm/index.js +3 -3
- package/dist/repositories/realm/index.js.map +1 -1
- package/dist/repositories/sqlite/index.cjs +11 -11
- package/dist/repositories/sqlite/index.d.cts +1 -1
- package/dist/repositories/sqlite/index.d.ts +1 -1
- package/dist/repositories/sqlite/index.js +2 -2
- package/dist/{taskRunner-vFRA3F9b.d.cts → taskRunner-DCyp6Gea.d.cts} +2 -2
- package/dist/{taskRunner-B1NUWyWR.d.ts → taskRunner-DnxtObeq.d.ts} +2 -2
- package/dist/wallet/expo/background.cjs +12 -12
- package/dist/wallet/expo/background.d.cts +3 -3
- package/dist/wallet/expo/background.d.ts +3 -3
- package/dist/wallet/expo/background.js +4 -4
- package/dist/wallet/expo/index.cjs +11 -11
- package/dist/wallet/expo/index.d.cts +4 -4
- package/dist/wallet/expo/index.d.ts +4 -4
- package/dist/wallet/expo/index.js +3 -3
- package/dist/{wallet-D6uoBLmS.d.ts → wallet-BWHbd5b1.d.cts} +231 -8
- package/dist/{wallet-By9HIo0Q.d.cts → wallet-Bth5uucA.d.ts} +231 -8
- package/dist/worker/expo/index.cjs +7 -7
- package/dist/worker/expo/index.d.cts +4 -4
- package/dist/worker/expo/index.d.ts +4 -4
- package/dist/worker/expo/index.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-5WDBHWX3.js.map +0 -1
- package/dist/chunk-CCLNFHJ5.cjs.map +0 -1
- package/dist/chunk-FSAXPBGP.cjs.map +0 -1
- package/dist/chunk-FXFBPXV3.js.map +0 -1
- package/dist/chunk-HFXEUW55.js.map +0 -1
- package/dist/chunk-XCHBQVMK.cjs.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { craftToSpendTx, Transaction, OP_RETURN_EMPTY_PKSCRIPT, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-
|
|
1
|
+
import { craftToSpendTx, Transaction, OP_RETURN_EMPTY_PKSCRIPT, baseFetch, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-3JR77WQ4.js';
|
|
2
2
|
import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-CUSABEUQ.js';
|
|
3
3
|
import { VtxoScript, timelockToSequence, DEFAULT_NETWORK, DEFAULT_NETWORK_NAME, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, ArkAddress, CSVMultisigTapscript, getSequence, getNetwork, MultisigTapscript, networks as networks$1, DEFAULT_ARKADE_SERVER_URL } from './chunk-OUVTG72A.js';
|
|
4
4
|
import { sha256, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr, equalBytes as equalBytes$1 } from '@scure/btc-signer/utils.js';
|
|
@@ -958,7 +958,7 @@ var RestDelegateProvider = class {
|
|
|
958
958
|
*/
|
|
959
959
|
async delegate(intent, forfeitTxs, options) {
|
|
960
960
|
const url = `${this.url}/v1/delegate`;
|
|
961
|
-
const response = await
|
|
961
|
+
const response = await baseFetch(url, {
|
|
962
962
|
method: "POST",
|
|
963
963
|
headers: {
|
|
964
964
|
"Content-Type": "application/json"
|
|
@@ -973,8 +973,8 @@ var RestDelegateProvider = class {
|
|
|
973
973
|
})
|
|
974
974
|
});
|
|
975
975
|
if (!response.ok) {
|
|
976
|
-
const
|
|
977
|
-
throw new Error(`Failed to delegate: ${
|
|
976
|
+
const errorText2 = await response.text();
|
|
977
|
+
throw new Error(`Failed to delegate: ${errorText2}`);
|
|
978
978
|
}
|
|
979
979
|
}
|
|
980
980
|
/**
|
|
@@ -985,10 +985,10 @@ var RestDelegateProvider = class {
|
|
|
985
985
|
*/
|
|
986
986
|
async getDelegateInfo() {
|
|
987
987
|
const url = `${this.url}/v1/delegator/info`;
|
|
988
|
-
const response = await
|
|
988
|
+
const response = await baseFetch(url);
|
|
989
989
|
if (!response.ok) {
|
|
990
|
-
const
|
|
991
|
-
throw new Error(`Failed to get delegate info: ${
|
|
990
|
+
const errorText2 = await response.text();
|
|
991
|
+
throw new Error(`Failed to get delegate info: ${errorText2}`);
|
|
992
992
|
}
|
|
993
993
|
const data = await response.json();
|
|
994
994
|
if (!isDelegateInfo(data)) {
|
|
@@ -1020,14 +1020,14 @@ var EsploraProvider = class {
|
|
|
1020
1020
|
pollingInterval;
|
|
1021
1021
|
forcePolling;
|
|
1022
1022
|
async getCoins(address) {
|
|
1023
|
-
const response = await
|
|
1023
|
+
const response = await baseFetch(`${this.baseUrl}/address/${address}/utxo`);
|
|
1024
1024
|
if (!response.ok) {
|
|
1025
1025
|
throw new Error(`Failed to fetch UTXOs: ${response.statusText}`);
|
|
1026
1026
|
}
|
|
1027
1027
|
return response.json();
|
|
1028
1028
|
}
|
|
1029
1029
|
async getFeeRate() {
|
|
1030
|
-
const response = await
|
|
1030
|
+
const response = await baseFetch(`${this.baseUrl}/fee-estimates`);
|
|
1031
1031
|
if (response.status === 404) {
|
|
1032
1032
|
return void 0;
|
|
1033
1033
|
}
|
|
@@ -1048,7 +1048,7 @@ var EsploraProvider = class {
|
|
|
1048
1048
|
}
|
|
1049
1049
|
}
|
|
1050
1050
|
async getTxOutspends(txid) {
|
|
1051
|
-
const response = await
|
|
1051
|
+
const response = await baseFetch(`${this.baseUrl}/tx/${txid}/outspends`);
|
|
1052
1052
|
if (!response.ok) {
|
|
1053
1053
|
const error = await response.text();
|
|
1054
1054
|
throw new Error(`Failed to get transaction outspends: ${error}`);
|
|
@@ -1056,7 +1056,7 @@ var EsploraProvider = class {
|
|
|
1056
1056
|
return response.json();
|
|
1057
1057
|
}
|
|
1058
1058
|
async getTransactions(address) {
|
|
1059
|
-
const response = await
|
|
1059
|
+
const response = await baseFetch(`${this.baseUrl}/address/${address}/txs`);
|
|
1060
1060
|
if (!response.ok) {
|
|
1061
1061
|
const error = await response.text();
|
|
1062
1062
|
throw new Error(`Failed to get transactions: ${error}`);
|
|
@@ -1064,7 +1064,7 @@ var EsploraProvider = class {
|
|
|
1064
1064
|
return response.json();
|
|
1065
1065
|
}
|
|
1066
1066
|
async getTxStatus(txid) {
|
|
1067
|
-
const txresponse = await
|
|
1067
|
+
const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
|
|
1068
1068
|
if (!txresponse.ok) {
|
|
1069
1069
|
throw new Error(txresponse.statusText);
|
|
1070
1070
|
}
|
|
@@ -1072,7 +1072,7 @@ var EsploraProvider = class {
|
|
|
1072
1072
|
if (!tx.status.confirmed) {
|
|
1073
1073
|
return { confirmed: false };
|
|
1074
1074
|
}
|
|
1075
|
-
const response = await
|
|
1075
|
+
const response = await baseFetch(`${this.baseUrl}/tx/${txid}/status`);
|
|
1076
1076
|
if (!response.ok) {
|
|
1077
1077
|
throw new Error(`Failed to get transaction status: ${response.statusText}`);
|
|
1078
1078
|
}
|
|
@@ -1156,7 +1156,7 @@ var EsploraProvider = class {
|
|
|
1156
1156
|
return stopFunc;
|
|
1157
1157
|
}
|
|
1158
1158
|
async getChainTip() {
|
|
1159
|
-
const tipBlocks = await
|
|
1159
|
+
const tipBlocks = await baseFetch(`${this.baseUrl}/blocks`);
|
|
1160
1160
|
if (!tipBlocks.ok) {
|
|
1161
1161
|
throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
|
|
1162
1162
|
}
|
|
@@ -1175,7 +1175,7 @@ var EsploraProvider = class {
|
|
|
1175
1175
|
};
|
|
1176
1176
|
}
|
|
1177
1177
|
async broadcastPackage(parent, child) {
|
|
1178
|
-
const response = await
|
|
1178
|
+
const response = await baseFetch(`${this.baseUrl}/txs/package`, {
|
|
1179
1179
|
method: "POST",
|
|
1180
1180
|
headers: {
|
|
1181
1181
|
"Content-Type": "application/json"
|
|
@@ -1189,7 +1189,7 @@ var EsploraProvider = class {
|
|
|
1189
1189
|
return response.json();
|
|
1190
1190
|
}
|
|
1191
1191
|
async broadcastTx(tx) {
|
|
1192
|
-
const response = await
|
|
1192
|
+
const response = await baseFetch(`${this.baseUrl}/tx`, {
|
|
1193
1193
|
method: "POST",
|
|
1194
1194
|
headers: {
|
|
1195
1195
|
"Content-Type": "text/plain"
|
|
@@ -1779,6 +1779,45 @@ function selectedCoinsToAssetInputs(selectedCoins) {
|
|
|
1779
1779
|
}
|
|
1780
1780
|
return assetInputs;
|
|
1781
1781
|
}
|
|
1782
|
+
function toXOnlySignerHex(pubkeyHex) {
|
|
1783
|
+
const bytes = hex.decode(pubkeyHex);
|
|
1784
|
+
if (bytes.length === 33) return hex.encode(bytes.slice(1));
|
|
1785
|
+
if (bytes.length === 32) return hex.encode(bytes);
|
|
1786
|
+
throw new Error(`invalid signer pubkey length: expected 32 or 33 bytes, got ${bytes.length}`);
|
|
1787
|
+
}
|
|
1788
|
+
function signerSetFromInfo(info) {
|
|
1789
|
+
const active = toXOnlySignerHex(info.signerPubkey);
|
|
1790
|
+
const deprecated = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const signer of info.deprecatedSigners) {
|
|
1792
|
+
if (!signer.pubkey) continue;
|
|
1793
|
+
deprecated.set(toXOnlySignerHex(signer.pubkey), signer.cutoffDate);
|
|
1794
|
+
}
|
|
1795
|
+
return { active, deprecated };
|
|
1796
|
+
}
|
|
1797
|
+
function classifyAgainstSignerSet(contractServerPubKeyHex, signerSet, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
1798
|
+
const signerPubKey = toXOnlySignerHex(contractServerPubKeyHex);
|
|
1799
|
+
if (signerPubKey === signerSet.active) {
|
|
1800
|
+
return { status: "CURRENT", signerPubKey };
|
|
1801
|
+
}
|
|
1802
|
+
if (!signerSet.deprecated.has(signerPubKey)) {
|
|
1803
|
+
return { status: "UNKNOWN_SIGNER", signerPubKey };
|
|
1804
|
+
}
|
|
1805
|
+
const cutoffDate = signerSet.deprecated.get(signerPubKey);
|
|
1806
|
+
if (cutoffDate === 0n) {
|
|
1807
|
+
return { status: "DUE_NOW", signerPubKey };
|
|
1808
|
+
}
|
|
1809
|
+
const secondsUntilCutoff = Number(cutoffDate) - nowSeconds;
|
|
1810
|
+
if (secondsUntilCutoff <= 0) {
|
|
1811
|
+
return { status: "EXPIRED", signerPubKey, cutoffDate, secondsUntilCutoff };
|
|
1812
|
+
}
|
|
1813
|
+
return { status: "MIGRATABLE", signerPubKey, cutoffDate, secondsUntilCutoff };
|
|
1814
|
+
}
|
|
1815
|
+
function classifyContractSigner(contractServerPubKeyHex, info, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
1816
|
+
return classifyAgainstSignerSet(contractServerPubKeyHex, signerSetFromInfo(info), nowSeconds);
|
|
1817
|
+
}
|
|
1818
|
+
function isCooperativelyMigratable(status) {
|
|
1819
|
+
return status === "MIGRATABLE" || status === "DUE_NOW";
|
|
1820
|
+
}
|
|
1782
1821
|
function buildOffchainTx(inputs, outputs, serverUnrollScript) {
|
|
1783
1822
|
const MAX_OP_RETURN = 2;
|
|
1784
1823
|
let countOpReturn = 0;
|
|
@@ -2359,6 +2398,20 @@ function validateRecipients(recipients, dustAmount) {
|
|
|
2359
2398
|
}
|
|
2360
2399
|
|
|
2361
2400
|
// src/wallet/vtxo-manager.ts
|
|
2401
|
+
function selectPendingRecoveryOutpoints(contractsWithVtxos, signerSet, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
2402
|
+
const out = /* @__PURE__ */ new Set();
|
|
2403
|
+
for (const { contract, vtxos } of contractsWithVtxos) {
|
|
2404
|
+
const serverPubKey = contract.params.serverPubKey;
|
|
2405
|
+
if (!serverPubKey) continue;
|
|
2406
|
+
if (classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds).status !== "EXPIRED") {
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
for (const v of vtxos) {
|
|
2410
|
+
if (isSpendable(v) && !isRecoverable(v)) out.add(`${v.txid}:${v.vout}`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
return out;
|
|
2414
|
+
}
|
|
2362
2415
|
function isSweepCapable(wallet) {
|
|
2363
2416
|
return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet && "signOnchainBoardingTx" in wallet;
|
|
2364
2417
|
}
|
|
@@ -2396,6 +2449,18 @@ function byExpiryAscending(vtxos) {
|
|
|
2396
2449
|
};
|
|
2397
2450
|
return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
|
|
2398
2451
|
}
|
|
2452
|
+
function capSettlementBatch(sorted, maxAmount) {
|
|
2453
|
+
const batch = [];
|
|
2454
|
+
let total = 0n;
|
|
2455
|
+
for (const vtxo of sorted) {
|
|
2456
|
+
if (batch.length >= MAX_VTXOS_PER_SETTLEMENT) break;
|
|
2457
|
+
const next = total + BigInt(vtxo.value);
|
|
2458
|
+
if (maxAmount >= 0n && next > maxAmount) continue;
|
|
2459
|
+
batch.push(vtxo);
|
|
2460
|
+
total = next;
|
|
2461
|
+
}
|
|
2462
|
+
return batch;
|
|
2463
|
+
}
|
|
2399
2464
|
var DEFAULT_THRESHOLD_SECONDS = 259200;
|
|
2400
2465
|
var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
|
|
2401
2466
|
var DEFAULT_RENEWAL_CONFIG = {
|
|
@@ -2405,7 +2470,8 @@ var DEFAULT_RENEWAL_CONFIG = {
|
|
|
2405
2470
|
var DEFAULT_SETTLEMENT_CONFIG = {
|
|
2406
2471
|
vtxoThreshold: DEFAULT_THRESHOLD_SECONDS,
|
|
2407
2472
|
boardingUtxoSweep: true,
|
|
2408
|
-
pollIntervalMs: 6e4
|
|
2473
|
+
pollIntervalMs: 6e4,
|
|
2474
|
+
deprecatedSignerMigration: true
|
|
2409
2475
|
};
|
|
2410
2476
|
function getRecoverableVtxos(vtxos, dustAmount) {
|
|
2411
2477
|
return vtxos.filter((vtxo) => {
|
|
@@ -2459,6 +2525,51 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
|
2459
2525
|
(vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || isRecoverable(vtxo) || isSpendable(vtxo) && isExpired(vtxo) || isSubdust(vtxo, dustAmount)
|
|
2460
2526
|
);
|
|
2461
2527
|
}
|
|
2528
|
+
function isMigrationCapable(wallet) {
|
|
2529
|
+
return "arkProvider" in wallet && "arkServerPublicKey" in wallet && "onchainProvider" in wallet && typeof wallet.rotateServerSigner === "function" && typeof wallet.sendSelectedVtxosToSelf === "function" && typeof wallet.getBoardingUtxosForSigners === "function";
|
|
2530
|
+
}
|
|
2531
|
+
function classifiedToRef(c) {
|
|
2532
|
+
return {
|
|
2533
|
+
txid: c.vtxo.txid,
|
|
2534
|
+
vout: c.vtxo.vout,
|
|
2535
|
+
value: c.vtxo.value,
|
|
2536
|
+
signerPubKey: c.classification.signerPubKey,
|
|
2537
|
+
cutoffDate: c.classification.cutoffDate
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
function classifiedBoardingToRef(c) {
|
|
2541
|
+
return {
|
|
2542
|
+
txid: c.coin.txid,
|
|
2543
|
+
vout: c.coin.vout,
|
|
2544
|
+
value: c.coin.value,
|
|
2545
|
+
signerPubKey: c.classification.signerPubKey,
|
|
2546
|
+
cutoffDate: c.classification.cutoffDate
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function mergeSignerReports(...reportLists) {
|
|
2550
|
+
const bySigner = /* @__PURE__ */ new Map();
|
|
2551
|
+
for (const list of reportLists) {
|
|
2552
|
+
for (const r of list) {
|
|
2553
|
+
const existing = bySigner.get(r.signerPubKey);
|
|
2554
|
+
if (existing) {
|
|
2555
|
+
existing.vtxoCount += r.vtxoCount;
|
|
2556
|
+
existing.totalValue += r.totalValue;
|
|
2557
|
+
existing.boardingCount += r.boardingCount;
|
|
2558
|
+
existing.boardingValue += r.boardingValue;
|
|
2559
|
+
existing.recoverableCount += r.recoverableCount;
|
|
2560
|
+
existing.recoverableValue += r.recoverableValue;
|
|
2561
|
+
existing.awaitingSweepCount += r.awaitingSweepCount;
|
|
2562
|
+
existing.awaitingSweepValue += r.awaitingSweepValue;
|
|
2563
|
+
if (r.nextSweepEta !== void 0) {
|
|
2564
|
+
existing.nextSweepEta = existing.nextSweepEta === void 0 ? r.nextSweepEta : Math.min(existing.nextSweepEta, r.nextSweepEta);
|
|
2565
|
+
}
|
|
2566
|
+
} else {
|
|
2567
|
+
bySigner.set(r.signerPubKey, { ...r });
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return Array.from(bySigner.values());
|
|
2572
|
+
}
|
|
2462
2573
|
var VtxoManager = class _VtxoManager {
|
|
2463
2574
|
constructor(wallet, renewalConfig, settlementConfig) {
|
|
2464
2575
|
this.wallet = wallet;
|
|
@@ -2474,15 +2585,9 @@ var VtxoManager = class _VtxoManager {
|
|
|
2474
2585
|
} else {
|
|
2475
2586
|
this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
|
|
2476
2587
|
}
|
|
2477
|
-
this.contractEventsSubscriptionReady = this.initializeSubscription()
|
|
2478
|
-
(subscription) => {
|
|
2479
|
-
this.contractEventsSubscription = subscription;
|
|
2480
|
-
return subscription;
|
|
2481
|
-
}
|
|
2482
|
-
);
|
|
2588
|
+
this.contractEventsSubscriptionReady = this.initializeSubscription();
|
|
2483
2589
|
}
|
|
2484
2590
|
settlementConfig;
|
|
2485
|
-
contractEventsSubscription;
|
|
2486
2591
|
contractEventsSubscriptionReady;
|
|
2487
2592
|
disposePromise;
|
|
2488
2593
|
pollTimeoutId;
|
|
@@ -2519,6 +2624,15 @@ var VtxoManager = class _VtxoManager {
|
|
|
2519
2624
|
lastVtxoSpentRefreshTimestamp = 0;
|
|
2520
2625
|
vtxoSpentRefreshPromise;
|
|
2521
2626
|
static VTXO_SPENT_REFRESH_COOLDOWN_MS = 3e4;
|
|
2627
|
+
// Cooldown/backoff for the automatic deprecated-signer migration pass.
|
|
2628
|
+
// Mirrors the periodic-settle machinery so a server-side migration failure
|
|
2629
|
+
// (e.g. arkd not yet accepting old-key inputs, or a closed cutoff window)
|
|
2630
|
+
// backs off exponentially instead of re-submitting an identical intent on
|
|
2631
|
+
// every poll. The manual migrateDeprecatedSignerVtxos() bypasses this.
|
|
2632
|
+
lastMigrationTimestamp = 0;
|
|
2633
|
+
consecutiveMigrationFailures = 0;
|
|
2634
|
+
static MIGRATION_COOLDOWN_MS = 3e4;
|
|
2635
|
+
static MIGRATION_MAX_BACKOFF_MS = 5 * 60 * 1e3;
|
|
2522
2636
|
// ========== Recovery Methods ==========
|
|
2523
2637
|
/**
|
|
2524
2638
|
* Recover swept/expired virtual outputs by settling them back to the wallet's Arkade address.
|
|
@@ -2559,16 +2673,21 @@ var VtxoManager = class _VtxoManager {
|
|
|
2559
2673
|
if (vtxosToRecover.length === 0) {
|
|
2560
2674
|
throw new Error("No recoverable VTXOs found");
|
|
2561
2675
|
}
|
|
2562
|
-
|
|
2676
|
+
const info = await this.getInfoProvider()?.getInfo();
|
|
2677
|
+
const vtxoMaxAmount = info?.vtxoMaxAmount ?? -1n;
|
|
2678
|
+
const capped = capSettlementBatch(byValueDescending(vtxosToRecover), vtxoMaxAmount);
|
|
2679
|
+
if (capped.length < vtxosToRecover.length) {
|
|
2563
2680
|
const recoverableCount = vtxosToRecover.length;
|
|
2564
|
-
const capped = byValueDescending(vtxosToRecover).slice(0, MAX_VTXOS_PER_SETTLEMENT);
|
|
2565
2681
|
({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
|
|
2566
2682
|
if (vtxosToRecover.length === 0) {
|
|
2567
2683
|
throw new Error(
|
|
2568
|
-
`Capped recovery batch (highest-value
|
|
2684
|
+
`Capped recovery batch (highest-value subset of ${recoverableCount} recoverable VTXOs within the ${MAX_VTXOS_PER_SETTLEMENT}-input and ${vtxoMaxAmount}-sat limits) is below the dust threshold ${dustAmount}`
|
|
2569
2685
|
);
|
|
2570
2686
|
}
|
|
2571
2687
|
}
|
|
2688
|
+
if (info && isMigrationCapable(this.wallet)) {
|
|
2689
|
+
await this.rotateForRecoverableInputs(vtxosToRecover, info);
|
|
2690
|
+
}
|
|
2572
2691
|
const arkAddress = await this.wallet.getAddress();
|
|
2573
2692
|
return this.wallet.settle(
|
|
2574
2693
|
{
|
|
@@ -2718,8 +2837,24 @@ var VtxoManager = class _VtxoManager {
|
|
|
2718
2837
|
if (vtxos.length === 0) {
|
|
2719
2838
|
throw new Error("No VTXOs available to renew");
|
|
2720
2839
|
}
|
|
2721
|
-
|
|
2722
|
-
|
|
2840
|
+
const info = await this.getInfoProvider()?.getInfo();
|
|
2841
|
+
const vtxoMaxAmount = info?.vtxoMaxAmount ?? -1n;
|
|
2842
|
+
const capped = capSettlementBatch(byExpiryAscending(vtxos), vtxoMaxAmount);
|
|
2843
|
+
if (vtxoMaxAmount >= 0n) {
|
|
2844
|
+
const oversized = vtxos.filter((vtxo) => BigInt(vtxo.value) > vtxoMaxAmount);
|
|
2845
|
+
if (oversized.length > 0) {
|
|
2846
|
+
console.warn(
|
|
2847
|
+
`Renewal: ${oversized.length} VTXO(s) exceed the per-output limit ${vtxoMaxAmount} and cannot be renewed; they risk unilateral exit`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (capped.length < vtxos.length) {
|
|
2852
|
+
vtxos = capped;
|
|
2853
|
+
if (vtxos.length === 0) {
|
|
2854
|
+
throw new Error(
|
|
2855
|
+
`No VTXOs available to renew within the per-output limit ${vtxoMaxAmount}`
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2723
2858
|
}
|
|
2724
2859
|
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
2725
2860
|
const dustAmount = getDustAmount(this.wallet);
|
|
@@ -2728,6 +2863,9 @@ var VtxoManager = class _VtxoManager {
|
|
|
2728
2863
|
`Total amount ${totalAmount} is below dust threshold ${dustAmount}`
|
|
2729
2864
|
);
|
|
2730
2865
|
}
|
|
2866
|
+
if (info && isMigrationCapable(this.wallet)) {
|
|
2867
|
+
await this.rotateForRecoverableInputs(vtxos, info);
|
|
2868
|
+
}
|
|
2731
2869
|
const arkAddress = await this.wallet.getAddress();
|
|
2732
2870
|
const txid = await this.wallet.settle(
|
|
2733
2871
|
{
|
|
@@ -2878,6 +3016,472 @@ var VtxoManager = class _VtxoManager {
|
|
|
2878
3016
|
this.knownBoardingUtxos.add(`${txid}:0`);
|
|
2879
3017
|
return txid;
|
|
2880
3018
|
}
|
|
3019
|
+
// ========== Deprecated-Signer Migration Methods ==========
|
|
3020
|
+
/**
|
|
3021
|
+
* Cooperatively migrate VTXOs minted under a now-deprecated server signer
|
|
3022
|
+
* to the wallet's active-signer address. See {@link IVtxoManager}.
|
|
3023
|
+
*/
|
|
3024
|
+
async migrateDeprecatedSignerVtxos(options) {
|
|
3025
|
+
return this.migrateCore(options);
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Machine-readable status of every deprecated server signer the wallet
|
|
3029
|
+
* currently holds funds under (Section 6), without migrating. Covers both
|
|
3030
|
+
* VTXO and boarding holdings (Section 7), merged per signer.
|
|
3031
|
+
*
|
|
3032
|
+
* @remarks This is no longer a pure repository/info read: surfacing boarding
|
|
3033
|
+
* holdings fans out per boarding address (`getCoins` round trips) and
|
|
3034
|
+
* refreshes the UTXO cache via `saveUtxos`.
|
|
3035
|
+
*/
|
|
3036
|
+
async getDeprecatedSignerStatus() {
|
|
3037
|
+
const wallet = this.requireMigrationCapableWallet();
|
|
3038
|
+
const info = await wallet.arkProvider.getInfo();
|
|
3039
|
+
const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
|
|
3040
|
+
const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
|
|
3041
|
+
return mergeSignerReports(vtxoReports, boardingReports);
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Core migration routine shared by the manual API and the automatic poll
|
|
3045
|
+
* pass. Fetches a fresh {@link ArkInfo}, applies a mid-session signer
|
|
3046
|
+
* rotation when the wallet's own snapshot signer has been deprecated,
|
|
3047
|
+
* selects spendable VTXOs under deprecated-signer contracts (cutoff-first),
|
|
3048
|
+
* and settles them to the active-signer Ark address.
|
|
3049
|
+
*/
|
|
3050
|
+
async migrateCore(options) {
|
|
3051
|
+
const wallet = this.requireMigrationCapableWallet();
|
|
3052
|
+
const info = await wallet.arkProvider.getInfo();
|
|
3053
|
+
const signerSet = signerSetFromInfo(info);
|
|
3054
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3055
|
+
const walletSignerHex = hex.encode(wallet.arkServerPublicKey);
|
|
3056
|
+
const walletClass = classifyAgainstSignerSet(walletSignerHex, signerSet, nowSeconds);
|
|
3057
|
+
if (signerSet.deprecated.size === 0 && walletClass.status === "CURRENT") {
|
|
3058
|
+
return { rotated: false, expired: [], signers: [] };
|
|
3059
|
+
}
|
|
3060
|
+
if (walletClass.status === "UNKNOWN_SIGNER") {
|
|
3061
|
+
const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
|
|
3062
|
+
const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
|
|
3063
|
+
return {
|
|
3064
|
+
rotated: false,
|
|
3065
|
+
expired: [],
|
|
3066
|
+
signers: mergeSignerReports(vtxoReports2, boardingReports2),
|
|
3067
|
+
skipped: "unknown-wallet-signer"
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
const rotated = await this.ensureReceiveOnActiveSigner(info);
|
|
3071
|
+
const {
|
|
3072
|
+
reports: vtxoReports,
|
|
3073
|
+
migratable: vtxoMigratable,
|
|
3074
|
+
expired: vtxoExpired
|
|
3075
|
+
} = await this.classifyDeprecatedSignerContracts(info);
|
|
3076
|
+
const {
|
|
3077
|
+
reports: boardingReports,
|
|
3078
|
+
migratable: boardingMigratable,
|
|
3079
|
+
expired: boardingExpired
|
|
3080
|
+
} = await this.classifyDeprecatedSignerBoarding(info);
|
|
3081
|
+
const reports = mergeSignerReports(vtxoReports, boardingReports);
|
|
3082
|
+
const expiredRefs = [
|
|
3083
|
+
...vtxoExpired.map(classifiedToRef),
|
|
3084
|
+
...boardingExpired.map(classifiedBoardingToRef)
|
|
3085
|
+
];
|
|
3086
|
+
if (vtxoMigratable.length === 0 && boardingMigratable.length === 0) {
|
|
3087
|
+
return {
|
|
3088
|
+
rotated,
|
|
3089
|
+
expired: expiredRefs,
|
|
3090
|
+
signers: reports,
|
|
3091
|
+
skipped: "no-deprecated-vtxos"
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
const vtxoMaxAmount = info.vtxoMaxAmount;
|
|
3095
|
+
const dustAmount = getDustAmount(this.wallet);
|
|
3096
|
+
const report = {
|
|
3097
|
+
rotated,
|
|
3098
|
+
expired: expiredRefs,
|
|
3099
|
+
signers: reports
|
|
3100
|
+
};
|
|
3101
|
+
if (vtxoMigratable.length > 0) {
|
|
3102
|
+
report.vtxos = await this.runMigrationLeg(
|
|
3103
|
+
vtxoMigratable,
|
|
3104
|
+
(c) => c.vtxo.value,
|
|
3105
|
+
classifiedToRef,
|
|
3106
|
+
vtxoMaxAmount,
|
|
3107
|
+
dustAmount,
|
|
3108
|
+
"VTXO",
|
|
3109
|
+
(capped) => wallet.sendSelectedVtxosToSelf(capped.map((c) => c.vtxo))
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
if (boardingMigratable.length > 0) {
|
|
3113
|
+
report.boarding = await this.runMigrationLeg(
|
|
3114
|
+
boardingMigratable,
|
|
3115
|
+
(c) => c.coin.value,
|
|
3116
|
+
classifiedBoardingToRef,
|
|
3117
|
+
vtxoMaxAmount,
|
|
3118
|
+
dustAmount,
|
|
3119
|
+
"boarding",
|
|
3120
|
+
async (capped) => {
|
|
3121
|
+
const arkAddress = await this.wallet.getAddress();
|
|
3122
|
+
const totalAmount = capped.reduce((sum, c) => sum + BigInt(c.coin.value), 0n);
|
|
3123
|
+
return this.wallet.settle(
|
|
3124
|
+
{
|
|
3125
|
+
inputs: capped.map((c) => c.coin),
|
|
3126
|
+
outputs: [{ address: arkAddress, amount: totalAmount }]
|
|
3127
|
+
},
|
|
3128
|
+
options?.eventCallback
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
return report;
|
|
3134
|
+
}
|
|
3135
|
+
/**
|
|
3136
|
+
* Size and submit one migration leg. Filters inputs whose value alone
|
|
3137
|
+
* exceeds the per-output ceiling (`vtxoMaxAmount`; `< 0` means no limit) —
|
|
3138
|
+
* those can never form a ≤-ceiling output and must exit unilaterally — then
|
|
3139
|
+
* caps the rest (highest-value first; bounded by {@link MAX_VTXOS_PER_SETTLEMENT}
|
|
3140
|
+
* AND a gross total within `vtxoMaxAmount`), applies the protocol dust floor,
|
|
3141
|
+
* and submits the capped batch through `submit`. A throw from `submit` lands
|
|
3142
|
+
* in `error`; the caller's other leg still runs.
|
|
3143
|
+
*
|
|
3144
|
+
* Migration is mandatory and fee-exempt: every selected input moves at its
|
|
3145
|
+
* full value, so the gross total IS the aggregated output amount (kept under
|
|
3146
|
+
* the server ceiling by the cap). The dust floor guards the degenerate cases
|
|
3147
|
+
* where every input was oversized or the whole holding sums below dust.
|
|
3148
|
+
*/
|
|
3149
|
+
async runMigrationLeg(candidates, valueOf, toRef, vtxoMaxAmount, dustAmount, legName, submit) {
|
|
3150
|
+
const oversizedRefs = [];
|
|
3151
|
+
const sized = [];
|
|
3152
|
+
for (const c of candidates) {
|
|
3153
|
+
if (vtxoMaxAmount >= 0n && BigInt(valueOf(c)) > vtxoMaxAmount) {
|
|
3154
|
+
oversizedRefs.push(toRef(c));
|
|
3155
|
+
} else {
|
|
3156
|
+
sized.push(c);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (oversizedRefs.length > 0) {
|
|
3160
|
+
console.warn(
|
|
3161
|
+
`Deprecated-signer migration (${legName}): ${oversizedRefs.length} input(s) exceed the per-output limit ${vtxoMaxAmount} and cannot be migrated cooperatively; they require a unilateral exit.`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
const oversizedField = oversizedRefs.length > 0 ? { oversized: oversizedRefs } : {};
|
|
3165
|
+
const capped = capSettlementBatch(
|
|
3166
|
+
byValueDescending(sized.map((c) => ({ value: valueOf(c), c }))),
|
|
3167
|
+
vtxoMaxAmount
|
|
3168
|
+
).map((w) => w.c);
|
|
3169
|
+
const deferred = sized.length - capped.length;
|
|
3170
|
+
const totalAmount = capped.reduce((sum, c) => sum + BigInt(valueOf(c)), 0n);
|
|
3171
|
+
if (totalAmount < dustAmount) {
|
|
3172
|
+
const onlyOversized = sized.length === 0 && oversizedRefs.length > 0;
|
|
3173
|
+
return {
|
|
3174
|
+
migrated: [],
|
|
3175
|
+
skipped: onlyOversized ? "oversized-only" : "below-dust",
|
|
3176
|
+
...oversizedField
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
try {
|
|
3180
|
+
const txid = await submit(capped);
|
|
3181
|
+
return {
|
|
3182
|
+
txid,
|
|
3183
|
+
migrated: capped.map(toRef),
|
|
3184
|
+
...deferred > 0 ? { deferred } : {},
|
|
3185
|
+
...oversizedField
|
|
3186
|
+
};
|
|
3187
|
+
} catch (e) {
|
|
3188
|
+
return {
|
|
3189
|
+
migrated: [],
|
|
3190
|
+
error: e instanceof Error ? e.message : String(e),
|
|
3191
|
+
...oversizedField
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
/**
|
|
3196
|
+
* Enumerate the wallet's `default`/`delegate` contracts, classify each
|
|
3197
|
+
* against the fresh signer set, and split their spendable VTXOs into
|
|
3198
|
+
* cooperatively-migratable and cutoff-expired sets while building the
|
|
3199
|
+
* per-signer status report. Current-signer contracts are skipped; swept
|
|
3200
|
+
* (recoverable) VTXOs are excluded from the settle sets — those follow the
|
|
3201
|
+
* recovery path — but are still counted on EXPIRED report rows
|
|
3202
|
+
* (`recoverableCount`) so post-cutoff funds in flight stay visible.
|
|
3203
|
+
*/
|
|
3204
|
+
async classifyDeprecatedSignerContracts(info) {
|
|
3205
|
+
const cm = await this.wallet.getContractManager();
|
|
3206
|
+
const signerSet = signerSetFromInfo(info);
|
|
3207
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3208
|
+
const contractsWithVtxos = await cm.getContractsWithVtxos({
|
|
3209
|
+
type: ["default", "delegate"]
|
|
3210
|
+
});
|
|
3211
|
+
const reportsBySigner = /* @__PURE__ */ new Map();
|
|
3212
|
+
const migratable = [];
|
|
3213
|
+
const expired = [];
|
|
3214
|
+
for (const { contract, vtxos } of contractsWithVtxos) {
|
|
3215
|
+
const serverPubKey = contract.params.serverPubKey;
|
|
3216
|
+
if (!serverPubKey) continue;
|
|
3217
|
+
const cls = classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds);
|
|
3218
|
+
if (cls.status === "CURRENT") continue;
|
|
3219
|
+
const recoverable = vtxos.filter((v) => isRecoverable(v));
|
|
3220
|
+
const spendable = vtxos.filter((v) => isSpendable(v) && !isRecoverable(v));
|
|
3221
|
+
const value = spendable.reduce((sum, v) => sum + v.value, 0);
|
|
3222
|
+
let recoverableCount = 0;
|
|
3223
|
+
let recoverableValue = 0;
|
|
3224
|
+
let awaitingSweepCount = 0;
|
|
3225
|
+
let awaitingSweepValue = 0;
|
|
3226
|
+
let nextSweepEta;
|
|
3227
|
+
if (cls.status === "EXPIRED") {
|
|
3228
|
+
recoverableCount = recoverable.length;
|
|
3229
|
+
recoverableValue = recoverable.reduce((sum, v) => sum + v.value, 0);
|
|
3230
|
+
awaitingSweepCount = spendable.length;
|
|
3231
|
+
awaitingSweepValue = value;
|
|
3232
|
+
for (const v of spendable) {
|
|
3233
|
+
const exp = v.virtualStatus.batchExpiry;
|
|
3234
|
+
if (exp !== void 0 && (nextSweepEta === void 0 || exp < nextSweepEta)) {
|
|
3235
|
+
nextSweepEta = exp;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
const existing = reportsBySigner.get(cls.signerPubKey);
|
|
3240
|
+
if (existing) {
|
|
3241
|
+
existing.vtxoCount += spendable.length;
|
|
3242
|
+
existing.totalValue += value;
|
|
3243
|
+
existing.recoverableCount += recoverableCount;
|
|
3244
|
+
existing.recoverableValue += recoverableValue;
|
|
3245
|
+
existing.awaitingSweepCount += awaitingSweepCount;
|
|
3246
|
+
existing.awaitingSweepValue += awaitingSweepValue;
|
|
3247
|
+
if (nextSweepEta !== void 0) {
|
|
3248
|
+
existing.nextSweepEta = existing.nextSweepEta === void 0 ? nextSweepEta : Math.min(existing.nextSweepEta, nextSweepEta);
|
|
3249
|
+
}
|
|
3250
|
+
} else {
|
|
3251
|
+
reportsBySigner.set(cls.signerPubKey, {
|
|
3252
|
+
signerPubKey: cls.signerPubKey,
|
|
3253
|
+
status: cls.status,
|
|
3254
|
+
cutoffDate: cls.cutoffDate,
|
|
3255
|
+
secondsUntilCutoff: cls.secondsUntilCutoff,
|
|
3256
|
+
vtxoCount: spendable.length,
|
|
3257
|
+
totalValue: value,
|
|
3258
|
+
boardingCount: 0,
|
|
3259
|
+
boardingValue: 0,
|
|
3260
|
+
recoverableCount,
|
|
3261
|
+
recoverableValue,
|
|
3262
|
+
awaitingSweepCount,
|
|
3263
|
+
awaitingSweepValue,
|
|
3264
|
+
nextSweepEta
|
|
3265
|
+
});
|
|
3266
|
+
}
|
|
3267
|
+
if (isCooperativelyMigratable(cls.status)) {
|
|
3268
|
+
for (const v of spendable) {
|
|
3269
|
+
if (!v.virtualStatus.batchExpiry) continue;
|
|
3270
|
+
migratable.push({ vtxo: v, classification: cls });
|
|
3271
|
+
}
|
|
3272
|
+
} else if (cls.status === "EXPIRED") {
|
|
3273
|
+
for (const v of spendable) expired.push({ vtxo: v, classification: cls });
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return {
|
|
3277
|
+
reports: Array.from(reportsBySigner.values()),
|
|
3278
|
+
migratable,
|
|
3279
|
+
expired
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
/**
|
|
3283
|
+
* Boarding sibling of {@link classifyDeprecatedSignerContracts} (Section 7):
|
|
3284
|
+
* fan out over the wallet's boarding addresses (current + historical), group
|
|
3285
|
+
* the on-chain UTXOs per address, classify each address's signer against the
|
|
3286
|
+
* fresh signer set, and split the confirmed boarding coins into cooperatively-
|
|
3287
|
+
* migratable and cutoff-expired sets while building the per-signer report.
|
|
3288
|
+
*
|
|
3289
|
+
* Discovery sees the active signer plus EVERY deprecated key (EXPIRED
|
|
3290
|
+
* included), so expired-signer boarding is still reported; migration
|
|
3291
|
+
* eligibility is gated afterwards by {@link isCooperativelyMigratable} and a
|
|
3292
|
+
* per-row boarding-output CSV check — never by the fetch. Current-signer
|
|
3293
|
+
* coins are classified `CURRENT` and ignored; foreign-ASP rows are excluded
|
|
3294
|
+
* because their keys are not in the signer set.
|
|
3295
|
+
*/
|
|
3296
|
+
async classifyDeprecatedSignerBoarding(info) {
|
|
3297
|
+
const wallet = this.requireMigrationCapableWallet();
|
|
3298
|
+
const signerSet = signerSetFromInfo(info);
|
|
3299
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3300
|
+
const allowed = /* @__PURE__ */ new Set([signerSet.active, ...signerSet.deprecated.keys()]);
|
|
3301
|
+
const groups = await wallet.getBoardingUtxosForSigners(allowed);
|
|
3302
|
+
let chainTipHeight;
|
|
3303
|
+
if (groups.some((g) => g.csvTimelock.type === "blocks")) {
|
|
3304
|
+
const tip = await wallet.onchainProvider.getChainTip();
|
|
3305
|
+
chainTipHeight = tip.height;
|
|
3306
|
+
}
|
|
3307
|
+
const reportsBySigner = /* @__PURE__ */ new Map();
|
|
3308
|
+
const migratable = [];
|
|
3309
|
+
const expired = [];
|
|
3310
|
+
for (const group of groups) {
|
|
3311
|
+
const cls = classifyAgainstSignerSet(group.serverPubKey, signerSet, nowSeconds);
|
|
3312
|
+
if (cls.status === "CURRENT") continue;
|
|
3313
|
+
const confirmed = group.coins.filter((c) => c.status.confirmed);
|
|
3314
|
+
if (confirmed.length === 0) continue;
|
|
3315
|
+
const value = confirmed.reduce((sum, c) => sum + c.value, 0);
|
|
3316
|
+
const existing = reportsBySigner.get(cls.signerPubKey);
|
|
3317
|
+
if (existing) {
|
|
3318
|
+
existing.boardingCount += confirmed.length;
|
|
3319
|
+
existing.boardingValue += value;
|
|
3320
|
+
} else {
|
|
3321
|
+
reportsBySigner.set(cls.signerPubKey, {
|
|
3322
|
+
signerPubKey: cls.signerPubKey,
|
|
3323
|
+
status: cls.status,
|
|
3324
|
+
cutoffDate: cls.cutoffDate,
|
|
3325
|
+
secondsUntilCutoff: cls.secondsUntilCutoff,
|
|
3326
|
+
vtxoCount: 0,
|
|
3327
|
+
totalValue: 0,
|
|
3328
|
+
boardingCount: confirmed.length,
|
|
3329
|
+
boardingValue: value,
|
|
3330
|
+
// Boarding UTXOs don't carry an offchain sweep lifecycle; the
|
|
3331
|
+
// post-cutoff recover-on-sweep fields apply to VTXOs only and
|
|
3332
|
+
// are merged in from the VTXO classifier (mergeSignerReports).
|
|
3333
|
+
recoverableCount: 0,
|
|
3334
|
+
recoverableValue: 0,
|
|
3335
|
+
awaitingSweepCount: 0,
|
|
3336
|
+
awaitingSweepValue: 0
|
|
3337
|
+
});
|
|
3338
|
+
}
|
|
3339
|
+
for (const coin of confirmed) {
|
|
3340
|
+
const boardingExpired = hasBoardingTxExpired(
|
|
3341
|
+
coin,
|
|
3342
|
+
group.csvTimelock,
|
|
3343
|
+
chainTipHeight
|
|
3344
|
+
);
|
|
3345
|
+
if (isCooperativelyMigratable(cls.status) && !boardingExpired) {
|
|
3346
|
+
migratable.push({ coin, classification: cls });
|
|
3347
|
+
} else if (cls.status === "EXPIRED") {
|
|
3348
|
+
expired.push({ coin, classification: cls });
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
return {
|
|
3353
|
+
reports: Array.from(reportsBySigner.values()),
|
|
3354
|
+
migratable,
|
|
3355
|
+
expired
|
|
3356
|
+
};
|
|
3357
|
+
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Automatic migration pass invoked from the poll loop. Self-contained:
|
|
3360
|
+
* respects an exponential cooldown and logs failures rather than throwing,
|
|
3361
|
+
* so a persistently failing migration backs off instead of re-submitting
|
|
3362
|
+
* an identical intent every cycle.
|
|
3363
|
+
*/
|
|
3364
|
+
async runMigrationPass() {
|
|
3365
|
+
const cooldownMs = Math.min(
|
|
3366
|
+
_VtxoManager.MIGRATION_COOLDOWN_MS * Math.pow(2, this.consecutiveMigrationFailures),
|
|
3367
|
+
_VtxoManager.MIGRATION_MAX_BACKOFF_MS
|
|
3368
|
+
);
|
|
3369
|
+
if (Date.now() - this.lastMigrationTimestamp < cooldownMs) return;
|
|
3370
|
+
try {
|
|
3371
|
+
const report = await this.migrateCore();
|
|
3372
|
+
const legError = report.vtxos?.error ?? report.boarding?.error;
|
|
3373
|
+
if (legError) {
|
|
3374
|
+
this.consecutiveMigrationFailures++;
|
|
3375
|
+
console.error("Deprecated-signer migration leg failed:", legError);
|
|
3376
|
+
} else {
|
|
3377
|
+
this.consecutiveMigrationFailures = 0;
|
|
3378
|
+
}
|
|
3379
|
+
} catch (e) {
|
|
3380
|
+
this.consecutiveMigrationFailures++;
|
|
3381
|
+
console.error("Error during deprecated-signer migration:", e);
|
|
3382
|
+
} finally {
|
|
3383
|
+
this.lastMigrationTimestamp = Date.now();
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
/** Asserts migration capability and returns the typed wallet. */
|
|
3387
|
+
requireMigrationCapableWallet() {
|
|
3388
|
+
if (!isMigrationCapable(this.wallet)) {
|
|
3389
|
+
throw new Error(
|
|
3390
|
+
"Deprecated-signer migration requires a Wallet instance with arkProvider, arkServerPublicKey, and rotateServerSigner"
|
|
3391
|
+
);
|
|
3392
|
+
}
|
|
3393
|
+
return this.wallet;
|
|
3394
|
+
}
|
|
3395
|
+
/**
|
|
3396
|
+
* If the wallet's own construction-time signer snapshot has been deprecated,
|
|
3397
|
+
* re-derive its receive/boarding state under the active signer so any output
|
|
3398
|
+
* built afterwards commits to the active key. No-op when the snapshot is
|
|
3399
|
+
* already current. Returns whether a rotation was applied. Treats an
|
|
3400
|
+
* unknown-signer snapshot as "do not rotate" (caller decides).
|
|
3401
|
+
*
|
|
3402
|
+
* Shared by the migration pass (where the wallet's own snapshot is the thing
|
|
3403
|
+
* being migrated) and the recovery/renewal/periodic-settle paths (via
|
|
3404
|
+
* {@link rotateForRecoverableInputs}), so a swept old-signer VTXO recovered
|
|
3405
|
+
* after cutoff re-mints under the active signer rather than re-committing to
|
|
3406
|
+
* the deprecated key (Section 6 / post-cutoff). `rotateServerSigner` is
|
|
3407
|
+
* idempotent and serializes itself against HD receive rotation, so repeated
|
|
3408
|
+
* calls across passes are safe.
|
|
3409
|
+
*/
|
|
3410
|
+
async ensureReceiveOnActiveSigner(info) {
|
|
3411
|
+
const wallet = this.requireMigrationCapableWallet();
|
|
3412
|
+
const signerSet = signerSetFromInfo(info);
|
|
3413
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3414
|
+
const walletClass = classifyAgainstSignerSet(
|
|
3415
|
+
hex.encode(wallet.arkServerPublicKey),
|
|
3416
|
+
signerSet,
|
|
3417
|
+
nowSeconds
|
|
3418
|
+
);
|
|
3419
|
+
if (walletClass.status === "CURRENT" || walletClass.status === "UNKNOWN_SIGNER") {
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
await wallet.rotateServerSigner(hex.decode(info.signerPubkey), info.checkpointTapscript);
|
|
3423
|
+
return true;
|
|
3424
|
+
}
|
|
3425
|
+
/**
|
|
3426
|
+
* Rotation guard for the recovery-bearing settle paths (recover / renew /
|
|
3427
|
+
* periodic settle). Pins the wallet's receive snapshot to the active signer
|
|
3428
|
+
* before they build their output, but ONLY when this pass actually carries
|
|
3429
|
+
* an input minted under a deprecated signer — so a routine current-signer
|
|
3430
|
+
* settle on a long-lived pre-rotation instance does not eagerly rotate.
|
|
3431
|
+
*
|
|
3432
|
+
* Cheap in the common case: a watch-only/proxy wallet (not migration-capable)
|
|
3433
|
+
* and a current/unknown wallet snapshot both short-circuit before the
|
|
3434
|
+
* contract round-trip, so the only instance that pays for the input scan is
|
|
3435
|
+
* the long-lived deprecated-snapshot one that genuinely needs rotating.
|
|
3436
|
+
*
|
|
3437
|
+
* Runs OUTSIDE any `renewalInProgress` window the caller sets, and
|
|
3438
|
+
* `rotateServerSigner` does not depend on that flag, so it cannot deadlock
|
|
3439
|
+
* against the receive rotator. Returns whether a rotation was applied.
|
|
3440
|
+
*/
|
|
3441
|
+
async rotateForRecoverableInputs(inputs, info) {
|
|
3442
|
+
if (!isMigrationCapable(this.wallet)) return false;
|
|
3443
|
+
const signerSet = signerSetFromInfo(info);
|
|
3444
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3445
|
+
const walletClass = classifyAgainstSignerSet(
|
|
3446
|
+
hex.encode(this.wallet.arkServerPublicKey),
|
|
3447
|
+
signerSet,
|
|
3448
|
+
nowSeconds
|
|
3449
|
+
);
|
|
3450
|
+
if (walletClass.status === "CURRENT" || walletClass.status === "UNKNOWN_SIGNER") {
|
|
3451
|
+
return false;
|
|
3452
|
+
}
|
|
3453
|
+
if (!await this.anyInputUnderDeprecatedSigner(inputs, signerSet, nowSeconds)) {
|
|
3454
|
+
return false;
|
|
3455
|
+
}
|
|
3456
|
+
return this.ensureReceiveOnActiveSigner(info);
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Whether any of the given input outpoints belongs to a contract whose
|
|
3460
|
+
* server signer classifies as non-`CURRENT` against the fresh signer set —
|
|
3461
|
+
* i.e. a deprecated-signer (incl. EXPIRED) input. Maps outpoints to their
|
|
3462
|
+
* owning contract via the ContractManager so it works on the typed
|
|
3463
|
+
* {@link ExtendedVirtualCoin}/{@link ExtendedCoin} inputs the recovery paths
|
|
3464
|
+
* carry (which don't expose `contractScript`).
|
|
3465
|
+
*/
|
|
3466
|
+
async anyInputUnderDeprecatedSigner(inputs, signerSet, nowSeconds) {
|
|
3467
|
+
if (inputs.length === 0) return false;
|
|
3468
|
+
const wanted = new Set(inputs.map((i) => `${i.txid}:${i.vout}`));
|
|
3469
|
+
const cm = await this.wallet.getContractManager();
|
|
3470
|
+
const contractsWithVtxos = await cm.getContractsWithVtxos({
|
|
3471
|
+
type: ["default", "delegate"]
|
|
3472
|
+
});
|
|
3473
|
+
for (const { contract, vtxos } of contractsWithVtxos) {
|
|
3474
|
+
const serverPubKey = contract.params.serverPubKey;
|
|
3475
|
+
if (!serverPubKey) continue;
|
|
3476
|
+
if (classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds).status === "CURRENT") {
|
|
3477
|
+
continue;
|
|
3478
|
+
}
|
|
3479
|
+
for (const v of vtxos) {
|
|
3480
|
+
if (wanted.has(`${v.txid}:${v.vout}`)) return true;
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
return false;
|
|
3484
|
+
}
|
|
2881
3485
|
// ========== Private Helpers ==========
|
|
2882
3486
|
/** Asserts sweep capability and returns the typed wallet. */
|
|
2883
3487
|
getSweepWallet() {
|
|
@@ -2904,6 +3508,16 @@ var VtxoManager = class _VtxoManager {
|
|
|
2904
3508
|
getArkProvider() {
|
|
2905
3509
|
return this.getSweepWallet().arkProvider;
|
|
2906
3510
|
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Read-only access to the ark provider for fetching server limits. Unlike
|
|
3513
|
+
* {@link getArkProvider}, this does not require full boarding-sweep
|
|
3514
|
+
* capability — recovery and renewal only need it to read `vtxoMaxAmount`.
|
|
3515
|
+
* Returns undefined when no provider is wired, which callers treat as
|
|
3516
|
+
* "no limit".
|
|
3517
|
+
*/
|
|
3518
|
+
getInfoProvider() {
|
|
3519
|
+
return this.wallet.arkProvider;
|
|
3520
|
+
}
|
|
2907
3521
|
/** Returns the Bitcoin network configuration from the wallet. */
|
|
2908
3522
|
getNetwork() {
|
|
2909
3523
|
return this.getSweepWallet().network;
|
|
@@ -3105,6 +3719,10 @@ var VtxoManager = class _VtxoManager {
|
|
|
3105
3719
|
}
|
|
3106
3720
|
}
|
|
3107
3721
|
}
|
|
3722
|
+
const migrationEnabled = this.settlementConfig !== false && (this.settlementConfig?.deprecatedSignerMigration ?? DEFAULT_SETTLEMENT_CONFIG.deprecatedSignerMigration);
|
|
3723
|
+
if (migrationEnabled && isMigrationCapable(this.wallet)) {
|
|
3724
|
+
await this.runMigrationPass();
|
|
3725
|
+
}
|
|
3108
3726
|
});
|
|
3109
3727
|
} catch (e) {
|
|
3110
3728
|
hadError = true;
|
|
@@ -3173,7 +3791,8 @@ var VtxoManager = class _VtxoManager {
|
|
|
3173
3791
|
return;
|
|
3174
3792
|
}
|
|
3175
3793
|
const dustAmount = getDustAmount(this.wallet);
|
|
3176
|
-
const
|
|
3794
|
+
const info = await this.getArkProvider().getInfo();
|
|
3795
|
+
const { fees, vtxoMaxAmount } = info;
|
|
3177
3796
|
const estimator = new Estimator(fees.intentFee);
|
|
3178
3797
|
let totalAmount = 0n;
|
|
3179
3798
|
const filteredBoarding = [];
|
|
@@ -3202,12 +3821,19 @@ var VtxoManager = class _VtxoManager {
|
|
|
3202
3821
|
if (inputFee.satoshis >= v.value) {
|
|
3203
3822
|
continue;
|
|
3204
3823
|
}
|
|
3824
|
+
const net = BigInt(v.value) - BigInt(inputFee.satoshis);
|
|
3825
|
+
if (vtxoMaxAmount >= 0n && totalAmount + net > vtxoMaxAmount) {
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3205
3828
|
filteredVtxos.push(v);
|
|
3206
|
-
totalAmount +=
|
|
3829
|
+
totalAmount += net;
|
|
3207
3830
|
}
|
|
3208
3831
|
if (filteredBoarding.length === 0 && filteredVtxos.length === 0) {
|
|
3209
3832
|
return;
|
|
3210
3833
|
}
|
|
3834
|
+
if (isMigrationCapable(this.wallet)) {
|
|
3835
|
+
await this.rotateForRecoverableInputs([...filteredBoarding, ...filteredVtxos], info);
|
|
3836
|
+
}
|
|
3211
3837
|
const arkAddress = await this.wallet.getAddress();
|
|
3212
3838
|
const outputFee = estimator.evalOffchainOutput({
|
|
3213
3839
|
amount: totalAmount,
|
|
@@ -3270,7 +3896,6 @@ var VtxoManager = class _VtxoManager {
|
|
|
3270
3896
|
clearTimeout(timer);
|
|
3271
3897
|
}
|
|
3272
3898
|
const subscription = await this.contractEventsSubscriptionReady;
|
|
3273
|
-
this.contractEventsSubscription = void 0;
|
|
3274
3899
|
subscription?.();
|
|
3275
3900
|
})();
|
|
3276
3901
|
return this.disposePromise;
|
|
@@ -3811,15 +4436,17 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
|
|
|
3811
4436
|
txTime = getTxCreatedAt ? await getTxCreatedAt(vtxo.arkTxId) ?? vtxo.createdAt.getTime() + 1 : vtxo.createdAt.getTime() + 1;
|
|
3812
4437
|
}
|
|
3813
4438
|
const assets = subtractAssets(allSpent, changes);
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
4439
|
+
if (txAmount !== 0 || assets) {
|
|
4440
|
+
sent.push({
|
|
4441
|
+
key: { ...txKey, arkTxid: vtxo.arkTxId },
|
|
4442
|
+
tag: "offchain",
|
|
4443
|
+
type: "SENT" /* TxSent */,
|
|
4444
|
+
amount: txAmount,
|
|
4445
|
+
settled: true,
|
|
4446
|
+
createdAt: txTime,
|
|
4447
|
+
...assets && { assets }
|
|
4448
|
+
});
|
|
4449
|
+
}
|
|
3823
4450
|
}
|
|
3824
4451
|
if (vtxo.settledBy && !commitmentsToIgnore.has(vtxo.settledBy) && !sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
|
|
3825
4452
|
const changes = fromOldestVtxo.filter(
|
|
@@ -5992,6 +6619,16 @@ function isDiscoverable(handler) {
|
|
|
5992
6619
|
}
|
|
5993
6620
|
|
|
5994
6621
|
// src/contracts/contractWatcher.ts
|
|
6622
|
+
function computeReconnectDelay(attempt, baseMs, maxMs) {
|
|
6623
|
+
return Math.min(baseMs * Math.pow(2, attempt - 1), maxMs);
|
|
6624
|
+
}
|
|
6625
|
+
var DEFAULT_CONTRACT_WATCHER_CONFIG = {
|
|
6626
|
+
failsafePollIntervalMs: 2e4,
|
|
6627
|
+
reconnectDelayMs: 1e3,
|
|
6628
|
+
maxReconnectDelayMs: 5e3,
|
|
6629
|
+
maxReconnectAttempts: 0
|
|
6630
|
+
// unlimited
|
|
6631
|
+
};
|
|
5995
6632
|
var ContractWatcher = class {
|
|
5996
6633
|
config;
|
|
5997
6634
|
contracts = /* @__PURE__ */ new Map();
|
|
@@ -6011,14 +6648,7 @@ var ContractWatcher = class {
|
|
|
6011
6648
|
*/
|
|
6012
6649
|
constructor(config) {
|
|
6013
6650
|
this.config = {
|
|
6014
|
-
|
|
6015
|
-
// 1 minute
|
|
6016
|
-
reconnectDelayMs: 1e3,
|
|
6017
|
-
// 1 second
|
|
6018
|
-
maxReconnectDelayMs: 3e4,
|
|
6019
|
-
// 30 seconds
|
|
6020
|
-
maxReconnectAttempts: 0,
|
|
6021
|
-
// unlimited
|
|
6651
|
+
...DEFAULT_CONTRACT_WATCHER_CONFIG,
|
|
6022
6652
|
...config
|
|
6023
6653
|
};
|
|
6024
6654
|
}
|
|
@@ -6246,8 +6876,9 @@ var ContractWatcher = class {
|
|
|
6246
6876
|
}
|
|
6247
6877
|
this.connectionState = "reconnecting";
|
|
6248
6878
|
this.reconnectAttempts++;
|
|
6249
|
-
const delay =
|
|
6250
|
-
this.
|
|
6879
|
+
const delay = computeReconnectDelay(
|
|
6880
|
+
this.reconnectAttempts,
|
|
6881
|
+
this.config.reconnectDelayMs,
|
|
6251
6882
|
this.config.maxReconnectDelayMs
|
|
6252
6883
|
);
|
|
6253
6884
|
this.reconnectTimeoutId = setTimeout(() => {
|
|
@@ -7618,6 +8249,22 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
|
|
|
7618
8249
|
async drain() {
|
|
7619
8250
|
await this.chain.catch(() => void 0);
|
|
7620
8251
|
}
|
|
8252
|
+
/**
|
|
8253
|
+
* Run `fn` on the rotator's serialization chain, so it cannot interleave
|
|
8254
|
+
* with a receive `rotate()`. Used by {@link Wallet.rotateServerSigner} to
|
|
8255
|
+
* serialize server-signer rotation against HD receive rotation: both
|
|
8256
|
+
* rebuild and swap `offchainTapscript`, so running them concurrently could
|
|
8257
|
+
* tear the wallet's visible receive state. The chain keeps advancing even
|
|
8258
|
+
* if `fn` rejects (its own caller still sees the rejection).
|
|
8259
|
+
*/
|
|
8260
|
+
runExclusive(fn) {
|
|
8261
|
+
const run = this.chain.catch(() => void 0).then(fn);
|
|
8262
|
+
this.chain = run.then(
|
|
8263
|
+
() => void 0,
|
|
8264
|
+
() => void 0
|
|
8265
|
+
);
|
|
8266
|
+
return run;
|
|
8267
|
+
}
|
|
7621
8268
|
/**
|
|
7622
8269
|
* Tear down the subscription first so no late `vtxo_received` event
|
|
7623
8270
|
* can queue work on a disposing wallet, then drain any in-flight
|
|
@@ -7975,7 +8622,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7975
8622
|
this.network = network;
|
|
7976
8623
|
this.onchainProvider = onchainProvider;
|
|
7977
8624
|
this.indexerProvider = indexerProvider;
|
|
7978
|
-
this.arkServerPublicKey = arkServerPublicKey;
|
|
7979
8625
|
this.dustAmount = dustAmount;
|
|
7980
8626
|
this.walletRepository = walletRepository;
|
|
7981
8627
|
this.contractRepository = contractRepository;
|
|
@@ -7992,6 +8638,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7992
8638
|
}
|
|
7993
8639
|
this._offchainTapscript = offchainTapscript;
|
|
7994
8640
|
this._boardingTapscript = boardingTapscript;
|
|
8641
|
+
this._arkServerPublicKey = arkServerPublicKey;
|
|
7995
8642
|
this.watcherConfig = watcherConfig;
|
|
7996
8643
|
this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
|
|
7997
8644
|
this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
|
|
@@ -8000,7 +8647,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8000
8647
|
_contractManagerInitializing;
|
|
8001
8648
|
watcherConfig;
|
|
8002
8649
|
_assetManager;
|
|
8003
|
-
_syncVtxosInflight;
|
|
8004
8650
|
walletContractTimelocks;
|
|
8005
8651
|
// Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
|
|
8006
8652
|
// from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
|
|
@@ -8029,6 +8675,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8029
8675
|
* it stays the index-0 baseline for their lifetime.
|
|
8030
8676
|
*/
|
|
8031
8677
|
_boardingTapscript;
|
|
8678
|
+
/**
|
|
8679
|
+
* Backing field for the active server signer (x-only, 32 bytes). Read via
|
|
8680
|
+
* the public {@link arkServerPublicKey} getter; written only by
|
|
8681
|
+
* {@link Wallet.setArkServerPublicKeyForRotation}, the sanctioned
|
|
8682
|
+
* server-signer rotation write path (analogue of `_offchainTapscript`). It
|
|
8683
|
+
* is a *current value*, not a fixed constructor constant, because
|
|
8684
|
+
* mid-session server-signer rotation (plan §4) swaps it when arkd rotates
|
|
8685
|
+
* its active signer. Wallets that never span a rotation keep their
|
|
8686
|
+
* construction-time snapshot for their lifetime.
|
|
8687
|
+
*/
|
|
8688
|
+
_arkServerPublicKey;
|
|
8689
|
+
/**
|
|
8690
|
+
* x-only hex of the operator's deprecated signer keys (from
|
|
8691
|
+
* `ArkInfo.deprecatedSigners`), cached for the OFFLINE read/watch paths.
|
|
8692
|
+
* The boarding watch/history surfaces ({@link getBoardingAddresses},
|
|
8693
|
+
* {@link getBoardingTxs}) fan out over {current} ∪ this set so a deposit at
|
|
8694
|
+
* a boarding address minted under a now-rotated operator signer keeps being
|
|
8695
|
+
* watched. Refreshed from the server-info snapshot at construction (via the
|
|
8696
|
+
* create() factories) and on a detected signer change. Deliberately NOT
|
|
8697
|
+
* consulted by the spend path — {@link getBoardingUtxos} stays
|
|
8698
|
+
* current-signer-only (a deprecated-signer input in a plain settle() is
|
|
8699
|
+
* rejected; old-signer recovery goes through the migration API).
|
|
8700
|
+
*/
|
|
8701
|
+
_deprecatedSigners = /* @__PURE__ */ new Map();
|
|
8702
|
+
/**
|
|
8703
|
+
* Refresh the cached deprecated-signer set from a fresh server-info
|
|
8704
|
+
* snapshot. Called by the create() factories at construction and by the
|
|
8705
|
+
* server-info-change handler mid-session. Lenient: a malformed deprecated
|
|
8706
|
+
* entry is skipped, never fatal to wallet creation.
|
|
8707
|
+
*/
|
|
8708
|
+
refreshDeprecatedSigners(info) {
|
|
8709
|
+
const next = /* @__PURE__ */ new Map();
|
|
8710
|
+
for (const s of info.deprecatedSigners ?? []) {
|
|
8711
|
+
if (!s.pubkey) continue;
|
|
8712
|
+
try {
|
|
8713
|
+
next.set(toXOnlySignerHex(s.pubkey), s.cutoffDate ?? 0n);
|
|
8714
|
+
} catch (e) {
|
|
8715
|
+
console.warn("Skipping malformed deprecated signer pubkey", s.pubkey, e);
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
this._deprecatedSigners = next;
|
|
8719
|
+
}
|
|
8720
|
+
/**
|
|
8721
|
+
* The signer set the boarding WATCH/HISTORY paths fan out over: the wallet's
|
|
8722
|
+
* current signer plus every cached deprecated signer. Distinct from the
|
|
8723
|
+
* spend path, which is current-signer-only.
|
|
8724
|
+
*/
|
|
8725
|
+
watchedBoardingSigners() {
|
|
8726
|
+
return /* @__PURE__ */ new Set([
|
|
8727
|
+
toXOnlySignerHex(hex.encode(this.boardingTapscript.options.serverPubKey)),
|
|
8728
|
+
...this._deprecatedSigners.keys()
|
|
8729
|
+
]);
|
|
8730
|
+
}
|
|
8032
8731
|
/**
|
|
8033
8732
|
* Currently-active receive tapscript. Read-only from the outside;
|
|
8034
8733
|
* mutated only via {@link Wallet.setOffchainTapscriptForRotation}
|
|
@@ -8037,6 +8736,16 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8037
8736
|
get offchainTapscript() {
|
|
8038
8737
|
return this._offchainTapscript;
|
|
8039
8738
|
}
|
|
8739
|
+
/**
|
|
8740
|
+
* The wallet's current active server signer (x-only, 32 bytes). Read-only
|
|
8741
|
+
* from the outside; mutated only via
|
|
8742
|
+
* {@link Wallet.setArkServerPublicKeyForRotation} during mid-session
|
|
8743
|
+
* server-signer rotation (plan §4). Single-valued for wallets that never
|
|
8744
|
+
* span a rotation.
|
|
8745
|
+
*/
|
|
8746
|
+
get arkServerPublicKey() {
|
|
8747
|
+
return this._arkServerPublicKey;
|
|
8748
|
+
}
|
|
8040
8749
|
/**
|
|
8041
8750
|
* The wallet's current boarding tapscript (the on-chain onboarding
|
|
8042
8751
|
* target). Read-only from the outside; mutated only via
|
|
@@ -8195,7 +8904,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8195
8904
|
throw new Error("Invalid configured public key");
|
|
8196
8905
|
}
|
|
8197
8906
|
const setup = await _ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
8198
|
-
|
|
8907
|
+
const wallet = new _ReadonlyWallet(
|
|
8199
8908
|
config.identity,
|
|
8200
8909
|
setup.network,
|
|
8201
8910
|
setup.onchainProvider,
|
|
@@ -8210,6 +8919,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8210
8919
|
config.watcherConfig,
|
|
8211
8920
|
setup.walletContractTimelocks
|
|
8212
8921
|
);
|
|
8922
|
+
wallet.refreshDeprecatedSigners(setup.info);
|
|
8923
|
+
return wallet;
|
|
8213
8924
|
}
|
|
8214
8925
|
get arkAddress() {
|
|
8215
8926
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -8233,10 +8944,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8233
8944
|
* Return the wallet's combined onchain and offchain balances.
|
|
8234
8945
|
*/
|
|
8235
8946
|
async getBalance() {
|
|
8236
|
-
const [boardingUtxos, vtxos] = await Promise.all([
|
|
8947
|
+
const [boardingUtxos, vtxos, pendingOutpoints] = await Promise.all([
|
|
8237
8948
|
this.getBoardingUtxos(),
|
|
8238
|
-
this.getVtxos()
|
|
8949
|
+
this.getVtxos(),
|
|
8950
|
+
this.pendingRecoveryOutpoints()
|
|
8239
8951
|
]);
|
|
8952
|
+
const isPendingRecovery = (coin) => pendingOutpoints.has(`${coin.txid}:${coin.vout}`);
|
|
8240
8953
|
let confirmed = 0;
|
|
8241
8954
|
let unconfirmed = 0;
|
|
8242
8955
|
for (const utxo of boardingUtxos) {
|
|
@@ -8249,11 +8962,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8249
8962
|
let settled = 0;
|
|
8250
8963
|
let preconfirmed = 0;
|
|
8251
8964
|
let recoverable = 0;
|
|
8252
|
-
|
|
8253
|
-
|
|
8965
|
+
let pendingRecovery = 0;
|
|
8966
|
+
settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled" && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
|
|
8967
|
+
preconfirmed = vtxos.filter(
|
|
8968
|
+
(coin) => coin.virtualStatus.state === "preconfirmed" && !isPendingRecovery(coin)
|
|
8969
|
+
).reduce((sum, coin) => sum + coin.value, 0);
|
|
8254
8970
|
recoverable = vtxos.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept").reduce((sum, coin) => sum + coin.value, 0);
|
|
8971
|
+
pendingRecovery = vtxos.filter(isPendingRecovery).reduce((sum, coin) => sum + coin.value, 0);
|
|
8255
8972
|
const totalBoarding = confirmed + unconfirmed;
|
|
8256
|
-
const totalOffchain = settled + preconfirmed + recoverable;
|
|
8973
|
+
const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
|
|
8257
8974
|
const assetBalances = /* @__PURE__ */ new Map();
|
|
8258
8975
|
for (const vtxo of vtxos) {
|
|
8259
8976
|
if (!isSpendable(vtxo)) continue;
|
|
@@ -8278,6 +8995,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8278
8995
|
preconfirmed,
|
|
8279
8996
|
available: settled + preconfirmed,
|
|
8280
8997
|
recoverable,
|
|
8998
|
+
pendingRecovery,
|
|
8281
8999
|
total: totalBoarding + totalOffchain,
|
|
8282
9000
|
assets
|
|
8283
9001
|
};
|
|
@@ -8304,6 +9022,23 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8304
9022
|
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
8305
9023
|
});
|
|
8306
9024
|
}
|
|
9025
|
+
/**
|
|
9026
|
+
* Outpoints of VTXOs whose deprecated signer is past its cutoff (EXPIRED) and
|
|
9027
|
+
* which have not yet been swept — unspendable until they recover. Offline:
|
|
9028
|
+
* classifies the repo's contracts against the cached signer set (active +
|
|
9029
|
+
* {@link _deprecatedSigners}, cutoffs included). Empty fast-path when no
|
|
9030
|
+
* signer is deprecated. Consumed by {@link getBalance} (the `pendingRecovery`
|
|
9031
|
+
* bucket) and the send coin-selection path so neither counts nor spends them.
|
|
9032
|
+
*/
|
|
9033
|
+
async pendingRecoveryOutpoints() {
|
|
9034
|
+
if (this._deprecatedSigners.size === 0) return /* @__PURE__ */ new Set();
|
|
9035
|
+
const contractManager = await this.getContractManager();
|
|
9036
|
+
const contractsWithVtxos = await contractManager.getContractsWithVtxos();
|
|
9037
|
+
return selectPendingRecoveryOutpoints(contractsWithVtxos, {
|
|
9038
|
+
active: toXOnlySignerHex(hex.encode(this.offchainTapscript.options.serverPubKey)),
|
|
9039
|
+
deprecated: this._deprecatedSigners
|
|
9040
|
+
});
|
|
9041
|
+
}
|
|
8307
9042
|
/**
|
|
8308
9043
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
8309
9044
|
*/
|
|
@@ -8330,7 +9065,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8330
9065
|
* surfaced (plan §6-IV); {@link getBoardingAddress} stays single-valued.
|
|
8331
9066
|
*/
|
|
8332
9067
|
async getBoardingAddresses() {
|
|
8333
|
-
const tapscripts = await this.getBoardingTapscripts();
|
|
9068
|
+
const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
|
|
8334
9069
|
return tapscripts.map((t) => t.onchainAddress(this.network));
|
|
8335
9070
|
}
|
|
8336
9071
|
/**
|
|
@@ -8340,7 +9075,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8340
9075
|
async getBoardingTxs() {
|
|
8341
9076
|
const utxos = [];
|
|
8342
9077
|
const commitmentsToIgnore = /* @__PURE__ */ new Set();
|
|
8343
|
-
const tapscripts = await this.getBoardingTapscripts();
|
|
9078
|
+
const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
|
|
8344
9079
|
const outspendCache = /* @__PURE__ */ new Map();
|
|
8345
9080
|
for (const tapscript of tapscripts) {
|
|
8346
9081
|
const boardingAddress = tapscript.onchainAddress(this.network);
|
|
@@ -8415,8 +9150,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8415
9150
|
* Always includes the index-0 baseline (identity x-only key), which covers
|
|
8416
9151
|
* the degenerate equal-delay case where the index-0 boarding row is
|
|
8417
9152
|
* coalesced onto a `default` row and so isn't a `boarding`-typed contract.
|
|
9153
|
+
*
|
|
9154
|
+
* @param allowedSigners - Optional set of x-only-hex server keys whose
|
|
9155
|
+
* persisted boarding rows are included. Defaults to `{current x-only
|
|
9156
|
+
* signer}`, preserving today's current-signer-only discovery (and the
|
|
9157
|
+
* foreign-ASP guard). The deprecated-signer migration path widens this to
|
|
9158
|
+
* reach old-signer boarding addresses. The index-0 baseline and the
|
|
9159
|
+
* current display tapscript are always included regardless of the set.
|
|
8418
9160
|
*/
|
|
8419
|
-
async getBoardingTapscripts() {
|
|
9161
|
+
async getBoardingTapscripts(allowedSigners) {
|
|
8420
9162
|
const byScript = /* @__PURE__ */ new Map();
|
|
8421
9163
|
const add = (s) => byScript.set(hex.encode(s.pkScript), s);
|
|
8422
9164
|
const boardingCsv = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
@@ -8429,11 +9171,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8429
9171
|
);
|
|
8430
9172
|
add(this.boardingTapscript);
|
|
8431
9173
|
const serverPubKeyHex = hex.encode(this.boardingTapscript.options.serverPubKey);
|
|
9174
|
+
const allowed = allowedSigners ?? /* @__PURE__ */ new Set([toXOnlySignerHex(serverPubKeyHex)]);
|
|
8432
9175
|
const boardingContracts = await this.contractRepository.getContracts({
|
|
8433
9176
|
type: ["boarding"]
|
|
8434
9177
|
});
|
|
8435
9178
|
for (const c of boardingContracts) {
|
|
8436
|
-
if (c.params.serverPubKey
|
|
9179
|
+
if (!allowed.has(toXOnlySignerHex(c.params.serverPubKey))) continue;
|
|
8437
9180
|
try {
|
|
8438
9181
|
add(BoardingContractHandler.createScript(c.params));
|
|
8439
9182
|
} catch (e) {
|
|
@@ -8443,23 +9186,61 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8443
9186
|
return [...byScript.values()];
|
|
8444
9187
|
}
|
|
8445
9188
|
/**
|
|
8446
|
-
* Fetch and cache onchain inputs (UTXOs) received at the
|
|
8447
|
-
*
|
|
8448
|
-
*
|
|
8449
|
-
*
|
|
8450
|
-
*
|
|
9189
|
+
* Fetch and cache onchain inputs (UTXOs) received at the boarding addresses
|
|
9190
|
+
* of the given signer set, grouped per boarding address so the caller keeps
|
|
9191
|
+
* the address↔signer association that {@link ExtendedCoin} cannot carry
|
|
9192
|
+
* (it retains only the encoded leaves/tapTree the spend needs, not the
|
|
9193
|
+
* `DefaultVtxo.Script` and its `serverPubKey`/CSV delay).
|
|
9194
|
+
*
|
|
9195
|
+
* Per group it does exactly what {@link getBoardingUtxos} does per tapscript:
|
|
9196
|
+
* `getCoins` → {@link extendCoinWithTapscript} → `saveUtxos`. Offline-first:
|
|
9197
|
+
* it does not call `getInfo()`; the caller supplies the allowed signer set,
|
|
9198
|
+
* so the only network calls are the per-address `getCoins`.
|
|
9199
|
+
*
|
|
9200
|
+
* @param allowedSigners - x-only-hex server keys whose boarding addresses to
|
|
9201
|
+
* fetch (passed through to {@link getBoardingTapscripts}).
|
|
8451
9202
|
*/
|
|
8452
|
-
async
|
|
8453
|
-
const tapscripts = await this.getBoardingTapscripts();
|
|
8454
|
-
const
|
|
9203
|
+
async getBoardingUtxosForSigners(allowedSigners) {
|
|
9204
|
+
const tapscripts = await this.getBoardingTapscripts(allowedSigners);
|
|
9205
|
+
const groups = [];
|
|
8455
9206
|
for (const tapscript of tapscripts) {
|
|
8456
9207
|
const address = tapscript.onchainAddress(this.network);
|
|
8457
9208
|
const coins = await this.onchainProvider.getCoins(address);
|
|
8458
9209
|
const utxos = coins.map((utxo) => extendCoinWithTapscript(tapscript, utxo));
|
|
8459
9210
|
await this.walletRepository.saveUtxos(address, utxos);
|
|
8460
|
-
|
|
9211
|
+
groups.push({
|
|
9212
|
+
tapscript,
|
|
9213
|
+
// Normalize so the group key matches the axis/contract x-only
|
|
9214
|
+
// form regardless of how the tapscript's key was stored.
|
|
9215
|
+
serverPubKey: toXOnlySignerHex(hex.encode(tapscript.options.serverPubKey)),
|
|
9216
|
+
// Per-row CSV delay decoded from THIS tapscript's exit leaf —
|
|
9217
|
+
// not the wallet's current boarding timelock, which a signer
|
|
9218
|
+
// rotation may have changed.
|
|
9219
|
+
csvTimelock: CSVMultisigTapscript.decode(hex.decode(tapscript.exitScript)).params.timelock,
|
|
9220
|
+
coins: utxos
|
|
9221
|
+
});
|
|
8461
9222
|
}
|
|
8462
|
-
return
|
|
9223
|
+
return groups;
|
|
9224
|
+
}
|
|
9225
|
+
/**
|
|
9226
|
+
* Fetch and cache onchain inputs (UTXOs) received at the wallet's boarding
|
|
9227
|
+
* addresses — the current address plus any historical rotated boarding
|
|
9228
|
+
* addresses that still hold unspent UTXOs (plan §6-III.1). Each UTXO is
|
|
9229
|
+
* annotated with the tapscript of the address it actually sits on, so the
|
|
9230
|
+
* spending path forfeits / exits it with the correct per-index leaves.
|
|
9231
|
+
*
|
|
9232
|
+
* Current-signer only: a flatten of {@link getBoardingUtxosForSigners} over
|
|
9233
|
+
* the wallet's current signer, so the two paths cannot drift. Old-signer
|
|
9234
|
+
* boarding recovery goes through the deprecated-signer migration API
|
|
9235
|
+
* instead (it would otherwise pull EXPIRED-signer inputs into a plain
|
|
9236
|
+
* `settle()` that the server must reject).
|
|
9237
|
+
*/
|
|
9238
|
+
async getBoardingUtxos() {
|
|
9239
|
+
const currentOnly = /* @__PURE__ */ new Set([
|
|
9240
|
+
toXOnlySignerHex(hex.encode(this.boardingTapscript.options.serverPubKey))
|
|
9241
|
+
]);
|
|
9242
|
+
const groups = await this.getBoardingUtxosForSigners(currentOnly);
|
|
9243
|
+
return groups.flatMap((g) => g.coins);
|
|
8463
9244
|
}
|
|
8464
9245
|
/**
|
|
8465
9246
|
* Subscribe to onchain and offchain notifications for newly received funds.
|
|
@@ -8666,64 +9447,82 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8666
9447
|
watcherConfig: this.watcherConfig
|
|
8667
9448
|
});
|
|
8668
9449
|
const baselinePubkey = await this.identity.xOnlyPublicKey();
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
9450
|
+
const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
|
|
9451
|
+
const baselineSigners = [
|
|
9452
|
+
this.offchainTapscript.options.serverPubKey,
|
|
9453
|
+
...[...this._deprecatedSigners.keys()].map((h) => hex.decode(h))
|
|
9454
|
+
];
|
|
9455
|
+
const seenBaselineScripts = /* @__PURE__ */ new Set();
|
|
9456
|
+
for (const serverPubKey of baselineSigners) {
|
|
9457
|
+
for (const csvTimelock of this.walletContractTimelocks) {
|
|
9458
|
+
const csvTimelockStr = timelockToSequence(csvTimelock).toString();
|
|
9459
|
+
const defaultScript = new DefaultVtxo.Script({
|
|
9460
|
+
pubKey: baselinePubkey,
|
|
9461
|
+
serverPubKey,
|
|
9462
|
+
csvTimelock
|
|
9463
|
+
});
|
|
9464
|
+
const defaultScriptHex = hex.encode(defaultScript.pkScript);
|
|
9465
|
+
if (!seenBaselineScripts.has(defaultScriptHex)) {
|
|
9466
|
+
seenBaselineScripts.add(defaultScriptHex);
|
|
9467
|
+
await ensureWalletContract(manager, {
|
|
9468
|
+
type: "default",
|
|
9469
|
+
params: {
|
|
9470
|
+
pubKey: hex.encode(defaultScript.options.pubKey),
|
|
9471
|
+
serverPubKey: hex.encode(serverPubKey),
|
|
9472
|
+
csvTimelock: csvTimelockStr
|
|
9473
|
+
},
|
|
9474
|
+
script: defaultScriptHex,
|
|
9475
|
+
address: defaultScript.address(this.network.hrp, serverPubKey).encode(),
|
|
9476
|
+
state: "active"
|
|
9477
|
+
});
|
|
9478
|
+
}
|
|
9479
|
+
if (delegatePubKey) {
|
|
9480
|
+
const delegateScript = new DelegateVtxo.Script({
|
|
9481
|
+
pubKey: baselinePubkey,
|
|
9482
|
+
serverPubKey,
|
|
9483
|
+
delegatePubKey,
|
|
9484
|
+
csvTimelock
|
|
9485
|
+
});
|
|
9486
|
+
const delegateScriptHex = hex.encode(delegateScript.pkScript);
|
|
9487
|
+
if (seenBaselineScripts.has(delegateScriptHex)) continue;
|
|
9488
|
+
seenBaselineScripts.add(delegateScriptHex);
|
|
9489
|
+
await manager.createContract({
|
|
9490
|
+
type: "delegate",
|
|
9491
|
+
params: {
|
|
9492
|
+
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
9493
|
+
serverPubKey: hex.encode(serverPubKey),
|
|
9494
|
+
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
9495
|
+
csvTimelock: csvTimelockStr
|
|
9496
|
+
},
|
|
9497
|
+
script: delegateScriptHex,
|
|
9498
|
+
address: delegateScript.address(this.network.hrp, serverPubKey).encode(),
|
|
9499
|
+
state: "active"
|
|
9500
|
+
});
|
|
9501
|
+
}
|
|
9502
|
+
}
|
|
9503
|
+
}
|
|
9504
|
+
const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
9505
|
+
for (const serverPubKey of baselineSigners) {
|
|
9506
|
+
const baselineBoarding = new DefaultVtxo.Script({
|
|
8672
9507
|
pubKey: baselinePubkey,
|
|
8673
|
-
serverPubKey
|
|
8674
|
-
csvTimelock
|
|
9508
|
+
serverPubKey,
|
|
9509
|
+
csvTimelock: boardingCsvTimelock
|
|
8675
9510
|
});
|
|
8676
|
-
const
|
|
9511
|
+
const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
|
|
9512
|
+
if (seenBaselineScripts.has(boardingScriptHex)) continue;
|
|
9513
|
+
seenBaselineScripts.add(boardingScriptHex);
|
|
8677
9514
|
await ensureWalletContract(manager, {
|
|
8678
|
-
type: "
|
|
9515
|
+
type: "boarding",
|
|
8679
9516
|
params: {
|
|
8680
|
-
pubKey: hex.encode(
|
|
8681
|
-
serverPubKey: hex.encode(
|
|
8682
|
-
csvTimelock:
|
|
9517
|
+
pubKey: hex.encode(baselineBoarding.options.pubKey),
|
|
9518
|
+
serverPubKey: hex.encode(serverPubKey),
|
|
9519
|
+
csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
|
|
8683
9520
|
},
|
|
8684
|
-
script:
|
|
8685
|
-
address:
|
|
9521
|
+
script: boardingScriptHex,
|
|
9522
|
+
address: baselineBoarding.address(this.network.hrp, serverPubKey).encode(),
|
|
8686
9523
|
state: "active"
|
|
8687
9524
|
});
|
|
8688
|
-
if (this.offchainTapscript instanceof DelegateVtxo.Script) {
|
|
8689
|
-
const delegateScript = new DelegateVtxo.Script({
|
|
8690
|
-
pubKey: baselinePubkey,
|
|
8691
|
-
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
8692
|
-
delegatePubKey: this.offchainTapscript.options.delegatePubKey,
|
|
8693
|
-
csvTimelock
|
|
8694
|
-
});
|
|
8695
|
-
const delegateScriptHex = hex.encode(delegateScript.pkScript);
|
|
8696
|
-
await manager.createContract({
|
|
8697
|
-
type: "delegate",
|
|
8698
|
-
params: {
|
|
8699
|
-
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
8700
|
-
serverPubKey: hex.encode(delegateScript.options.serverPubKey),
|
|
8701
|
-
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
8702
|
-
csvTimelock: csvTimelockStr
|
|
8703
|
-
},
|
|
8704
|
-
script: delegateScriptHex,
|
|
8705
|
-
address: delegateScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
8706
|
-
state: "active"
|
|
8707
|
-
});
|
|
8708
|
-
}
|
|
8709
9525
|
}
|
|
8710
|
-
const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
8711
|
-
const baselineBoarding = new DefaultVtxo.Script({
|
|
8712
|
-
pubKey: baselinePubkey,
|
|
8713
|
-
serverPubKey: this.boardingTapscript.options.serverPubKey,
|
|
8714
|
-
csvTimelock: boardingCsvTimelock
|
|
8715
|
-
});
|
|
8716
|
-
await ensureWalletContract(manager, {
|
|
8717
|
-
type: "boarding",
|
|
8718
|
-
params: {
|
|
8719
|
-
pubKey: hex.encode(baselineBoarding.options.pubKey),
|
|
8720
|
-
serverPubKey: hex.encode(baselineBoarding.options.serverPubKey),
|
|
8721
|
-
csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
|
|
8722
|
-
},
|
|
8723
|
-
script: hex.encode(baselineBoarding.pkScript),
|
|
8724
|
-
address: baselineBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
8725
|
-
state: "active"
|
|
8726
|
-
});
|
|
8727
9526
|
return manager;
|
|
8728
9527
|
}
|
|
8729
9528
|
/** Dispose wallet-owned managers and release background resources. */
|
|
@@ -8756,7 +9555,6 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8756
9555
|
walletContractTimelocks
|
|
8757
9556
|
);
|
|
8758
9557
|
this.arkProvider = arkProvider;
|
|
8759
|
-
this.serverUnrollScript = serverUnrollScript;
|
|
8760
9558
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
8761
9559
|
this.forfeitPubkey = forfeitPubkey;
|
|
8762
9560
|
this.identity = identity;
|
|
@@ -8777,6 +9575,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8777
9575
|
this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
|
|
8778
9576
|
}
|
|
8779
9577
|
this._delegateManager = delegateProvider ? new DelegateManagerImpl(delegateProvider, arkProvider, identity) : void 0;
|
|
9578
|
+
this._serverUnrollScript = serverUnrollScript;
|
|
8780
9579
|
this._receiveRotator = receiveRotator;
|
|
8781
9580
|
this._descriptorProvider = descriptorProvider;
|
|
8782
9581
|
this._signerRouter = new InputSignerRouter({
|
|
@@ -8802,6 +9601,43 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8802
9601
|
* the contract manager is up first.
|
|
8803
9602
|
*/
|
|
8804
9603
|
_receiveRotator;
|
|
9604
|
+
/**
|
|
9605
|
+
* Unsubscribe handle for the arkProvider's `onServerInfoChanged` stream
|
|
9606
|
+
* (mid-session signer-rotation detection). Torn down in {@link dispose}.
|
|
9607
|
+
*/
|
|
9608
|
+
_serverInfoUnsub;
|
|
9609
|
+
/**
|
|
9610
|
+
* Tail of the serialized {@link handleServerInfoChanged} chain. Each
|
|
9611
|
+
* `onServerInfoChanged` event chains onto it so handlers run one at a time,
|
|
9612
|
+
* and {@link dispose} awaits it so an in-flight re-derive/rotation settles
|
|
9613
|
+
* before the contract manager is torn down underneath it.
|
|
9614
|
+
*/
|
|
9615
|
+
_serverInfoInFlight = Promise.resolve();
|
|
9616
|
+
/**
|
|
9617
|
+
* React to a mid-session server-info change (driven by the arkProvider's
|
|
9618
|
+
* `DIGEST_MISMATCH` detection). First refresh the cached deprecated-signer
|
|
9619
|
+
* set so the boarding WATCH path immediately widens to the just-deprecated
|
|
9620
|
+
* signer, then — only if the active signer actually changed — rotate the
|
|
9621
|
+
* wallet onto it via {@link rotateServerSigner} (re-deriving the offchain +
|
|
9622
|
+
* boarding display tapscripts and registering the current-signer rows).
|
|
9623
|
+
* Old-signer rows stay active, so existing funds remain watched. Failures
|
|
9624
|
+
* are logged, never thrown back into the provider's emit loop.
|
|
9625
|
+
*/
|
|
9626
|
+
async handleServerInfoChanged(info) {
|
|
9627
|
+
this.refreshDeprecatedSigners(info);
|
|
9628
|
+
try {
|
|
9629
|
+
const newActive = toXOnlySignerHex(info.signerPubkey);
|
|
9630
|
+
const current = toXOnlySignerHex(hex.encode(this.arkServerPublicKey));
|
|
9631
|
+
if (newActive !== current) {
|
|
9632
|
+
await this.rotateServerSigner(
|
|
9633
|
+
hex.decode(info.signerPubkey),
|
|
9634
|
+
info.checkpointTapscript
|
|
9635
|
+
);
|
|
9636
|
+
}
|
|
9637
|
+
} catch (e) {
|
|
9638
|
+
console.warn("server-signer rotation on info change failed", e);
|
|
9639
|
+
}
|
|
9640
|
+
}
|
|
8805
9641
|
_receiveRotatorInstalled = false;
|
|
8806
9642
|
/**
|
|
8807
9643
|
* Descriptor-aware signer used by {@link _signerRouter} to sign
|
|
@@ -8831,6 +9667,44 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8831
9667
|
this._boardingTapscript = tapscript;
|
|
8832
9668
|
this.notifyBoardingRotation();
|
|
8833
9669
|
}
|
|
9670
|
+
/**
|
|
9671
|
+
* @internal Sole write path for `arkServerPublicKey` after construction.
|
|
9672
|
+
* Called by {@link Wallet.rotateServerSigner} once the rotated offchain and
|
|
9673
|
+
* boarding contract rows have been persisted. External code must treat
|
|
9674
|
+
* `arkServerPublicKey` as read-only.
|
|
9675
|
+
*/
|
|
9676
|
+
setArkServerPublicKeyForRotation(serverPubKey) {
|
|
9677
|
+
this._arkServerPublicKey = serverPubKey;
|
|
9678
|
+
}
|
|
9679
|
+
/**
|
|
9680
|
+
* Output script for checkpoint transactions, decoded from the server's
|
|
9681
|
+
* `checkpointTapscript`. Server-controlled state: pinned at construction
|
|
9682
|
+
* and re-sourced from a fresh `ArkInfo` on server-signer rotation. Read it
|
|
9683
|
+
* through {@link serverUnrollScript}; write it only through
|
|
9684
|
+
* {@link setServerUnrollScriptForRotation}.
|
|
9685
|
+
*/
|
|
9686
|
+
_serverUnrollScript;
|
|
9687
|
+
get serverUnrollScript() {
|
|
9688
|
+
return this._serverUnrollScript;
|
|
9689
|
+
}
|
|
9690
|
+
/**
|
|
9691
|
+
* @internal Sole write path for `serverUnrollScript` after construction.
|
|
9692
|
+
* Called by {@link Wallet._doRotateServerSigner} with the checkpoint script
|
|
9693
|
+
* sourced from the fresh `ArkInfo` that triggered the rotation, so the send
|
|
9694
|
+
* path builds checkpoints against the new server epoch. External code must
|
|
9695
|
+
* treat `serverUnrollScript` as read-only.
|
|
9696
|
+
*/
|
|
9697
|
+
setServerUnrollScriptForRotation(script) {
|
|
9698
|
+
this._serverUnrollScript = script;
|
|
9699
|
+
}
|
|
9700
|
+
/**
|
|
9701
|
+
* Serializes {@link rotateServerSigner} for static / non-HD wallets (which
|
|
9702
|
+
* have no {@link WalletReceiveRotator} chain to ride). Coalesces concurrent
|
|
9703
|
+
* migration passes so two callers cannot both rebuild and swap the
|
|
9704
|
+
* tapscripts. HD wallets serialize on the rotator's chain instead, via
|
|
9705
|
+
* {@link WalletReceiveRotator.runExclusive}.
|
|
9706
|
+
*/
|
|
9707
|
+
_serverRotationChain = Promise.resolve();
|
|
8834
9708
|
/**
|
|
8835
9709
|
* Allocate and return a *fresh* on-chain boarding address, rotating the
|
|
8836
9710
|
* wallet's current boarding tapscript to a new HD index.
|
|
@@ -8887,6 +9761,126 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8887
9761
|
this.setBoardingTapscriptForRotation(newBoarding);
|
|
8888
9762
|
return newBoarding.onchainAddress(this.network);
|
|
8889
9763
|
}
|
|
9764
|
+
/**
|
|
9765
|
+
* Mid-session server-signer rotation (plan §4). When arkd rotates its
|
|
9766
|
+
* active signer mid-session — the case the long-lived service worker and
|
|
9767
|
+
* Expo background processes that own automatic migration must handle — a
|
|
9768
|
+
* wallet constructed before the rotation keeps deriving old-signer receive
|
|
9769
|
+
* addresses. Building a migration output to such an address would produce a
|
|
9770
|
+
* VTXO the server must reject, so the wallet must first re-derive its own
|
|
9771
|
+
* receive state under the new active signer.
|
|
9772
|
+
*
|
|
9773
|
+
* Follows the {@link WalletReceiveRotator.rotate} write-path pattern with
|
|
9774
|
+
* the server key swapped instead of the user key: build the new offchain
|
|
9775
|
+
* and boarding tapscripts locally (preserving every other option),
|
|
9776
|
+
* register the matching `default`/`delegate` and `boarding` contract rows
|
|
9777
|
+
* through {@link ContractManager.createContract}, and only then commit the
|
|
9778
|
+
* new tapscripts and server key to the wallet's visible state. The signing
|
|
9779
|
+
* metadata of the current receive/boarding rows is carried onto the new
|
|
9780
|
+
* rows so a rotated (descriptor-backed) receive pubkey can still sign.
|
|
9781
|
+
*
|
|
9782
|
+
* The old-signer contract rows are intentionally left `active` and watched
|
|
9783
|
+
* — they are exactly the deprecated-signer contracts the migration pass
|
|
9784
|
+
* drains. Idempotent: a no-op when the wallet already tracks `xonly`.
|
|
9785
|
+
*
|
|
9786
|
+
* Serialized against HD receive rotation so the two paths (both of which
|
|
9787
|
+
* rebuild and swap `offchainTapscript`) cannot interleave.
|
|
9788
|
+
*
|
|
9789
|
+
* @internal Invoked by the {@link VtxoManager} migration pass; not part of
|
|
9790
|
+
* the stable public API.
|
|
9791
|
+
*/
|
|
9792
|
+
async rotateServerSigner(newServerPubKey, checkpointTapscript) {
|
|
9793
|
+
const xonly = toXOnlyPubKey(newServerPubKey);
|
|
9794
|
+
let newServerUnrollScript;
|
|
9795
|
+
try {
|
|
9796
|
+
newServerUnrollScript = CSVMultisigTapscript.decode(hex.decode(checkpointTapscript));
|
|
9797
|
+
} catch (e) {
|
|
9798
|
+
throw new Error("Invalid checkpointTapscript from server");
|
|
9799
|
+
}
|
|
9800
|
+
if (equalBytes$1(xonly, this.arkServerPublicKey)) return;
|
|
9801
|
+
if (this._receiveRotator) {
|
|
9802
|
+
await this._receiveRotator.runExclusive(
|
|
9803
|
+
() => this._doRotateServerSigner(xonly, newServerUnrollScript)
|
|
9804
|
+
);
|
|
9805
|
+
return;
|
|
9806
|
+
}
|
|
9807
|
+
const run = this._serverRotationChain.catch(() => void 0).then(() => this._doRotateServerSigner(xonly, newServerUnrollScript));
|
|
9808
|
+
this._serverRotationChain = run.then(
|
|
9809
|
+
() => void 0,
|
|
9810
|
+
() => void 0
|
|
9811
|
+
);
|
|
9812
|
+
return run;
|
|
9813
|
+
}
|
|
9814
|
+
async _doRotateServerSigner(xonly, newServerUnrollScript) {
|
|
9815
|
+
if (equalBytes$1(xonly, this.arkServerPublicKey)) return;
|
|
9816
|
+
const manager = await this.getContractManager();
|
|
9817
|
+
const [currentOffchainRow] = await manager.getContracts({
|
|
9818
|
+
script: this.defaultContractScript
|
|
9819
|
+
});
|
|
9820
|
+
const currentBoardingScript = hex.encode(this._boardingTapscript.pkScript);
|
|
9821
|
+
const [currentBoardingRow] = await manager.getContracts({
|
|
9822
|
+
script: currentBoardingScript
|
|
9823
|
+
});
|
|
9824
|
+
const newOffchain = this.offchainTapscript instanceof DelegateVtxo.Script ? new DelegateVtxo.Script({
|
|
9825
|
+
...this.offchainTapscript.options,
|
|
9826
|
+
serverPubKey: xonly
|
|
9827
|
+
}) : new DefaultVtxo.Script({
|
|
9828
|
+
...this.offchainTapscript.options,
|
|
9829
|
+
serverPubKey: xonly
|
|
9830
|
+
});
|
|
9831
|
+
const newBoarding = new DefaultVtxo.Script({
|
|
9832
|
+
...this._boardingTapscript.options,
|
|
9833
|
+
serverPubKey: xonly
|
|
9834
|
+
});
|
|
9835
|
+
const offchainCsv = timelockToSequence(newOffchain.options.csvTimelock).toString();
|
|
9836
|
+
const newOffchainScript = hex.encode(newOffchain.pkScript);
|
|
9837
|
+
const newOffchainAddress = newOffchain.address(this.network.hrp, xonly).encode();
|
|
9838
|
+
if (newOffchain instanceof DelegateVtxo.Script) {
|
|
9839
|
+
await manager.createContract({
|
|
9840
|
+
type: "delegate",
|
|
9841
|
+
params: {
|
|
9842
|
+
pubKey: hex.encode(newOffchain.options.pubKey),
|
|
9843
|
+
serverPubKey: hex.encode(xonly),
|
|
9844
|
+
delegatePubKey: hex.encode(newOffchain.options.delegatePubKey),
|
|
9845
|
+
csvTimelock: offchainCsv
|
|
9846
|
+
},
|
|
9847
|
+
script: newOffchainScript,
|
|
9848
|
+
address: newOffchainAddress,
|
|
9849
|
+
state: "active",
|
|
9850
|
+
metadata: currentOffchainRow?.metadata
|
|
9851
|
+
});
|
|
9852
|
+
} else {
|
|
9853
|
+
await manager.createContract({
|
|
9854
|
+
type: "default",
|
|
9855
|
+
params: {
|
|
9856
|
+
pubKey: hex.encode(newOffchain.options.pubKey),
|
|
9857
|
+
serverPubKey: hex.encode(xonly),
|
|
9858
|
+
csvTimelock: offchainCsv
|
|
9859
|
+
},
|
|
9860
|
+
script: newOffchainScript,
|
|
9861
|
+
address: newOffchainAddress,
|
|
9862
|
+
state: "active",
|
|
9863
|
+
metadata: currentOffchainRow?.metadata
|
|
9864
|
+
});
|
|
9865
|
+
}
|
|
9866
|
+
const boardingCsv = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
9867
|
+
await manager.createContract({
|
|
9868
|
+
type: "boarding",
|
|
9869
|
+
params: {
|
|
9870
|
+
pubKey: hex.encode(newBoarding.options.pubKey),
|
|
9871
|
+
serverPubKey: hex.encode(xonly),
|
|
9872
|
+
csvTimelock: timelockToSequence(boardingCsv).toString()
|
|
9873
|
+
},
|
|
9874
|
+
script: hex.encode(newBoarding.pkScript),
|
|
9875
|
+
address: newBoarding.address(this.network.hrp, xonly).encode(),
|
|
9876
|
+
state: "active",
|
|
9877
|
+
metadata: currentBoardingRow?.metadata
|
|
9878
|
+
});
|
|
9879
|
+
this.setOffchainTapscriptForRotation(newOffchain);
|
|
9880
|
+
this.setBoardingTapscriptForRotation(newBoarding);
|
|
9881
|
+
this.setArkServerPublicKeyForRotation(xonly);
|
|
9882
|
+
this.setServerUnrollScriptForRotation(newServerUnrollScript);
|
|
9883
|
+
}
|
|
8890
9884
|
/**
|
|
8891
9885
|
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
8892
9886
|
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
@@ -9047,6 +10041,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9047
10041
|
}
|
|
9048
10042
|
async dispose() {
|
|
9049
10043
|
await this._restoreInFlight?.catch(() => void 0);
|
|
10044
|
+
this._serverInfoUnsub?.();
|
|
10045
|
+
this._serverInfoUnsub = void 0;
|
|
10046
|
+
await this._serverInfoInFlight?.catch(() => void 0);
|
|
9050
10047
|
let rotatorError;
|
|
9051
10048
|
try {
|
|
9052
10049
|
await this._receiveRotator?.dispose();
|
|
@@ -9121,6 +10118,15 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9121
10118
|
boot?.rotator,
|
|
9122
10119
|
boot?.provider
|
|
9123
10120
|
);
|
|
10121
|
+
wallet.refreshDeprecatedSigners(setup.info);
|
|
10122
|
+
{
|
|
10123
|
+
const ap = setup.arkProvider;
|
|
10124
|
+
if (typeof ap.onServerInfoChanged === "function") {
|
|
10125
|
+
wallet._serverInfoUnsub = ap.onServerInfoChanged((info) => {
|
|
10126
|
+
wallet._serverInfoInFlight = wallet._serverInfoInFlight.then(() => wallet.handleServerInfoChanged(info)).catch(() => void 0);
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
}
|
|
9124
10130
|
if (boot?.provider) {
|
|
9125
10131
|
const resolvedBoarding = await resolveBoardingBootTapscript(
|
|
9126
10132
|
setup.contractRepository,
|
|
@@ -9153,7 +10159,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9153
10159
|
*/
|
|
9154
10160
|
async toReadonly() {
|
|
9155
10161
|
const readonlyIdentity = hasToReadonly(this.identity) ? await this.identity.toReadonly() : this.identity;
|
|
9156
|
-
|
|
10162
|
+
const readonly = new ReadonlyWallet(
|
|
9157
10163
|
readonlyIdentity,
|
|
9158
10164
|
this.network,
|
|
9159
10165
|
this.onchainProvider,
|
|
@@ -9168,6 +10174,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9168
10174
|
this.watcherConfig,
|
|
9169
10175
|
this.walletContractTimelocks
|
|
9170
10176
|
);
|
|
10177
|
+
readonly._deprecatedSigners = new Map(this._deprecatedSigners);
|
|
10178
|
+
return readonly;
|
|
9171
10179
|
}
|
|
9172
10180
|
/** Returns the delegate manager when delegation support is configured. */
|
|
9173
10181
|
async getDelegateManager() {
|
|
@@ -9193,10 +10201,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9193
10201
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
9194
10202
|
return this._withTxLock(async () => {
|
|
9195
10203
|
const offchainTapscript = this.offchainTapscript;
|
|
9196
|
-
const
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
);
|
|
10204
|
+
const serverPubKey = this.arkServerPublicKey;
|
|
10205
|
+
const serverUnrollScript = this.serverUnrollScript;
|
|
10206
|
+
const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
|
|
9200
10207
|
const selectedVtxoSum = params.selectedVtxos.map((v) => v.value).reduce((a, b) => a + b, 0);
|
|
9201
10208
|
if (selectedVtxoSum < params.amount) {
|
|
9202
10209
|
throw new Error("Selected VTXOs do not cover specified amount");
|
|
@@ -9221,25 +10228,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9221
10228
|
amount: BigInt(selected.changeAmount)
|
|
9222
10229
|
});
|
|
9223
10230
|
}
|
|
9224
|
-
this.
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
arkTxid,
|
|
9233
|
-
signedCheckpointTxs,
|
|
9234
|
-
params.amount,
|
|
9235
|
-
selected.changeAmount,
|
|
9236
|
-
selected.changeAmount > 0n ? outputs.length - 1 : 0,
|
|
9237
|
-
offchainTapscript
|
|
9238
|
-
);
|
|
9239
|
-
return arkTxid;
|
|
9240
|
-
} finally {
|
|
9241
|
-
this._removePendingSpends(selected.inputs);
|
|
9242
|
-
}
|
|
10231
|
+
return this._submitOffchainSpend(selected.inputs, outputs, {
|
|
10232
|
+
sentAmount: params.amount,
|
|
10233
|
+
changeAmount: selected.changeAmount,
|
|
10234
|
+
changeVout: selected.changeAmount > 0n ? outputs.length - 1 : 0,
|
|
10235
|
+
offchainTapscript,
|
|
10236
|
+
serverPubKey,
|
|
10237
|
+
serverUnrollScript
|
|
10238
|
+
});
|
|
9243
10239
|
});
|
|
9244
10240
|
}
|
|
9245
10241
|
return this.send({
|
|
@@ -9269,8 +10265,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9269
10265
|
}
|
|
9270
10266
|
}
|
|
9271
10267
|
}
|
|
10268
|
+
const offchainAddress = await this.getAddress();
|
|
10269
|
+
const offchainPkScript = ArkAddress.decode(offchainAddress).pkScript;
|
|
10270
|
+
const offchainOutputScript = hex.encode(offchainPkScript);
|
|
9272
10271
|
if (!params) {
|
|
9273
|
-
const { fees } = await this.arkProvider.getInfo();
|
|
10272
|
+
const { fees, vtxoMaxAmount } = await this.arkProvider.getInfo();
|
|
9274
10273
|
const estimator = new Estimator(fees.intentFee);
|
|
9275
10274
|
let amount = 0;
|
|
9276
10275
|
const exitScript = CSVMultisigTapscript.decode(
|
|
@@ -9312,20 +10311,31 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9312
10311
|
if (inputFee.satoshis >= vtxo.value) {
|
|
9313
10312
|
continue;
|
|
9314
10313
|
}
|
|
10314
|
+
const net = vtxo.value - inputFee.satoshis;
|
|
10315
|
+
if (vtxoMaxAmount >= 0n) {
|
|
10316
|
+
const projectedAmount = BigInt(amount + net);
|
|
10317
|
+
const projectedOutputFee = estimator.evalOffchainOutput({
|
|
10318
|
+
amount: projectedAmount,
|
|
10319
|
+
script: offchainOutputScript
|
|
10320
|
+
});
|
|
10321
|
+
if (projectedAmount - BigInt(projectedOutputFee.satoshis) > vtxoMaxAmount) {
|
|
10322
|
+
continue;
|
|
10323
|
+
}
|
|
10324
|
+
}
|
|
9315
10325
|
filteredVtxos.push(vtxo);
|
|
9316
|
-
amount +=
|
|
10326
|
+
amount += net;
|
|
9317
10327
|
}
|
|
9318
10328
|
const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
|
|
9319
10329
|
if (inputs.length === 0) {
|
|
9320
10330
|
throw new Error("No inputs found");
|
|
9321
10331
|
}
|
|
9322
10332
|
const output = {
|
|
9323
|
-
address:
|
|
10333
|
+
address: offchainAddress,
|
|
9324
10334
|
amount: BigInt(amount)
|
|
9325
10335
|
};
|
|
9326
10336
|
const outputFee = estimator.evalOffchainOutput({
|
|
9327
10337
|
amount: output.amount,
|
|
9328
|
-
script:
|
|
10338
|
+
script: offchainOutputScript
|
|
9329
10339
|
});
|
|
9330
10340
|
output.amount -= BigInt(outputFee.satoshis);
|
|
9331
10341
|
if (output.amount <= this.dustAmount) {
|
|
@@ -9365,8 +10375,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9365
10375
|
}
|
|
9366
10376
|
}
|
|
9367
10377
|
let outputAssets;
|
|
9368
|
-
const
|
|
9369
|
-
const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
|
|
10378
|
+
const assetOutputIndex = findDestinationOutputIndex(outputs, offchainPkScript);
|
|
9370
10379
|
if (assetInputs.size > 0) {
|
|
9371
10380
|
if (assetOutputIndex === -1) {
|
|
9372
10381
|
throw new Error("Cannot assign assets: no output matches the destination address");
|
|
@@ -9905,12 +10914,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9905
10914
|
throw new Error("At least one receiver is required");
|
|
9906
10915
|
}
|
|
9907
10916
|
const offchainTapscript = this.offchainTapscript;
|
|
9908
|
-
const
|
|
10917
|
+
const serverPubKey = this.arkServerPublicKey;
|
|
10918
|
+
const serverUnrollScript = this.serverUnrollScript;
|
|
10919
|
+
const outputAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
|
|
9909
10920
|
const address = outputAddress.encode();
|
|
9910
10921
|
const recipients = validateRecipients(args, Number(this.dustAmount));
|
|
9911
|
-
const
|
|
10922
|
+
const allVirtualCoins = await this.getVtxos({
|
|
9912
10923
|
withRecoverable: false
|
|
9913
10924
|
});
|
|
10925
|
+
const pendingRecovery = await this.pendingRecoveryOutpoints();
|
|
10926
|
+
const virtualCoins = pendingRecovery.size ? allVirtualCoins.filter((c) => !pendingRecovery.has(`${c.txid}:${c.vout}`)) : allVirtualCoins;
|
|
9914
10927
|
const assetChanges = /* @__PURE__ */ new Map();
|
|
9915
10928
|
let selectedCoins = [];
|
|
9916
10929
|
let btcAmountToSelect = 0;
|
|
@@ -10032,33 +11045,128 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10032
11045
|
outputs.push(Extension.create([assetPacket]).txOut());
|
|
10033
11046
|
}
|
|
10034
11047
|
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
10035
|
-
this.
|
|
11048
|
+
return this._submitOffchainSpend(selectedCoins, outputs, {
|
|
11049
|
+
sentAmount,
|
|
11050
|
+
changeAmount: BigInt(changeAmount),
|
|
11051
|
+
changeVout: changeReceiver ? changeIndex : 0,
|
|
11052
|
+
offchainTapscript,
|
|
11053
|
+
serverPubKey,
|
|
11054
|
+
serverUnrollScript,
|
|
11055
|
+
changeAssets: changeReceiver?.assets
|
|
11056
|
+
});
|
|
11057
|
+
}
|
|
11058
|
+
/**
|
|
11059
|
+
* Shared tail of every Ark-transaction spend path (`send`, selected-VTXO
|
|
11060
|
+
* `sendBitcoin`, and {@link sendSelectedVtxosToSelf}): hide the inputs from
|
|
11061
|
+
* concurrent `getVtxos()`, build+submit the offchain tx, persist the spent
|
|
11062
|
+
* inputs and any wallet-owned (change / self) output, then release the
|
|
11063
|
+
* pending-spend hold. Callers own coin selection, output construction, and
|
|
11064
|
+
* the synchronous epoch snapshot; this owns the submit/persist sequence.
|
|
11065
|
+
*/
|
|
11066
|
+
async _submitOffchainSpend(inputs, outputs, persist) {
|
|
11067
|
+
this._addPendingSpends(inputs);
|
|
10036
11068
|
try {
|
|
10037
11069
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
|
|
10038
|
-
|
|
10039
|
-
outputs
|
|
11070
|
+
inputs,
|
|
11071
|
+
outputs,
|
|
11072
|
+
persist.serverUnrollScript
|
|
10040
11073
|
);
|
|
10041
11074
|
await this.updateDbAfterOffchainTx(
|
|
10042
|
-
|
|
11075
|
+
inputs,
|
|
10043
11076
|
arkTxid,
|
|
10044
11077
|
signedCheckpointTxs,
|
|
10045
|
-
sentAmount,
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
offchainTapscript,
|
|
10049
|
-
|
|
11078
|
+
persist.sentAmount,
|
|
11079
|
+
persist.changeAmount,
|
|
11080
|
+
persist.changeVout,
|
|
11081
|
+
persist.offchainTapscript,
|
|
11082
|
+
persist.serverPubKey,
|
|
11083
|
+
persist.changeAssets,
|
|
11084
|
+
persist.recordSentHistory ?? true
|
|
10050
11085
|
);
|
|
10051
11086
|
return arkTxid;
|
|
10052
11087
|
} finally {
|
|
10053
|
-
this._removePendingSpends(
|
|
10054
|
-
}
|
|
11088
|
+
this._removePendingSpends(inputs);
|
|
11089
|
+
}
|
|
11090
|
+
}
|
|
11091
|
+
/**
|
|
11092
|
+
* @internal Migration primitive (deprecated-signer plan, step 1). Spend an
|
|
11093
|
+
* explicit set of the wallet's own deprecated-signer VTXOs into a single
|
|
11094
|
+
* full-value output on the wallet's *active* signer, through the Ark send
|
|
11095
|
+
* path (not `settle`) so arkd builds checkpoints against the active server
|
|
11096
|
+
* epoch. Consumed in-process by {@link VtxoManager}'s migration pass; not
|
|
11097
|
+
* part of the public `IWallet` API and never accepts boarding `ExtendedCoin`
|
|
11098
|
+
* inputs.
|
|
11099
|
+
*
|
|
11100
|
+
* The caller (`migrateCore`) must have already moved the wallet onto the
|
|
11101
|
+
* active signer (`ensureReceiveOnActiveSigner`) and sized the batch (caps +
|
|
11102
|
+
* dust floor); this method validates the inputs, preserves all input assets
|
|
11103
|
+
* on the self output, and persists the new active-signer VTXO even though
|
|
11104
|
+
* there is no separate change output. It records no `TxSent` history — the
|
|
11105
|
+
* funds never leave the wallet.
|
|
11106
|
+
*/
|
|
11107
|
+
async sendSelectedVtxosToSelf(inputs) {
|
|
11108
|
+
if (inputs.length === 0) {
|
|
11109
|
+
throw new Error("sendSelectedVtxosToSelf: no inputs");
|
|
11110
|
+
}
|
|
11111
|
+
return this._withTxLock(async () => {
|
|
11112
|
+
const offchainTapscript = this.offchainTapscript;
|
|
11113
|
+
const serverPubKey = this.arkServerPublicKey;
|
|
11114
|
+
const serverUnrollScript = this.serverUnrollScript;
|
|
11115
|
+
const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
|
|
11116
|
+
for (const input of inputs) {
|
|
11117
|
+
if (!isSpendable(input) || isRecoverable(input)) {
|
|
11118
|
+
throw new Error(
|
|
11119
|
+
`sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} is not cooperatively spendable`
|
|
11120
|
+
);
|
|
11121
|
+
}
|
|
11122
|
+
if (!input.virtualStatus.batchExpiry) {
|
|
11123
|
+
throw new Error(
|
|
11124
|
+
`sendSelectedVtxosToSelf: input ${input.txid}:${input.vout} has no batchExpiry`
|
|
11125
|
+
);
|
|
11126
|
+
}
|
|
11127
|
+
}
|
|
11128
|
+
const total = inputs.reduce((sum, c) => sum + BigInt(c.value), 0n);
|
|
11129
|
+
const outputs = [
|
|
11130
|
+
{
|
|
11131
|
+
script: total < this.dustAmount ? arkAddress.subdustPkScript : arkAddress.pkScript,
|
|
11132
|
+
amount: total
|
|
11133
|
+
}
|
|
11134
|
+
];
|
|
11135
|
+
const assetInputs = selectedCoinsToAssetInputs(inputs);
|
|
11136
|
+
let selfAssets;
|
|
11137
|
+
if (assetInputs.size > 0) {
|
|
11138
|
+
const totals = /* @__PURE__ */ new Map();
|
|
11139
|
+
for (const [, assets] of assetInputs) {
|
|
11140
|
+
for (const a of assets) {
|
|
11141
|
+
totals.set(a.assetId, (totals.get(a.assetId) ?? 0n) + a.amount);
|
|
11142
|
+
}
|
|
11143
|
+
}
|
|
11144
|
+
selfAssets = [...totals].map(([assetId, amount]) => ({ assetId, amount }));
|
|
11145
|
+
const selfReceiver = {
|
|
11146
|
+
address: arkAddress.encode(),
|
|
11147
|
+
assets: selfAssets
|
|
11148
|
+
};
|
|
11149
|
+
const packet = createAssetPacket(assetInputs, [], selfReceiver);
|
|
11150
|
+
outputs.push(Extension.create([packet]).txOut());
|
|
11151
|
+
}
|
|
11152
|
+
return this._submitOffchainSpend(inputs, outputs, {
|
|
11153
|
+
sentAmount: 0,
|
|
11154
|
+
changeAmount: total,
|
|
11155
|
+
changeVout: 0,
|
|
11156
|
+
offchainTapscript,
|
|
11157
|
+
serverPubKey,
|
|
11158
|
+
changeAssets: selfAssets,
|
|
11159
|
+
recordSentHistory: false,
|
|
11160
|
+
serverUnrollScript
|
|
11161
|
+
});
|
|
11162
|
+
});
|
|
10055
11163
|
}
|
|
10056
11164
|
/**
|
|
10057
11165
|
* Build an offchain transaction from the given inputs and outputs,
|
|
10058
11166
|
* sign it, submit to the Arkade provider, and finalize.
|
|
10059
11167
|
* @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
|
|
10060
11168
|
*/
|
|
10061
|
-
async buildAndSubmitOffchainTx(inputs, outputs) {
|
|
11169
|
+
async buildAndSubmitOffchainTx(inputs, outputs, serverUnrollScript = this.serverUnrollScript) {
|
|
10062
11170
|
const offchainTx = buildOffchainTx(
|
|
10063
11171
|
inputs.map((input) => {
|
|
10064
11172
|
return {
|
|
@@ -10067,7 +11175,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10067
11175
|
};
|
|
10068
11176
|
}),
|
|
10069
11177
|
outputs,
|
|
10070
|
-
|
|
11178
|
+
serverUnrollScript
|
|
10071
11179
|
);
|
|
10072
11180
|
const arkTxJobs = inputs.map((input, index) => ({
|
|
10073
11181
|
index,
|
|
@@ -10141,14 +11249,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10141
11249
|
return { arkTxid, signedCheckpointTxs };
|
|
10142
11250
|
}
|
|
10143
11251
|
// mark virtual outputs as spent, save change outputs if any.
|
|
10144
|
-
// `offchainTapscript`
|
|
10145
|
-
// `_txLock` before any `await`; deriving both the
|
|
10146
|
-
// metadata and `primaryAddress` from
|
|
10147
|
-
// record matches the pkScript the server saw on the inbound
|
|
10148
|
-
// transaction, even if `
|
|
10149
|
-
// `this.
|
|
10150
|
-
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
|
|
10151
|
-
const primaryAddress = offchainTapscript.address(this.network.hrp,
|
|
11252
|
+
// `offchainTapscript` and `serverPubKey` are the epoch snapshot the
|
|
11253
|
+
// caller captured under `_txLock` before any `await`; deriving both the
|
|
11254
|
+
// change-VTXO metadata and `primaryAddress` from them here guarantees the
|
|
11255
|
+
// local record matches the address/pkScript the server saw on the inbound
|
|
11256
|
+
// transaction, even if `rotateServerSigner` swaps `this.offchainTapscript`
|
|
11257
|
+
// / `this.arkServerPublicKey` mid-flight.
|
|
11258
|
+
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, serverPubKey, changeAssets, recordSentHistory = true) {
|
|
11259
|
+
const primaryAddress = offchainTapscript.address(this.network.hrp, serverPubKey).encode();
|
|
10152
11260
|
try {
|
|
10153
11261
|
const spentVtxos = [];
|
|
10154
11262
|
const commitmentTxIds = /* @__PURE__ */ new Set();
|
|
@@ -10255,19 +11363,21 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10255
11363
|
[changeVtxo]
|
|
10256
11364
|
);
|
|
10257
11365
|
}
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
11366
|
+
if (recordSentHistory) {
|
|
11367
|
+
await this.walletRepository.saveTransactions(primaryAddress, [
|
|
11368
|
+
{
|
|
11369
|
+
key: {
|
|
11370
|
+
boardingTxid: "",
|
|
11371
|
+
commitmentTxid: "",
|
|
11372
|
+
arkTxid
|
|
11373
|
+
},
|
|
11374
|
+
amount: sentAmount,
|
|
11375
|
+
type: "SENT" /* TxSent */,
|
|
11376
|
+
settled: false,
|
|
11377
|
+
createdAt
|
|
11378
|
+
}
|
|
11379
|
+
]);
|
|
11380
|
+
}
|
|
10271
11381
|
} catch (e) {
|
|
10272
11382
|
console.warn("error saving offchain tx to repository", e);
|
|
10273
11383
|
throw e;
|
|
@@ -11125,6 +12235,82 @@ var DelegateNotConfiguredError = class extends Error {
|
|
|
11125
12235
|
};
|
|
11126
12236
|
var DelegatorNotConfiguredError = DelegateNotConfiguredError;
|
|
11127
12237
|
var DEFAULT_MESSAGE_TAG = "WALLET_UPDATER";
|
|
12238
|
+
var serializeMigrationVtxoRef = (ref) => ({
|
|
12239
|
+
txid: ref.txid,
|
|
12240
|
+
vout: ref.vout,
|
|
12241
|
+
value: ref.value,
|
|
12242
|
+
signerPubKey: ref.signerPubKey,
|
|
12243
|
+
cutoffDate: ref.cutoffDate?.toString()
|
|
12244
|
+
});
|
|
12245
|
+
var deserializeMigrationVtxoRef = (ref) => ({
|
|
12246
|
+
txid: ref.txid,
|
|
12247
|
+
vout: ref.vout,
|
|
12248
|
+
value: ref.value,
|
|
12249
|
+
signerPubKey: ref.signerPubKey,
|
|
12250
|
+
cutoffDate: ref.cutoffDate != null ? BigInt(ref.cutoffDate) : void 0
|
|
12251
|
+
});
|
|
12252
|
+
var serializeDeprecatedSignerReport = (report) => ({
|
|
12253
|
+
signerPubKey: report.signerPubKey,
|
|
12254
|
+
status: report.status,
|
|
12255
|
+
cutoffDate: report.cutoffDate?.toString(),
|
|
12256
|
+
secondsUntilCutoff: report.secondsUntilCutoff,
|
|
12257
|
+
vtxoCount: report.vtxoCount,
|
|
12258
|
+
totalValue: report.totalValue,
|
|
12259
|
+
boardingCount: report.boardingCount,
|
|
12260
|
+
boardingValue: report.boardingValue,
|
|
12261
|
+
recoverableCount: report.recoverableCount,
|
|
12262
|
+
recoverableValue: report.recoverableValue,
|
|
12263
|
+
awaitingSweepCount: report.awaitingSweepCount,
|
|
12264
|
+
awaitingSweepValue: report.awaitingSweepValue,
|
|
12265
|
+
nextSweepEta: report.nextSweepEta
|
|
12266
|
+
});
|
|
12267
|
+
var deserializeDeprecatedSignerReport = (report) => ({
|
|
12268
|
+
signerPubKey: report.signerPubKey,
|
|
12269
|
+
status: report.status,
|
|
12270
|
+
cutoffDate: report.cutoffDate != null ? BigInt(report.cutoffDate) : void 0,
|
|
12271
|
+
secondsUntilCutoff: report.secondsUntilCutoff,
|
|
12272
|
+
vtxoCount: report.vtxoCount,
|
|
12273
|
+
totalValue: report.totalValue,
|
|
12274
|
+
boardingCount: report.boardingCount,
|
|
12275
|
+
boardingValue: report.boardingValue,
|
|
12276
|
+
recoverableCount: report.recoverableCount,
|
|
12277
|
+
recoverableValue: report.recoverableValue,
|
|
12278
|
+
awaitingSweepCount: report.awaitingSweepCount,
|
|
12279
|
+
awaitingSweepValue: report.awaitingSweepValue,
|
|
12280
|
+
nextSweepEta: report.nextSweepEta
|
|
12281
|
+
});
|
|
12282
|
+
var serializeMigrationLegReport = (leg) => ({
|
|
12283
|
+
txid: leg.txid,
|
|
12284
|
+
migrated: leg.migrated.map(serializeMigrationVtxoRef),
|
|
12285
|
+
skipped: leg.skipped,
|
|
12286
|
+
deferred: leg.deferred,
|
|
12287
|
+
oversized: leg.oversized?.map(serializeMigrationVtxoRef),
|
|
12288
|
+
error: leg.error
|
|
12289
|
+
});
|
|
12290
|
+
var deserializeMigrationLegReport = (leg) => ({
|
|
12291
|
+
txid: leg.txid,
|
|
12292
|
+
migrated: leg.migrated.map(deserializeMigrationVtxoRef),
|
|
12293
|
+
skipped: leg.skipped,
|
|
12294
|
+
deferred: leg.deferred,
|
|
12295
|
+
oversized: leg.oversized?.map(deserializeMigrationVtxoRef),
|
|
12296
|
+
error: leg.error
|
|
12297
|
+
});
|
|
12298
|
+
var serializeMigrationReport = (report) => ({
|
|
12299
|
+
rotated: report.rotated,
|
|
12300
|
+
skipped: report.skipped,
|
|
12301
|
+
vtxos: report.vtxos ? serializeMigrationLegReport(report.vtxos) : void 0,
|
|
12302
|
+
boarding: report.boarding ? serializeMigrationLegReport(report.boarding) : void 0,
|
|
12303
|
+
expired: report.expired.map(serializeMigrationVtxoRef),
|
|
12304
|
+
signers: report.signers.map(serializeDeprecatedSignerReport)
|
|
12305
|
+
});
|
|
12306
|
+
var deserializeMigrationReport = (report) => ({
|
|
12307
|
+
rotated: report.rotated,
|
|
12308
|
+
skipped: report.skipped,
|
|
12309
|
+
vtxos: report.vtxos ? deserializeMigrationLegReport(report.vtxos) : void 0,
|
|
12310
|
+
boarding: report.boarding ? deserializeMigrationLegReport(report.boarding) : void 0,
|
|
12311
|
+
expired: report.expired.map(deserializeMigrationVtxoRef),
|
|
12312
|
+
signers: report.signers.map(deserializeDeprecatedSignerReport)
|
|
12313
|
+
});
|
|
11128
12314
|
var WalletMessageHandler = class {
|
|
11129
12315
|
messageTag;
|
|
11130
12316
|
wallet;
|
|
@@ -11206,7 +12392,9 @@ var WalletMessageHandler = class {
|
|
|
11206
12392
|
// page-side PING / MESSAGE_BUS_NOT_INITIALIZED path triggered by concurrent
|
|
11207
12393
|
// short requests (GET_STATUS, GET_BALANCE, ...).
|
|
11208
12394
|
isLongRunning(message) {
|
|
11209
|
-
return message.type === "SETTLE" || message.type === "RECOVER_VTXOS" || message.type === "RENEW_VTXOS" || //
|
|
12395
|
+
return message.type === "SETTLE" || message.type === "RECOVER_VTXOS" || message.type === "RENEW_VTXOS" || // Migration may apply a server-signer rotation and then run a full
|
|
12396
|
+
// settle, so it streams settlement events like RENEW_VTXOS.
|
|
12397
|
+
message.type === "MIGRATE_DEPRECATED_SIGNER_VTXOS" || // HD restore walks the index range with one indexer round-trip per
|
|
11210
12398
|
// step until it hits gapLimit consecutive unused indices. The bus
|
|
11211
12399
|
// deadline must not race the scan; liveness stays covered by PING.
|
|
11212
12400
|
message.type === "RESTORE_WALLET";
|
|
@@ -11572,6 +12760,36 @@ var WalletMessageHandler = class {
|
|
|
11572
12760
|
payload: { txid }
|
|
11573
12761
|
});
|
|
11574
12762
|
}
|
|
12763
|
+
case "MIGRATE_DEPRECATED_SIGNER_VTXOS": {
|
|
12764
|
+
const wallet = this.requireWallet();
|
|
12765
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
12766
|
+
const report = await vtxoManager.migrateDeprecatedSignerVtxos({
|
|
12767
|
+
eventCallback: (e) => {
|
|
12768
|
+
this.scheduleForNextTick(
|
|
12769
|
+
() => this.tagged({
|
|
12770
|
+
id,
|
|
12771
|
+
type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_EVENT",
|
|
12772
|
+
payload: e
|
|
12773
|
+
})
|
|
12774
|
+
);
|
|
12775
|
+
}
|
|
12776
|
+
});
|
|
12777
|
+
return this.tagged({
|
|
12778
|
+
id,
|
|
12779
|
+
type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_SUCCESS",
|
|
12780
|
+
payload: { report: serializeMigrationReport(report) }
|
|
12781
|
+
});
|
|
12782
|
+
}
|
|
12783
|
+
case "GET_DEPRECATED_SIGNER_STATUS": {
|
|
12784
|
+
const wallet = this.requireWallet();
|
|
12785
|
+
const vtxoManager = await wallet.getVtxoManager();
|
|
12786
|
+
const signers = await vtxoManager.getDeprecatedSignerStatus();
|
|
12787
|
+
return this.tagged({
|
|
12788
|
+
id,
|
|
12789
|
+
type: "DEPRECATED_SIGNER_STATUS",
|
|
12790
|
+
payload: { signers: signers.map(serializeDeprecatedSignerReport) }
|
|
12791
|
+
});
|
|
12792
|
+
}
|
|
11575
12793
|
case "RESTORE_WALLET": {
|
|
11576
12794
|
const wallet = this.requireWallet();
|
|
11577
12795
|
try {
|
|
@@ -11605,9 +12823,10 @@ var WalletMessageHandler = class {
|
|
|
11605
12823
|
await this.onWalletInitialized();
|
|
11606
12824
|
}
|
|
11607
12825
|
async handleGetBalance() {
|
|
11608
|
-
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
12826
|
+
const [boardingUtxos, allVtxos, pendingOutpoints] = await Promise.all([
|
|
11609
12827
|
this.getAllBoardingUtxos(),
|
|
11610
|
-
this.getVtxosFromRepo()
|
|
12828
|
+
this.getVtxosFromRepo(),
|
|
12829
|
+
this.readonlyWallet ? this.readonlyWallet.pendingRecoveryOutpoints() : Promise.resolve(/* @__PURE__ */ new Set())
|
|
11611
12830
|
]);
|
|
11612
12831
|
let confirmed = 0;
|
|
11613
12832
|
let unconfirmed = 0;
|
|
@@ -11623,8 +12842,11 @@ var WalletMessageHandler = class {
|
|
|
11623
12842
|
let settled = 0;
|
|
11624
12843
|
let preconfirmed = 0;
|
|
11625
12844
|
let recoverable = 0;
|
|
12845
|
+
let pendingRecovery = 0;
|
|
11626
12846
|
for (const vtxo of spendableVtxos) {
|
|
11627
|
-
if (vtxo.
|
|
12847
|
+
if (pendingOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
|
|
12848
|
+
pendingRecovery += vtxo.value;
|
|
12849
|
+
} else if (vtxo.virtualStatus.state === "settled") {
|
|
11628
12850
|
settled += vtxo.value;
|
|
11629
12851
|
} else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
11630
12852
|
preconfirmed += vtxo.value;
|
|
@@ -11636,7 +12858,7 @@ var WalletMessageHandler = class {
|
|
|
11636
12858
|
}
|
|
11637
12859
|
}
|
|
11638
12860
|
const totalBoarding = confirmed + unconfirmed;
|
|
11639
|
-
const totalOffchain = settled + preconfirmed + recoverable;
|
|
12861
|
+
const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
|
|
11640
12862
|
const assetBalances = /* @__PURE__ */ new Map();
|
|
11641
12863
|
for (const vtxo of spendableVtxos) {
|
|
11642
12864
|
if (vtxo.assets) {
|
|
@@ -11660,6 +12882,7 @@ var WalletMessageHandler = class {
|
|
|
11660
12882
|
preconfirmed,
|
|
11661
12883
|
available: settled + preconfirmed,
|
|
11662
12884
|
recoverable,
|
|
12885
|
+
pendingRecovery,
|
|
11663
12886
|
total: totalBoarding + totalOffchain,
|
|
11664
12887
|
assets
|
|
11665
12888
|
};
|
|
@@ -12045,6 +13268,7 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
12045
13268
|
GET_EXPIRING_VTXOS: 2e4,
|
|
12046
13269
|
GET_EXPIRED_BOARDING_UTXOS: 2e4,
|
|
12047
13270
|
GET_RECOVERABLE_BALANCE: 2e4,
|
|
13271
|
+
GET_DEPRECATED_SIGNER_STATUS: 2e4,
|
|
12048
13272
|
RELOAD_WALLET: 2e4,
|
|
12049
13273
|
// Transactions — need more headroom.
|
|
12050
13274
|
// SETTLE / RECOVER_VTXOS / RENEW_VTXOS go through the streaming path and
|
|
@@ -12060,6 +13284,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
12060
13284
|
RECOVER_VTXOS: 5e4,
|
|
12061
13285
|
RENEW_VTXOS: 5e4,
|
|
12062
13286
|
SWEEP_EXPIRED_BOARDING_UTXOS: 5e4,
|
|
13287
|
+
// Streaming/long-running like RENEW_VTXOS (rotation + settle); the value is
|
|
13288
|
+
// kept for type completeness and is never enforced as an inactivity deadline.
|
|
13289
|
+
MIGRATE_DEPRECATED_SIGNER_VTXOS: 5e4,
|
|
12063
13290
|
// RESTORE_WALLET is a streaming/long-running path (sendMessageWithEvents)
|
|
12064
13291
|
// like SETTLE; the value here is kept for type completeness and is never
|
|
12065
13292
|
// enforced as an inactivity deadline.
|
|
@@ -12085,6 +13312,7 @@ var DEDUPABLE_REQUEST_TYPES = /* @__PURE__ */ new Set([
|
|
|
12085
13312
|
"GET_DELEGATE_INFO",
|
|
12086
13313
|
"GET_RECOVERABLE_BALANCE",
|
|
12087
13314
|
"GET_EXPIRED_BOARDING_UTXOS",
|
|
13315
|
+
"GET_DEPRECATED_SIGNER_STATUS",
|
|
12088
13316
|
"GET_VTXOS",
|
|
12089
13317
|
"GET_CONTRACTS",
|
|
12090
13318
|
"GET_CONTRACTS_WITH_VTXOS",
|
|
@@ -13207,6 +14435,42 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
|
|
|
13207
14435
|
throw new Error(`Failed to sweep expired boarding utxos: ${e}`);
|
|
13208
14436
|
}
|
|
13209
14437
|
},
|
|
14438
|
+
async migrateDeprecatedSignerVtxos(options) {
|
|
14439
|
+
const message = {
|
|
14440
|
+
tag: messageTag,
|
|
14441
|
+
type: "MIGRATE_DEPRECATED_SIGNER_VTXOS",
|
|
14442
|
+
id: getRandomId()
|
|
14443
|
+
};
|
|
14444
|
+
try {
|
|
14445
|
+
const response = await wallet.sendMessageWithEvents(
|
|
14446
|
+
message,
|
|
14447
|
+
(resp) => options?.eventCallback?.(
|
|
14448
|
+
resp.payload
|
|
14449
|
+
),
|
|
14450
|
+
(resp) => resp.type === "MIGRATE_DEPRECATED_SIGNER_VTXOS_SUCCESS"
|
|
14451
|
+
);
|
|
14452
|
+
return deserializeMigrationReport(
|
|
14453
|
+
response.payload.report
|
|
14454
|
+
);
|
|
14455
|
+
} catch (e) {
|
|
14456
|
+
throw new Error(`Failed to migrate deprecated-signer vtxos: ${e}`);
|
|
14457
|
+
}
|
|
14458
|
+
},
|
|
14459
|
+
async getDeprecatedSignerStatus() {
|
|
14460
|
+
const message = {
|
|
14461
|
+
tag: messageTag,
|
|
14462
|
+
type: "GET_DEPRECATED_SIGNER_STATUS",
|
|
14463
|
+
id: getRandomId()
|
|
14464
|
+
};
|
|
14465
|
+
try {
|
|
14466
|
+
const response = await wallet.sendMessage(message);
|
|
14467
|
+
return response.payload.signers.map(
|
|
14468
|
+
deserializeDeprecatedSignerReport
|
|
14469
|
+
);
|
|
14470
|
+
} catch (e) {
|
|
14471
|
+
throw new Error(`Failed to get deprecated-signer status: ${e}`);
|
|
14472
|
+
}
|
|
14473
|
+
},
|
|
13210
14474
|
async dispose() {
|
|
13211
14475
|
return;
|
|
13212
14476
|
}
|
|
@@ -14112,13 +15376,19 @@ function isHeaderSubscribeResult(v) {
|
|
|
14112
15376
|
const obj = v;
|
|
14113
15377
|
return typeof obj.height === "number" && typeof obj.hex === "string";
|
|
14114
15378
|
}
|
|
15379
|
+
function errorText(err) {
|
|
15380
|
+
if (typeof err === "string") return err;
|
|
15381
|
+
if (err && typeof err === "object") {
|
|
15382
|
+
const e = err;
|
|
15383
|
+
return [e.message, e.str].filter((v) => typeof v === "string").join(" ");
|
|
15384
|
+
}
|
|
15385
|
+
return "";
|
|
15386
|
+
}
|
|
14115
15387
|
function isMissingHeightError(err) {
|
|
14116
|
-
|
|
14117
|
-
return msg.toLowerCase().includes("missingheight");
|
|
15388
|
+
return errorText(err).toLowerCase().includes("missingheight");
|
|
14118
15389
|
}
|
|
14119
15390
|
function isTxNotInBlockError(err) {
|
|
14120
|
-
const
|
|
14121
|
-
const normalized = msg.toLowerCase();
|
|
15391
|
+
const normalized = errorText(err).toLowerCase();
|
|
14122
15392
|
return normalized.includes("not yet in a block") || normalized.includes("not in a block") || normalized.includes("not in block") || normalized.includes("no confirmed transaction");
|
|
14123
15393
|
}
|
|
14124
15394
|
function childTxidFromHex(txHex) {
|
|
@@ -14578,6 +15848,6 @@ function isArkContract(str) {
|
|
|
14578
15848
|
return str.startsWith(ARKCONTRACT_PREFIX + "=");
|
|
14579
15849
|
}
|
|
14580
15850
|
|
|
14581
|
-
export { ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, buildForfeitTx, buildOffchainTx, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
|
|
14582
|
-
//# sourceMappingURL=chunk-
|
|
14583
|
-
//# sourceMappingURL=chunk-
|
|
15851
|
+
export { ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
|
|
15852
|
+
//# sourceMappingURL=chunk-NOR7XOKN.js.map
|
|
15853
|
+
//# sourceMappingURL=chunk-NOR7XOKN.js.map
|