@arkade-os/sdk 0.4.33 → 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/README.md +1 -1
- package/dist/adapters/expo.cjs +5 -5
- package/dist/adapters/expo.d.cts +2 -2
- package/dist/adapters/expo.d.ts +2 -2
- package/dist/adapters/expo.js +3 -3
- package/dist/adapters/indexedDB.cjs +5 -5
- package/dist/adapters/indexedDB.js +4 -4
- package/dist/{ark-DEsDMYGv.d.cts → ark-D6sau_6-.d.cts} +522 -9
- package/dist/{ark-DEsDMYGv.d.ts → ark-D6sau_6-.d.ts} +522 -9
- package/dist/{asyncStorageTaskQueue-D8T1VXEx.d.cts → asyncStorageTaskQueue-CpC027t_.d.cts} +2 -2
- package/dist/{asyncStorageTaskQueue-CMrTYlKG.d.ts → asyncStorageTaskQueue-GT8fmPUG.d.ts} +2 -2
- package/dist/{chunk-E22HEKLN.js → chunk-3JR77WQ4.js} +140 -42
- package/dist/chunk-3JR77WQ4.js.map +1 -0
- package/dist/{chunk-WMIPYZSB.cjs → chunk-CMPJR3HS.cjs} +42 -9
- package/dist/chunk-CMPJR3HS.cjs.map +1 -0
- package/dist/{chunk-AOJUURHM.js → chunk-CUSABEUQ.js} +141 -37
- package/dist/chunk-CUSABEUQ.js.map +1 -0
- package/dist/{chunk-HAVA4XB7.cjs → chunk-FM7T7JVL.cjs} +7 -7
- package/dist/{chunk-HAVA4XB7.cjs.map → chunk-FM7T7JVL.cjs.map} +1 -1
- package/dist/{chunk-GYSK5R57.cjs → chunk-GUTKJMSF.cjs} +164 -59
- package/dist/chunk-GUTKJMSF.cjs.map +1 -0
- package/dist/{chunk-7K3ROJF6.cjs → chunk-H2LX2KKY.cjs} +2161 -466
- package/dist/chunk-H2LX2KKY.cjs.map +1 -0
- package/dist/{chunk-DSS2GQUG.js → chunk-NOR7XOKN.js} +2021 -331
- package/dist/chunk-NOR7XOKN.js.map +1 -0
- package/dist/{chunk-BU3BU6XK.js → chunk-OURFR4UR.js} +3 -3
- package/dist/{chunk-BU3BU6XK.js.map → chunk-OURFR4UR.js.map} +1 -1
- package/dist/{chunk-TU3LVAPX.js → chunk-OUVTG72A.js} +43 -11
- package/dist/chunk-OUVTG72A.js.map +1 -0
- package/dist/{chunk-5CCRRL5S.cjs → chunk-VYS3KGRI.cjs} +19 -13
- package/dist/chunk-VYS3KGRI.cjs.map +1 -0
- package/dist/{chunk-SPDNHPM4.cjs → chunk-X2EQLK4O.cjs} +149 -46
- package/dist/chunk-X2EQLK4O.cjs.map +1 -0
- package/dist/{chunk-L6ZETTX3.js → chunk-XQS2HW4Q.js} +11 -5
- package/dist/chunk-XQS2HW4Q.js.map +1 -0
- package/dist/contracts/handlers/index.cjs +7 -7
- package/dist/contracts/handlers/index.d.cts +3 -3
- package/dist/contracts/handlers/index.d.ts +3 -3
- package/dist/contracts/handlers/index.js +2 -2
- package/dist/{delegate-BJeBNP5a.d.cts → delegate-C-L6gSZx.d.cts} +1 -1
- package/dist/{delegate-EXN2mfkb.d.ts → delegate-De5__fpZ.d.ts} +1 -1
- package/dist/{index-BG2ooYKO.d.ts → index-BETdjE_o.d.ts} +22 -16
- package/dist/{index-DHjEeHEp.d.cts → index-jwQfHP6D.d.cts} +22 -16
- package/dist/index.cjs +158 -130
- package/dist/index.d.cts +125 -16
- package/dist/index.d.ts +125 -16
- package/dist/index.js +4 -4
- package/dist/repositories/realm/index.cjs +14 -14
- 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 +5 -5
- package/dist/repositories/realm/index.js.map +1 -1
- package/dist/repositories/sqlite/index.cjs +13 -13
- package/dist/repositories/sqlite/index.d.cts +1 -1
- package/dist/repositories/sqlite/index.d.ts +1 -1
- package/dist/repositories/sqlite/index.js +4 -4
- package/dist/{taskRunner-pIGyarFG.d.cts → taskRunner-DCyp6Gea.d.cts} +2 -2
- package/dist/{taskRunner-B7lBU45X.d.ts → taskRunner-DnxtObeq.d.ts} +2 -2
- package/dist/wallet/expo/background.cjs +14 -14
- package/dist/wallet/expo/background.d.cts +3 -3
- package/dist/wallet/expo/background.d.ts +3 -3
- package/dist/wallet/expo/background.js +6 -6
- package/dist/wallet/expo/index.cjs +13 -13
- package/dist/wallet/expo/index.d.cts +5 -5
- package/dist/wallet/expo/index.d.ts +5 -5
- package/dist/wallet/expo/index.js +5 -5
- package/dist/{wallet-D4Dll5Gu.d.cts → wallet-BWHbd5b1.d.cts} +388 -10
- package/dist/{wallet-C4L_X0i6.d.ts → wallet-Bth5uucA.d.ts} +388 -10
- package/dist/worker/expo/index.cjs +9 -9
- package/dist/worker/expo/index.d.cts +4 -4
- package/dist/worker/expo/index.d.ts +4 -4
- package/dist/worker/expo/index.js +5 -5
- package/package.json +5 -5
- package/dist/chunk-5CCRRL5S.cjs.map +0 -1
- package/dist/chunk-7K3ROJF6.cjs.map +0 -1
- package/dist/chunk-AOJUURHM.js.map +0 -1
- package/dist/chunk-DSS2GQUG.js.map +0 -1
- package/dist/chunk-E22HEKLN.js.map +0 -1
- package/dist/chunk-GYSK5R57.cjs.map +0 -1
- package/dist/chunk-L6ZETTX3.js.map +0 -1
- package/dist/chunk-SPDNHPM4.cjs.map +0 -1
- package/dist/chunk-TU3LVAPX.js.map +0 -1
- package/dist/chunk-WMIPYZSB.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
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-
|
|
2
|
-
import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-
|
|
3
|
-
import { VtxoScript, timelockToSequence, DEFAULT_NETWORK, DEFAULT_NETWORK_NAME, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, ArkAddress,
|
|
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
|
+
import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-CUSABEUQ.js';
|
|
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';
|
|
5
5
|
import { SigHash, Script, p2tr, RawWitness, Address, OutScript, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$2 } from '@scure/btc-signer';
|
|
6
6
|
import { hex, base58, base64 } from '@scure/base';
|
|
@@ -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)) {
|
|
@@ -1009,7 +1009,7 @@ var ESPLORA_URL = {
|
|
|
1009
1009
|
testnet: "https://mempool.space/testnet/api",
|
|
1010
1010
|
signet: "https://mempool.signet.arkade.sh/api",
|
|
1011
1011
|
mutinynet: "https://mempool.mutinynet.arkade.sh/api",
|
|
1012
|
-
regtest: "http://localhost:3000"
|
|
1012
|
+
regtest: "http://localhost:3000/api"
|
|
1013
1013
|
};
|
|
1014
1014
|
var EsploraProvider = class {
|
|
1015
1015
|
constructor(baseUrl = ESPLORA_URL[DEFAULT_NETWORK_NAME], opts) {
|
|
@@ -1020,14 +1020,17 @@ 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
|
+
if (response.status === 404) {
|
|
1032
|
+
return void 0;
|
|
1033
|
+
}
|
|
1031
1034
|
if (!response.ok) {
|
|
1032
1035
|
throw new Error(`Failed to fetch fee rate: ${response.statusText}`);
|
|
1033
1036
|
}
|
|
@@ -1045,7 +1048,7 @@ var EsploraProvider = class {
|
|
|
1045
1048
|
}
|
|
1046
1049
|
}
|
|
1047
1050
|
async getTxOutspends(txid) {
|
|
1048
|
-
const response = await
|
|
1051
|
+
const response = await baseFetch(`${this.baseUrl}/tx/${txid}/outspends`);
|
|
1049
1052
|
if (!response.ok) {
|
|
1050
1053
|
const error = await response.text();
|
|
1051
1054
|
throw new Error(`Failed to get transaction outspends: ${error}`);
|
|
@@ -1053,7 +1056,7 @@ var EsploraProvider = class {
|
|
|
1053
1056
|
return response.json();
|
|
1054
1057
|
}
|
|
1055
1058
|
async getTransactions(address) {
|
|
1056
|
-
const response = await
|
|
1059
|
+
const response = await baseFetch(`${this.baseUrl}/address/${address}/txs`);
|
|
1057
1060
|
if (!response.ok) {
|
|
1058
1061
|
const error = await response.text();
|
|
1059
1062
|
throw new Error(`Failed to get transactions: ${error}`);
|
|
@@ -1061,7 +1064,7 @@ var EsploraProvider = class {
|
|
|
1061
1064
|
return response.json();
|
|
1062
1065
|
}
|
|
1063
1066
|
async getTxStatus(txid) {
|
|
1064
|
-
const txresponse = await
|
|
1067
|
+
const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
|
|
1065
1068
|
if (!txresponse.ok) {
|
|
1066
1069
|
throw new Error(txresponse.statusText);
|
|
1067
1070
|
}
|
|
@@ -1069,7 +1072,7 @@ var EsploraProvider = class {
|
|
|
1069
1072
|
if (!tx.status.confirmed) {
|
|
1070
1073
|
return { confirmed: false };
|
|
1071
1074
|
}
|
|
1072
|
-
const response = await
|
|
1075
|
+
const response = await baseFetch(`${this.baseUrl}/tx/${txid}/status`);
|
|
1073
1076
|
if (!response.ok) {
|
|
1074
1077
|
throw new Error(`Failed to get transaction status: ${response.statusText}`);
|
|
1075
1078
|
}
|
|
@@ -1153,7 +1156,7 @@ var EsploraProvider = class {
|
|
|
1153
1156
|
return stopFunc;
|
|
1154
1157
|
}
|
|
1155
1158
|
async getChainTip() {
|
|
1156
|
-
const tipBlocks = await
|
|
1159
|
+
const tipBlocks = await baseFetch(`${this.baseUrl}/blocks`);
|
|
1157
1160
|
if (!tipBlocks.ok) {
|
|
1158
1161
|
throw new Error(`Failed to get chain tip: ${tipBlocks.statusText}`);
|
|
1159
1162
|
}
|
|
@@ -1172,7 +1175,7 @@ var EsploraProvider = class {
|
|
|
1172
1175
|
};
|
|
1173
1176
|
}
|
|
1174
1177
|
async broadcastPackage(parent, child) {
|
|
1175
|
-
const response = await
|
|
1178
|
+
const response = await baseFetch(`${this.baseUrl}/txs/package`, {
|
|
1176
1179
|
method: "POST",
|
|
1177
1180
|
headers: {
|
|
1178
1181
|
"Content-Type": "application/json"
|
|
@@ -1186,7 +1189,7 @@ var EsploraProvider = class {
|
|
|
1186
1189
|
return response.json();
|
|
1187
1190
|
}
|
|
1188
1191
|
async broadcastTx(tx) {
|
|
1189
|
-
const response = await
|
|
1192
|
+
const response = await baseFetch(`${this.baseUrl}/tx`, {
|
|
1190
1193
|
method: "POST",
|
|
1191
1194
|
headers: {
|
|
1192
1195
|
"Content-Type": "text/plain"
|
|
@@ -1776,6 +1779,45 @@ function selectedCoinsToAssetInputs(selectedCoins) {
|
|
|
1776
1779
|
}
|
|
1777
1780
|
return assetInputs;
|
|
1778
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
|
+
}
|
|
1779
1821
|
function buildOffchainTx(inputs, outputs, serverUnrollScript) {
|
|
1780
1822
|
const MAX_OP_RETURN = 2;
|
|
1781
1823
|
let countOpReturn = 0;
|
|
@@ -2258,12 +2300,12 @@ var FALLBACK_WALLET_DUST_AMOUNT = 330n;
|
|
|
2258
2300
|
function getDustAmount(wallet) {
|
|
2259
2301
|
return "dustAmount" in wallet ? wallet.dustAmount : FALLBACK_WALLET_DUST_AMOUNT;
|
|
2260
2302
|
}
|
|
2261
|
-
function
|
|
2303
|
+
function extendCoinWithTapscript(boardingTapscript, utxo) {
|
|
2262
2304
|
return {
|
|
2263
2305
|
...utxo,
|
|
2264
|
-
forfeitTapLeafScript:
|
|
2265
|
-
intentTapLeafScript:
|
|
2266
|
-
tapTree:
|
|
2306
|
+
forfeitTapLeafScript: boardingTapscript.forfeit(),
|
|
2307
|
+
intentTapLeafScript: boardingTapscript.forfeit(),
|
|
2308
|
+
tapTree: boardingTapscript.encode()
|
|
2267
2309
|
};
|
|
2268
2310
|
}
|
|
2269
2311
|
function deriveContractTapscripts(contract) {
|
|
@@ -2356,13 +2398,27 @@ function validateRecipients(recipients, dustAmount) {
|
|
|
2356
2398
|
}
|
|
2357
2399
|
|
|
2358
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
|
+
}
|
|
2359
2415
|
function isSweepCapable(wallet) {
|
|
2360
|
-
return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet;
|
|
2416
|
+
return "boardingTapscript" in wallet && "onchainProvider" in wallet && "arkProvider" in wallet && "network" in wallet && "signOnchainBoardingTx" in wallet;
|
|
2361
2417
|
}
|
|
2362
2418
|
function assertSweepCapable(wallet) {
|
|
2363
2419
|
if (!isSweepCapable(wallet)) {
|
|
2364
2420
|
throw new Error(
|
|
2365
|
-
"Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, and
|
|
2421
|
+
"Boarding UTXO sweep requires a Wallet instance with boardingTapscript, onchainProvider, arkProvider, network, and signOnchainBoardingTx"
|
|
2366
2422
|
);
|
|
2367
2423
|
}
|
|
2368
2424
|
}
|
|
@@ -2378,6 +2434,33 @@ async function runWithCrossInstanceLock(name, fn) {
|
|
|
2378
2434
|
await fn();
|
|
2379
2435
|
});
|
|
2380
2436
|
}
|
|
2437
|
+
var MAX_VTXOS_PER_SETTLEMENT = 50;
|
|
2438
|
+
function byValueDescending(vtxos) {
|
|
2439
|
+
return [...vtxos].sort((a, b) => b.value - a.value);
|
|
2440
|
+
}
|
|
2441
|
+
function byExpiryAscending(vtxos) {
|
|
2442
|
+
const expiryKey = (vtxo) => {
|
|
2443
|
+
if (isRecoverable(vtxo)) return -Infinity;
|
|
2444
|
+
const batchExpiry = vtxo.virtualStatus.batchExpiry;
|
|
2445
|
+
if (isExpired(vtxo)) return batchExpiry ?? -Infinity;
|
|
2446
|
+
if (!batchExpiry) return Infinity;
|
|
2447
|
+
if (new Date(batchExpiry).getFullYear() < 2025) return Infinity;
|
|
2448
|
+
return batchExpiry;
|
|
2449
|
+
};
|
|
2450
|
+
return [...vtxos].sort((a, b) => expiryKey(a) - expiryKey(b));
|
|
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
|
+
}
|
|
2381
2464
|
var DEFAULT_THRESHOLD_SECONDS = 259200;
|
|
2382
2465
|
var DEFAULT_THRESHOLD_MS = DEFAULT_THRESHOLD_SECONDS * 1e3;
|
|
2383
2466
|
var DEFAULT_RENEWAL_CONFIG = {
|
|
@@ -2387,7 +2470,8 @@ var DEFAULT_RENEWAL_CONFIG = {
|
|
|
2387
2470
|
var DEFAULT_SETTLEMENT_CONFIG = {
|
|
2388
2471
|
vtxoThreshold: DEFAULT_THRESHOLD_SECONDS,
|
|
2389
2472
|
boardingUtxoSweep: true,
|
|
2390
|
-
pollIntervalMs: 6e4
|
|
2473
|
+
pollIntervalMs: 6e4,
|
|
2474
|
+
deprecatedSignerMigration: true
|
|
2391
2475
|
};
|
|
2392
2476
|
function getRecoverableVtxos(vtxos, dustAmount) {
|
|
2393
2477
|
return vtxos.filter((vtxo) => {
|
|
@@ -2441,6 +2525,51 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
|
2441
2525
|
(vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) || isRecoverable(vtxo) || isSpendable(vtxo) && isExpired(vtxo) || isSubdust(vtxo, dustAmount)
|
|
2442
2526
|
);
|
|
2443
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
|
+
}
|
|
2444
2573
|
var VtxoManager = class _VtxoManager {
|
|
2445
2574
|
constructor(wallet, renewalConfig, settlementConfig) {
|
|
2446
2575
|
this.wallet = wallet;
|
|
@@ -2456,15 +2585,9 @@ var VtxoManager = class _VtxoManager {
|
|
|
2456
2585
|
} else {
|
|
2457
2586
|
this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
|
|
2458
2587
|
}
|
|
2459
|
-
this.contractEventsSubscriptionReady = this.initializeSubscription()
|
|
2460
|
-
(subscription) => {
|
|
2461
|
-
this.contractEventsSubscription = subscription;
|
|
2462
|
-
return subscription;
|
|
2463
|
-
}
|
|
2464
|
-
);
|
|
2588
|
+
this.contractEventsSubscriptionReady = this.initializeSubscription();
|
|
2465
2589
|
}
|
|
2466
2590
|
settlementConfig;
|
|
2467
|
-
contractEventsSubscription;
|
|
2468
2591
|
contractEventsSubscriptionReady;
|
|
2469
2592
|
disposePromise;
|
|
2470
2593
|
pollTimeoutId;
|
|
@@ -2501,6 +2624,15 @@ var VtxoManager = class _VtxoManager {
|
|
|
2501
2624
|
lastVtxoSpentRefreshTimestamp = 0;
|
|
2502
2625
|
vtxoSpentRefreshPromise;
|
|
2503
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;
|
|
2504
2636
|
// ========== Recovery Methods ==========
|
|
2505
2637
|
/**
|
|
2506
2638
|
* Recover swept/expired virtual outputs by settling them back to the wallet's Arkade address.
|
|
@@ -2537,10 +2669,25 @@ var VtxoManager = class _VtxoManager {
|
|
|
2537
2669
|
withUnrolled: false
|
|
2538
2670
|
});
|
|
2539
2671
|
const dustAmount = getDustAmount(this.wallet);
|
|
2540
|
-
|
|
2672
|
+
let { vtxosToRecover, totalAmount } = getRecoverableWithSubdust(allVtxos, dustAmount);
|
|
2541
2673
|
if (vtxosToRecover.length === 0) {
|
|
2542
2674
|
throw new Error("No recoverable VTXOs found");
|
|
2543
2675
|
}
|
|
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) {
|
|
2680
|
+
const recoverableCount = vtxosToRecover.length;
|
|
2681
|
+
({ vtxosToRecover, totalAmount } = getRecoverableWithSubdust(capped, dustAmount));
|
|
2682
|
+
if (vtxosToRecover.length === 0) {
|
|
2683
|
+
throw new Error(
|
|
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}`
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
if (info && isMigrationCapable(this.wallet)) {
|
|
2689
|
+
await this.rotateForRecoverableInputs(vtxosToRecover, info);
|
|
2690
|
+
}
|
|
2544
2691
|
const arkAddress = await this.wallet.getAddress();
|
|
2545
2692
|
return this.wallet.settle(
|
|
2546
2693
|
{
|
|
@@ -2690,6 +2837,25 @@ var VtxoManager = class _VtxoManager {
|
|
|
2690
2837
|
if (vtxos.length === 0) {
|
|
2691
2838
|
throw new Error("No VTXOs available to renew");
|
|
2692
2839
|
}
|
|
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
|
+
}
|
|
2858
|
+
}
|
|
2693
2859
|
const totalAmount = vtxos.reduce((sum, vtxo) => sum + vtxo.value, 0);
|
|
2694
2860
|
const dustAmount = getDustAmount(this.wallet);
|
|
2695
2861
|
if (BigInt(totalAmount) < dustAmount) {
|
|
@@ -2697,6 +2863,9 @@ var VtxoManager = class _VtxoManager {
|
|
|
2697
2863
|
`Total amount ${totalAmount} is below dust threshold ${dustAmount}`
|
|
2698
2864
|
);
|
|
2699
2865
|
}
|
|
2866
|
+
if (info && isMigrationCapable(this.wallet)) {
|
|
2867
|
+
await this.rotateForRecoverableInputs(vtxos, info);
|
|
2868
|
+
}
|
|
2700
2869
|
const arkAddress = await this.wallet.getAddress();
|
|
2701
2870
|
const txid = await this.wallet.settle(
|
|
2702
2871
|
{
|
|
@@ -2797,7 +2966,6 @@ var VtxoManager = class _VtxoManager {
|
|
|
2797
2966
|
const boardingAddress = await this.wallet.getBoardingAddress();
|
|
2798
2967
|
const feeRate = await this.getOnchainProvider().getFeeRate() ?? 1;
|
|
2799
2968
|
const exitTapLeafScript = this.getBoardingExitLeaf();
|
|
2800
|
-
const sequence = getSequence(exitTapLeafScript);
|
|
2801
2969
|
const leafScript = exitTapLeafScript[1];
|
|
2802
2970
|
const leafScriptSize = leafScript.length - 1;
|
|
2803
2971
|
const controlBlockSize = exitTapLeafScript[0].merklePath.length * 32;
|
|
@@ -2818,19 +2986,28 @@ var VtxoManager = class _VtxoManager {
|
|
|
2818
2986
|
}
|
|
2819
2987
|
const tx = new Transaction();
|
|
2820
2988
|
for (const utxo of expiredUtxos) {
|
|
2989
|
+
const utxoScript = VtxoScript.decode(utxo.tapTree);
|
|
2990
|
+
const utxoExitLeaf = utxoScript.leaves.find(
|
|
2991
|
+
(leaf) => CSVMultisigTapscript.isScriptValid(scriptFromTapLeafScript(leaf)) === true
|
|
2992
|
+
);
|
|
2993
|
+
if (!utxoExitLeaf) {
|
|
2994
|
+
throw new Error(
|
|
2995
|
+
`Boarding sweep: no CSV exit leaf for UTXO ${utxo.txid}:${utxo.vout}`
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2821
2998
|
tx.addInput({
|
|
2822
2999
|
txid: utxo.txid,
|
|
2823
3000
|
index: utxo.vout,
|
|
2824
3001
|
witnessUtxo: {
|
|
2825
|
-
script:
|
|
3002
|
+
script: utxoScript.pkScript,
|
|
2826
3003
|
amount: BigInt(utxo.value)
|
|
2827
3004
|
},
|
|
2828
|
-
tapLeafScript: [
|
|
2829
|
-
sequence
|
|
3005
|
+
tapLeafScript: [utxoExitLeaf],
|
|
3006
|
+
sequence: getSequence(utxoExitLeaf)
|
|
2830
3007
|
});
|
|
2831
3008
|
}
|
|
2832
3009
|
tx.addOutputAddress(boardingAddress, outputAmount, this.getNetwork());
|
|
2833
|
-
const signedTx = await this.
|
|
3010
|
+
const signedTx = await this.getSweepWallet().signOnchainBoardingTx(tx);
|
|
2834
3011
|
signedTx.finalize();
|
|
2835
3012
|
const txid = await this.getOnchainProvider().broadcastTransaction(signedTx.hex);
|
|
2836
3013
|
for (const u of expiredUtxos) {
|
|
@@ -2839,6 +3016,472 @@ var VtxoManager = class _VtxoManager {
|
|
|
2839
3016
|
this.knownBoardingUtxos.add(`${txid}:0`);
|
|
2840
3017
|
return txid;
|
|
2841
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
|
+
}
|
|
2842
3485
|
// ========== Private Helpers ==========
|
|
2843
3486
|
/** Asserts sweep capability and returns the typed wallet. */
|
|
2844
3487
|
getSweepWallet() {
|
|
@@ -2857,10 +3500,6 @@ var VtxoManager = class _VtxoManager {
|
|
|
2857
3500
|
getBoardingExitLeaf() {
|
|
2858
3501
|
return this.getSweepWallet().boardingTapscript.exit();
|
|
2859
3502
|
}
|
|
2860
|
-
/** Returns the pkScript (output script) of the boarding tapscript. */
|
|
2861
|
-
getBoardingOutputScript() {
|
|
2862
|
-
return this.getSweepWallet().boardingTapscript.pkScript;
|
|
2863
|
-
}
|
|
2864
3503
|
/** Returns the onchain provider for fee estimation and broadcasting. */
|
|
2865
3504
|
getOnchainProvider() {
|
|
2866
3505
|
return this.getSweepWallet().onchainProvider;
|
|
@@ -2869,14 +3508,20 @@ var VtxoManager = class _VtxoManager {
|
|
|
2869
3508
|
getArkProvider() {
|
|
2870
3509
|
return this.getSweepWallet().arkProvider;
|
|
2871
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
|
+
}
|
|
2872
3521
|
/** Returns the Bitcoin network configuration from the wallet. */
|
|
2873
3522
|
getNetwork() {
|
|
2874
3523
|
return this.getSweepWallet().network;
|
|
2875
3524
|
}
|
|
2876
|
-
/** Returns the wallet's identity for transaction signing. */
|
|
2877
|
-
getIdentity() {
|
|
2878
|
-
return this.wallet.identity;
|
|
2879
|
-
}
|
|
2880
3525
|
async initializeSubscription() {
|
|
2881
3526
|
if (this.settlementConfig === false) {
|
|
2882
3527
|
return void 0;
|
|
@@ -3074,6 +3719,10 @@ var VtxoManager = class _VtxoManager {
|
|
|
3074
3719
|
}
|
|
3075
3720
|
}
|
|
3076
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
|
+
}
|
|
3077
3726
|
});
|
|
3078
3727
|
} catch (e) {
|
|
3079
3728
|
hadError = true;
|
|
@@ -3142,7 +3791,8 @@ var VtxoManager = class _VtxoManager {
|
|
|
3142
3791
|
return;
|
|
3143
3792
|
}
|
|
3144
3793
|
const dustAmount = getDustAmount(this.wallet);
|
|
3145
|
-
const
|
|
3794
|
+
const info = await this.getArkProvider().getInfo();
|
|
3795
|
+
const { fees, vtxoMaxAmount } = info;
|
|
3146
3796
|
const estimator = new Estimator(fees.intentFee);
|
|
3147
3797
|
let totalAmount = 0n;
|
|
3148
3798
|
const filteredBoarding = [];
|
|
@@ -3157,7 +3807,10 @@ var VtxoManager = class _VtxoManager {
|
|
|
3157
3807
|
totalAmount += BigInt(u.value) - BigInt(inputFee.satoshis);
|
|
3158
3808
|
}
|
|
3159
3809
|
const filteredVtxos = [];
|
|
3160
|
-
for (const v of expiringVtxos) {
|
|
3810
|
+
for (const v of byExpiryAscending(expiringVtxos)) {
|
|
3811
|
+
if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
|
|
3812
|
+
break;
|
|
3813
|
+
}
|
|
3161
3814
|
const inputFee = estimator.evalOffchainInput({
|
|
3162
3815
|
amount: BigInt(v.value),
|
|
3163
3816
|
type: v.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
|
|
@@ -3168,12 +3821,19 @@ var VtxoManager = class _VtxoManager {
|
|
|
3168
3821
|
if (inputFee.satoshis >= v.value) {
|
|
3169
3822
|
continue;
|
|
3170
3823
|
}
|
|
3824
|
+
const net = BigInt(v.value) - BigInt(inputFee.satoshis);
|
|
3825
|
+
if (vtxoMaxAmount >= 0n && totalAmount + net > vtxoMaxAmount) {
|
|
3826
|
+
continue;
|
|
3827
|
+
}
|
|
3171
3828
|
filteredVtxos.push(v);
|
|
3172
|
-
totalAmount +=
|
|
3829
|
+
totalAmount += net;
|
|
3173
3830
|
}
|
|
3174
3831
|
if (filteredBoarding.length === 0 && filteredVtxos.length === 0) {
|
|
3175
3832
|
return;
|
|
3176
3833
|
}
|
|
3834
|
+
if (isMigrationCapable(this.wallet)) {
|
|
3835
|
+
await this.rotateForRecoverableInputs([...filteredBoarding, ...filteredVtxos], info);
|
|
3836
|
+
}
|
|
3177
3837
|
const arkAddress = await this.wallet.getAddress();
|
|
3178
3838
|
const outputFee = estimator.evalOffchainOutput({
|
|
3179
3839
|
amount: totalAmount,
|
|
@@ -3236,7 +3896,6 @@ var VtxoManager = class _VtxoManager {
|
|
|
3236
3896
|
clearTimeout(timer);
|
|
3237
3897
|
}
|
|
3238
3898
|
const subscription = await this.contractEventsSubscriptionReady;
|
|
3239
|
-
this.contractEventsSubscription = void 0;
|
|
3240
3899
|
subscription?.();
|
|
3241
3900
|
})();
|
|
3242
3901
|
return this.disposePromise;
|
|
@@ -3777,15 +4436,17 @@ async function buildTransactionHistory(vtxos, allBoardingTxs, commitmentsToIgnor
|
|
|
3777
4436
|
txTime = getTxCreatedAt ? await getTxCreatedAt(vtxo.arkTxId) ?? vtxo.createdAt.getTime() + 1 : vtxo.createdAt.getTime() + 1;
|
|
3778
4437
|
}
|
|
3779
4438
|
const assets = subtractAssets(allSpent, changes);
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
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
|
+
}
|
|
3789
4450
|
}
|
|
3790
4451
|
if (vtxo.settledBy && !commitmentsToIgnore.has(vtxo.settledBy) && !sent.some((s) => s.key.commitmentTxid === vtxo.settledBy)) {
|
|
3791
4452
|
const changes = fromOldestVtxo.filter(
|
|
@@ -5958,6 +6619,16 @@ function isDiscoverable(handler) {
|
|
|
5958
6619
|
}
|
|
5959
6620
|
|
|
5960
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
|
+
};
|
|
5961
6632
|
var ContractWatcher = class {
|
|
5962
6633
|
config;
|
|
5963
6634
|
contracts = /* @__PURE__ */ new Map();
|
|
@@ -5977,14 +6648,7 @@ var ContractWatcher = class {
|
|
|
5977
6648
|
*/
|
|
5978
6649
|
constructor(config) {
|
|
5979
6650
|
this.config = {
|
|
5980
|
-
|
|
5981
|
-
// 1 minute
|
|
5982
|
-
reconnectDelayMs: 1e3,
|
|
5983
|
-
// 1 second
|
|
5984
|
-
maxReconnectDelayMs: 3e4,
|
|
5985
|
-
// 30 seconds
|
|
5986
|
-
maxReconnectAttempts: 0,
|
|
5987
|
-
// unlimited
|
|
6651
|
+
...DEFAULT_CONTRACT_WATCHER_CONFIG,
|
|
5988
6652
|
...config
|
|
5989
6653
|
};
|
|
5990
6654
|
}
|
|
@@ -6212,8 +6876,9 @@ var ContractWatcher = class {
|
|
|
6212
6876
|
}
|
|
6213
6877
|
this.connectionState = "reconnecting";
|
|
6214
6878
|
this.reconnectAttempts++;
|
|
6215
|
-
const delay =
|
|
6216
|
-
this.
|
|
6879
|
+
const delay = computeReconnectDelay(
|
|
6880
|
+
this.reconnectAttempts,
|
|
6881
|
+
this.config.reconnectDelayMs,
|
|
6217
6882
|
this.config.maxReconnectDelayMs
|
|
6218
6883
|
);
|
|
6219
6884
|
this.reconnectTimeoutId = setTimeout(() => {
|
|
@@ -6515,8 +7180,11 @@ function cursorCutoff(requestStartedAt) {
|
|
|
6515
7180
|
}
|
|
6516
7181
|
|
|
6517
7182
|
// src/contracts/contractManager.ts
|
|
6518
|
-
|
|
7183
|
+
function areCoalescibleContractTypes(a, b) {
|
|
7184
|
+
return a === "default" && b === "boarding" || a === "boarding" && b === "default";
|
|
7185
|
+
}
|
|
6519
7186
|
var SCAN_MAX_INDEX = 1e4;
|
|
7187
|
+
var DEFAULT_SCAN_BATCH = 10;
|
|
6520
7188
|
var ContractManager = class _ContractManager {
|
|
6521
7189
|
config;
|
|
6522
7190
|
watcher;
|
|
@@ -6640,8 +7308,11 @@ var ContractManager = class _ContractManager {
|
|
|
6640
7308
|
const [existing] = await this.getContracts({ script: params.script });
|
|
6641
7309
|
if (existing) {
|
|
6642
7310
|
if (existing.type === params.type) return { contract: existing, persisted: false };
|
|
7311
|
+
if (areCoalescibleContractTypes(existing.type, params.type)) {
|
|
7312
|
+
return { contract: existing, persisted: false };
|
|
7313
|
+
}
|
|
6643
7314
|
throw new Error(
|
|
6644
|
-
`Contract with script ${params.script} already exists with
|
|
7315
|
+
`Contract with script ${params.script} already exists with type ${existing.type}.`
|
|
6645
7316
|
);
|
|
6646
7317
|
}
|
|
6647
7318
|
const contract = {
|
|
@@ -6670,6 +7341,19 @@ var ContractManager = class _ContractManager {
|
|
|
6670
7341
|
* other handler hit it).
|
|
6671
7342
|
* - `persistAndWatchContract` rejecting is operational/fatal and
|
|
6672
7343
|
* propagates (only `discoverAt` is guarded).
|
|
7344
|
+
* - Within an index the handler probes run concurrently (independent
|
|
7345
|
+
* network reads); their hits are persisted sequentially in
|
|
7346
|
+
* `discoverables` order to preserve the first-wins collision tie-break.
|
|
7347
|
+
* - Indices are probed `batchSize` at a time (a second concurrency layer
|
|
7348
|
+
* over the per-index probes), but each window is CAPPED to
|
|
7349
|
+
* `gapLimit - unused` indices — the most a serial scan could still reach
|
|
7350
|
+
* before the gap window is guaranteed to close. So every index probed in
|
|
7351
|
+
* a window is one a one-index-at-a-time scan would also reach: nothing is
|
|
7352
|
+
* over-scanned, nothing is discarded, and `materialize`/`discoverAt` are
|
|
7353
|
+
* invoked on exactly the same index set. The window's hits are still
|
|
7354
|
+
* processed strictly in ascending index order, so the discovered set,
|
|
7355
|
+
* persisted rows, `lastIndexUsed`, and `handlerErrors` are byte-for-byte
|
|
7356
|
+
* identical to the serial path — only the wall-clock differs.
|
|
6673
7357
|
*/
|
|
6674
7358
|
async scanContracts(opts) {
|
|
6675
7359
|
const gapLimit = opts.gapLimit ?? 20;
|
|
@@ -6678,35 +7362,69 @@ var ContractManager = class _ContractManager {
|
|
|
6678
7362
|
`scanContracts: gapLimit must be a positive integer (got ${String(opts.gapLimit)})`
|
|
6679
7363
|
);
|
|
6680
7364
|
}
|
|
6681
|
-
const
|
|
7365
|
+
const batchSize = opts.batchSize ?? DEFAULT_SCAN_BATCH;
|
|
7366
|
+
if (!Number.isInteger(batchSize) || batchSize <= 0) {
|
|
7367
|
+
throw new Error(
|
|
7368
|
+
`scanContracts: batchSize must be a positive integer (got ${String(opts.batchSize)})`
|
|
7369
|
+
);
|
|
7370
|
+
}
|
|
7371
|
+
const registered = contractHandlers.getRegisteredTypes().map((t) => contractHandlers.get(t)).filter(isDiscoverable);
|
|
7372
|
+
const discoverables = [
|
|
7373
|
+
...registered.filter((h) => h.type === "boarding"),
|
|
7374
|
+
...registered.filter((h) => h.type !== "boarding")
|
|
7375
|
+
];
|
|
6682
7376
|
const maxIdx = opts.hd ? SCAN_MAX_INDEX : 0;
|
|
6683
7377
|
const handlerErrors = [];
|
|
6684
7378
|
let lastIndexUsed = -1;
|
|
6685
7379
|
let unused = 0;
|
|
6686
7380
|
let i = 0;
|
|
7381
|
+
const probeIndex = async (index) => {
|
|
7382
|
+
const descriptor = opts.materialize(index);
|
|
7383
|
+
return Promise.all(
|
|
7384
|
+
discoverables.map(async (h) => {
|
|
7385
|
+
try {
|
|
7386
|
+
return {
|
|
7387
|
+
ok: true,
|
|
7388
|
+
found: await h.discoverAt(index, descriptor, opts.deps)
|
|
7389
|
+
};
|
|
7390
|
+
} catch (error) {
|
|
7391
|
+
return { ok: false, error };
|
|
7392
|
+
}
|
|
7393
|
+
})
|
|
7394
|
+
);
|
|
7395
|
+
};
|
|
6687
7396
|
while (i <= maxIdx && unused < gapLimit) {
|
|
6688
|
-
const
|
|
6689
|
-
|
|
6690
|
-
for (
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
7397
|
+
const windowEnd = Math.min(maxIdx, i + Math.min(batchSize, gapLimit - unused) - 1);
|
|
7398
|
+
const windowIndices = [];
|
|
7399
|
+
for (let idx = i; idx <= windowEnd; idx++) windowIndices.push(idx);
|
|
7400
|
+
const windowProbes = await Promise.all(windowIndices.map(probeIndex));
|
|
7401
|
+
for (let w = 0; w < windowIndices.length; w++) {
|
|
7402
|
+
const index = windowIndices[w];
|
|
7403
|
+
const probes = windowProbes[w];
|
|
7404
|
+
let hitAtThisIndex = false;
|
|
7405
|
+
for (let h = 0; h < discoverables.length; h++) {
|
|
7406
|
+
const probe = probes[h];
|
|
7407
|
+
if (!probe.ok) {
|
|
7408
|
+
handlerErrors.push({
|
|
7409
|
+
handler: discoverables[h].type,
|
|
7410
|
+
index,
|
|
7411
|
+
error: probe.error
|
|
7412
|
+
});
|
|
7413
|
+
continue;
|
|
7414
|
+
}
|
|
7415
|
+
for (const c of probe.found) {
|
|
7416
|
+
await this.persistAndWatchContract(c);
|
|
7417
|
+
hitAtThisIndex = true;
|
|
7418
|
+
}
|
|
6697
7419
|
}
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
7420
|
+
if (hitAtThisIndex) {
|
|
7421
|
+
lastIndexUsed = index;
|
|
7422
|
+
unused = 0;
|
|
7423
|
+
} else {
|
|
7424
|
+
unused += 1;
|
|
6701
7425
|
}
|
|
6702
7426
|
}
|
|
6703
|
-
|
|
6704
|
-
lastIndexUsed = i;
|
|
6705
|
-
unused = 0;
|
|
6706
|
-
} else {
|
|
6707
|
-
unused += 1;
|
|
6708
|
-
}
|
|
6709
|
-
i += 1;
|
|
7427
|
+
i = windowEnd + 1;
|
|
6710
7428
|
}
|
|
6711
7429
|
if (opts.hd && i > maxIdx && unused < gapLimit) {
|
|
6712
7430
|
throw new Error(
|
|
@@ -7412,7 +8130,8 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
|
|
|
7412
8130
|
walletRepository: setup.walletRepository,
|
|
7413
8131
|
contractRepository: setup.contractRepository,
|
|
7414
8132
|
serverPubKey: setup.serverPubKey,
|
|
7415
|
-
expectedContractType
|
|
8133
|
+
expectedContractType,
|
|
8134
|
+
baselineReceivePubKey: setup.offchainTapscript.options.pubKey
|
|
7416
8135
|
};
|
|
7417
8136
|
let boot;
|
|
7418
8137
|
try {
|
|
@@ -7457,14 +8176,17 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
|
|
|
7457
8176
|
receivePubkey: existing.pubKey
|
|
7458
8177
|
};
|
|
7459
8178
|
}
|
|
7460
|
-
|
|
7461
|
-
if (
|
|
7462
|
-
descriptor = await provider.
|
|
8179
|
+
const current = hasPeekableDescriptor(provider) ? await provider.getCurrentSigningDescriptor() : void 0;
|
|
8180
|
+
if (current === void 0) {
|
|
8181
|
+
const descriptor = await provider.getNextSigningDescriptor();
|
|
8182
|
+
return {
|
|
8183
|
+
rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
|
|
8184
|
+
receivePubkey: deriveLeafPubkey(descriptor)
|
|
8185
|
+
};
|
|
7463
8186
|
}
|
|
7464
|
-
descriptor ??= await provider.getNextSigningDescriptor();
|
|
7465
8187
|
return {
|
|
7466
8188
|
rotator: new _WalletReceiveRotator(provider, void 0, opts.logger),
|
|
7467
|
-
receivePubkey: deriveLeafPubkey(
|
|
8189
|
+
receivePubkey: opts.baselineReceivePubKey ?? deriveLeafPubkey(current)
|
|
7468
8190
|
};
|
|
7469
8191
|
}
|
|
7470
8192
|
/**
|
|
@@ -7527,6 +8249,22 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
|
|
|
7527
8249
|
async drain() {
|
|
7528
8250
|
await this.chain.catch(() => void 0);
|
|
7529
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
|
+
}
|
|
7530
8268
|
/**
|
|
7531
8269
|
* Tear down the subscription first so no late `vtxo_received` event
|
|
7532
8270
|
* can queue work on a disposing wallet, then drain any in-flight
|
|
@@ -7697,7 +8435,7 @@ var DescriptorSigningProviderMissingError = class extends Error {
|
|
|
7697
8435
|
};
|
|
7698
8436
|
|
|
7699
8437
|
// src/wallet/inputSignerRouter.ts
|
|
7700
|
-
var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate"]);
|
|
8438
|
+
var DESCRIPTOR_CAPABLE_CONTRACT_TYPES = /* @__PURE__ */ new Set(["default", "delegate", "boarding"]);
|
|
7701
8439
|
var InputSignerRouter = class {
|
|
7702
8440
|
constructor(deps) {
|
|
7703
8441
|
this.deps = deps;
|
|
@@ -7829,6 +8567,11 @@ function extractArkProviderUrl(provider) {
|
|
|
7829
8567
|
return typeof serverUrl === "string" && serverUrl.length > 0 ? serverUrl : void 0;
|
|
7830
8568
|
}
|
|
7831
8569
|
var MAINNET_UNILATERAL_EXIT_DELAY = 605184n;
|
|
8570
|
+
function toXOnlyPubKey(pubkey) {
|
|
8571
|
+
if (pubkey.length === 33) return pubkey.slice(1);
|
|
8572
|
+
if (pubkey.length === 32) return pubkey;
|
|
8573
|
+
throw new Error(`invalid signer pubkey length: expected 32 or 33, got ${pubkey.length}`);
|
|
8574
|
+
}
|
|
7832
8575
|
function delayToTimelock(delay) {
|
|
7833
8576
|
return {
|
|
7834
8577
|
value: delay,
|
|
@@ -7846,20 +8589,30 @@ function dedupeTimelocks(timelocks) {
|
|
|
7846
8589
|
}
|
|
7847
8590
|
return deduped;
|
|
7848
8591
|
}
|
|
7849
|
-
function areSameScriptBaselineTypesCompatible(existingType, requestedType) {
|
|
7850
|
-
if (existingType === requestedType) return true;
|
|
7851
|
-
return existingType === "default" && requestedType === "boarding" || existingType === "boarding" && requestedType === "default";
|
|
7852
|
-
}
|
|
7853
8592
|
async function ensureWalletContract(manager, params) {
|
|
7854
|
-
const [existing] = await manager.getContracts({ script: params.script });
|
|
7855
|
-
if (existing && existing.type !== params.type && areSameScriptBaselineTypesCompatible(existing.type, params.type)) {
|
|
7856
|
-
if (params.type === "default" && existing.type === "boarding") {
|
|
7857
|
-
await manager.updateContract(params.script, { type: "default" });
|
|
7858
|
-
}
|
|
7859
|
-
return;
|
|
7860
|
-
}
|
|
7861
8593
|
await manager.createContract(params);
|
|
7862
8594
|
}
|
|
8595
|
+
async function resolveBoardingBootTapscript(contractRepository, serverPubKey, baseline) {
|
|
8596
|
+
const serverPubKeyHex = hex.encode(serverPubKey);
|
|
8597
|
+
const candidates = await contractRepository.getContracts({
|
|
8598
|
+
type: ["boarding"],
|
|
8599
|
+
state: "active"
|
|
8600
|
+
});
|
|
8601
|
+
const newest = candidates.filter(
|
|
8602
|
+
(c) => c.params.serverPubKey === serverPubKeyHex && c.metadata?.source === WALLET_RECEIVE_SOURCE
|
|
8603
|
+
).sort((a, b) => {
|
|
8604
|
+
if (b.createdAt !== a.createdAt) return b.createdAt - a.createdAt;
|
|
8605
|
+
return signingDescriptorIndex(b.metadata?.signingDescriptor) - signingDescriptorIndex(a.metadata?.signingDescriptor);
|
|
8606
|
+
})[0];
|
|
8607
|
+
if (!newest?.params.pubKey) return baseline;
|
|
8608
|
+
try {
|
|
8609
|
+
const pubKey = hex.decode(newest.params.pubKey);
|
|
8610
|
+
return new DefaultVtxo.Script({ ...baseline.options, pubKey });
|
|
8611
|
+
} catch (e) {
|
|
8612
|
+
console.warn("Skipping malformed boarding contract at boot", newest.script, e);
|
|
8613
|
+
return baseline;
|
|
8614
|
+
}
|
|
8615
|
+
}
|
|
7863
8616
|
function hasToReadonly(identity) {
|
|
7864
8617
|
return typeof identity === "object" && identity !== null && "toReadonly" in identity && typeof identity.toReadonly === "function";
|
|
7865
8618
|
}
|
|
@@ -7869,8 +8622,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7869
8622
|
this.network = network;
|
|
7870
8623
|
this.onchainProvider = onchainProvider;
|
|
7871
8624
|
this.indexerProvider = indexerProvider;
|
|
7872
|
-
this.arkServerPublicKey = arkServerPublicKey;
|
|
7873
|
-
this.boardingTapscript = boardingTapscript;
|
|
7874
8625
|
this.dustAmount = dustAmount;
|
|
7875
8626
|
this.walletRepository = walletRepository;
|
|
7876
8627
|
this.contractRepository = contractRepository;
|
|
@@ -7886,6 +8637,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7886
8637
|
}
|
|
7887
8638
|
}
|
|
7888
8639
|
this._offchainTapscript = offchainTapscript;
|
|
8640
|
+
this._boardingTapscript = boardingTapscript;
|
|
8641
|
+
this._arkServerPublicKey = arkServerPublicKey;
|
|
7889
8642
|
this.watcherConfig = watcherConfig;
|
|
7890
8643
|
this._assetManager = new ReadonlyAssetManager(this.indexerProvider);
|
|
7891
8644
|
this.walletContractTimelocks = walletContractTimelocks && walletContractTimelocks.length > 0 ? dedupeTimelocks(walletContractTimelocks) : [this.offchainTapscript.options.csvTimelock];
|
|
@@ -7894,7 +8647,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7894
8647
|
_contractManagerInitializing;
|
|
7895
8648
|
watcherConfig;
|
|
7896
8649
|
_assetManager;
|
|
7897
|
-
_syncVtxosInflight;
|
|
7898
8650
|
walletContractTimelocks;
|
|
7899
8651
|
// Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
|
|
7900
8652
|
// from getVtxos() so concurrent callers (UI, VtxoManager auto-renewal,
|
|
@@ -7912,6 +8664,70 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7912
8664
|
* {@link WalletReceiveRotator.rotate} is the sole intended caller of.
|
|
7913
8665
|
*/
|
|
7914
8666
|
_offchainTapscript;
|
|
8667
|
+
/**
|
|
8668
|
+
* Backing field for the current boarding tapscript (the QR / onboarding
|
|
8669
|
+
* target). Read via the public `boardingTapscript` getter; written only
|
|
8670
|
+
* by {@link Wallet.setBoardingTapscriptForRotation}, the sanctioned
|
|
8671
|
+
* boarding-rotation write path (analogue of `_offchainTapscript`). It is
|
|
8672
|
+
* a *current value*, not a fixed setup constant, because per-derivation
|
|
8673
|
+
* boarding rotation (plan §6-II) swaps it when a fresh boarding address
|
|
8674
|
+
* is explicitly allocated. Static / `auto` wallets never rotate it, so
|
|
8675
|
+
* it stays the index-0 baseline for their lifetime.
|
|
8676
|
+
*/
|
|
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
|
+
}
|
|
7915
8731
|
/**
|
|
7916
8732
|
* Currently-active receive tapscript. Read-only from the outside;
|
|
7917
8733
|
* mutated only via {@link Wallet.setOffchainTapscriptForRotation}
|
|
@@ -7921,13 +8737,69 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
7921
8737
|
return this._offchainTapscript;
|
|
7922
8738
|
}
|
|
7923
8739
|
/**
|
|
7924
|
-
*
|
|
7925
|
-
*
|
|
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.
|
|
7926
8745
|
*/
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
8746
|
+
get arkServerPublicKey() {
|
|
8747
|
+
return this._arkServerPublicKey;
|
|
8748
|
+
}
|
|
8749
|
+
/**
|
|
8750
|
+
* The wallet's current boarding tapscript (the on-chain onboarding
|
|
8751
|
+
* target). Read-only from the outside; mutated only via
|
|
8752
|
+
* {@link Wallet.setBoardingTapscriptForRotation} when a fresh boarding
|
|
8753
|
+
* address is explicitly allocated. Single-valued for static / `auto`
|
|
8754
|
+
* wallets.
|
|
8755
|
+
*/
|
|
8756
|
+
get boardingTapscript() {
|
|
8757
|
+
return this._boardingTapscript;
|
|
8758
|
+
}
|
|
8759
|
+
/**
|
|
8760
|
+
* Listeners fired after the boarding tapscript rotates to a fresh index
|
|
8761
|
+
* (see {@link Wallet.setBoardingTapscriptForRotation}). A live
|
|
8762
|
+
* {@link notifyIncomingFunds} onchain watcher registers one so it can
|
|
8763
|
+
* re-subscribe to include the newly allocated boarding address within the
|
|
8764
|
+
* same session — without it, a deposit to the fresh address wouldn't fire
|
|
8765
|
+
* a notification until the watcher's next re-init. Always empty for
|
|
8766
|
+
* readonly / static / `auto` wallets, which never rotate boarding.
|
|
8767
|
+
*/
|
|
8768
|
+
_boardingRotationListeners = /* @__PURE__ */ new Set();
|
|
8769
|
+
/**
|
|
8770
|
+
* Register a listener invoked synchronously after each boarding rotation.
|
|
8771
|
+
* Returns an unsubscribe function. Protected: only internal subscribers
|
|
8772
|
+
* (the incoming-funds watcher) participate.
|
|
8773
|
+
*/
|
|
8774
|
+
onBoardingRotation(listener) {
|
|
8775
|
+
this._boardingRotationListeners.add(listener);
|
|
8776
|
+
return () => {
|
|
8777
|
+
this._boardingRotationListeners.delete(listener);
|
|
8778
|
+
};
|
|
8779
|
+
}
|
|
8780
|
+
/**
|
|
8781
|
+
* Notify boarding-rotation listeners. Called by the boarding-rotation
|
|
8782
|
+
* write path ({@link Wallet.setBoardingTapscriptForRotation}) once the new
|
|
8783
|
+
* tapscript is in place. A throwing listener is isolated so it can neither
|
|
8784
|
+
* break the rotation nor starve sibling listeners.
|
|
8785
|
+
*/
|
|
8786
|
+
notifyBoardingRotation() {
|
|
8787
|
+
for (const listener of this._boardingRotationListeners) {
|
|
8788
|
+
try {
|
|
8789
|
+
listener();
|
|
8790
|
+
} catch (e) {
|
|
8791
|
+
console.warn("Boarding-rotation listener failed", e);
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
8795
|
+
/**
|
|
8796
|
+
* Protected helper to set up shared wallet configuration.
|
|
8797
|
+
* Extracts common logic used by both ReadonlyWallet.create() and Wallet.create().
|
|
8798
|
+
*/
|
|
8799
|
+
static async setupWalletConfig(config, pubKey) {
|
|
8800
|
+
const arkadeServerUrl = getArkadeServerUrl(config);
|
|
8801
|
+
const arkProvider = config.arkProvider || new RestArkProvider(arkadeServerUrl);
|
|
8802
|
+
let indexerProvider = config.indexerProvider;
|
|
7931
8803
|
if (!indexerProvider) {
|
|
7932
8804
|
let indexerUrl = config.indexerUrl;
|
|
7933
8805
|
if (!indexerUrl) {
|
|
@@ -8032,7 +8904,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8032
8904
|
throw new Error("Invalid configured public key");
|
|
8033
8905
|
}
|
|
8034
8906
|
const setup = await _ReadonlyWallet.setupWalletConfig(config, pubkey);
|
|
8035
|
-
|
|
8907
|
+
const wallet = new _ReadonlyWallet(
|
|
8036
8908
|
config.identity,
|
|
8037
8909
|
setup.network,
|
|
8038
8910
|
setup.onchainProvider,
|
|
@@ -8047,6 +8919,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8047
8919
|
config.watcherConfig,
|
|
8048
8920
|
setup.walletContractTimelocks
|
|
8049
8921
|
);
|
|
8922
|
+
wallet.refreshDeprecatedSigners(setup.info);
|
|
8923
|
+
return wallet;
|
|
8050
8924
|
}
|
|
8051
8925
|
get arkAddress() {
|
|
8052
8926
|
return this.offchainTapscript.address(this.network.hrp, this.arkServerPublicKey);
|
|
@@ -8070,10 +8944,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8070
8944
|
* Return the wallet's combined onchain and offchain balances.
|
|
8071
8945
|
*/
|
|
8072
8946
|
async getBalance() {
|
|
8073
|
-
const [boardingUtxos, vtxos] = await Promise.all([
|
|
8947
|
+
const [boardingUtxos, vtxos, pendingOutpoints] = await Promise.all([
|
|
8074
8948
|
this.getBoardingUtxos(),
|
|
8075
|
-
this.getVtxos()
|
|
8949
|
+
this.getVtxos(),
|
|
8950
|
+
this.pendingRecoveryOutpoints()
|
|
8076
8951
|
]);
|
|
8952
|
+
const isPendingRecovery = (coin) => pendingOutpoints.has(`${coin.txid}:${coin.vout}`);
|
|
8077
8953
|
let confirmed = 0;
|
|
8078
8954
|
let unconfirmed = 0;
|
|
8079
8955
|
for (const utxo of boardingUtxos) {
|
|
@@ -8086,11 +8962,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8086
8962
|
let settled = 0;
|
|
8087
8963
|
let preconfirmed = 0;
|
|
8088
8964
|
let recoverable = 0;
|
|
8089
|
-
|
|
8090
|
-
|
|
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);
|
|
8091
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);
|
|
8092
8972
|
const totalBoarding = confirmed + unconfirmed;
|
|
8093
|
-
const totalOffchain = settled + preconfirmed + recoverable;
|
|
8973
|
+
const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
|
|
8094
8974
|
const assetBalances = /* @__PURE__ */ new Map();
|
|
8095
8975
|
for (const vtxo of vtxos) {
|
|
8096
8976
|
if (!isSpendable(vtxo)) continue;
|
|
@@ -8115,6 +8995,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8115
8995
|
preconfirmed,
|
|
8116
8996
|
available: settled + preconfirmed,
|
|
8117
8997
|
recoverable,
|
|
8998
|
+
pendingRecovery,
|
|
8118
8999
|
total: totalBoarding + totalOffchain,
|
|
8119
9000
|
assets
|
|
8120
9001
|
};
|
|
@@ -8141,6 +9022,23 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8141
9022
|
return !!(f.withUnrolled && vtxo.isUnrolled);
|
|
8142
9023
|
});
|
|
8143
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
|
+
}
|
|
8144
9042
|
/**
|
|
8145
9043
|
* Return wallet transaction history derived from Arkade state and boarding transactions.
|
|
8146
9044
|
*/
|
|
@@ -8160,43 +9058,59 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8160
9058
|
await clearSyncCursor(this.walletRepository);
|
|
8161
9059
|
}
|
|
8162
9060
|
/**
|
|
8163
|
-
*
|
|
9061
|
+
* The on-chain (P2TR) addresses of every boarding tapscript this wallet
|
|
9062
|
+
* uses — the current address plus any historical rotated boarding
|
|
9063
|
+
* addresses. The aggregating boarding readers (history, notifications) fan
|
|
9064
|
+
* out over this set so deposits at previous boarding addresses are still
|
|
9065
|
+
* surfaced (plan §6-IV); {@link getBoardingAddress} stays single-valued.
|
|
9066
|
+
*/
|
|
9067
|
+
async getBoardingAddresses() {
|
|
9068
|
+
const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
|
|
9069
|
+
return tapscripts.map((t) => t.onchainAddress(this.network));
|
|
9070
|
+
}
|
|
9071
|
+
/**
|
|
9072
|
+
* Build a transaction history view across the wallet's boarding addresses
|
|
9073
|
+
* (current + historical rotated; plan §6-IV.1).
|
|
8164
9074
|
*/
|
|
8165
9075
|
async getBoardingTxs() {
|
|
8166
9076
|
const utxos = [];
|
|
8167
9077
|
const commitmentsToIgnore = /* @__PURE__ */ new Set();
|
|
8168
|
-
const
|
|
8169
|
-
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
9078
|
+
const tapscripts = await this.getBoardingTapscripts(this.watchedBoardingSigners());
|
|
8170
9079
|
const outspendCache = /* @__PURE__ */ new Map();
|
|
8171
|
-
for (const
|
|
8172
|
-
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
9080
|
+
for (const tapscript of tapscripts) {
|
|
9081
|
+
const boardingAddress = tapscript.onchainAddress(this.network);
|
|
9082
|
+
const scriptHex = hex.encode(tapscript.pkScript);
|
|
9083
|
+
const txs = await this.onchainProvider.getTransactions(boardingAddress);
|
|
9084
|
+
for (const tx of txs) {
|
|
9085
|
+
for (let i = 0; i < tx.vout.length; i++) {
|
|
9086
|
+
const vout = tx.vout[i];
|
|
9087
|
+
if (vout.scriptpubkey_address === boardingAddress) {
|
|
9088
|
+
let spentStatuses = outspendCache.get(tx.txid);
|
|
9089
|
+
if (!spentStatuses) {
|
|
9090
|
+
spentStatuses = await this.onchainProvider.getTxOutspends(tx.txid);
|
|
9091
|
+
outspendCache.set(tx.txid, spentStatuses);
|
|
9092
|
+
}
|
|
9093
|
+
const spentStatus = spentStatuses[i];
|
|
9094
|
+
if (spentStatus?.spent) {
|
|
9095
|
+
commitmentsToIgnore.add(spentStatus.txid);
|
|
9096
|
+
}
|
|
9097
|
+
utxos.push({
|
|
9098
|
+
txid: tx.txid,
|
|
9099
|
+
vout: i,
|
|
9100
|
+
value: Number(vout.value),
|
|
9101
|
+
status: {
|
|
9102
|
+
confirmed: tx.status.confirmed,
|
|
9103
|
+
block_time: tx.status.block_time
|
|
9104
|
+
},
|
|
9105
|
+
isUnrolled: true,
|
|
9106
|
+
virtualStatus: {
|
|
9107
|
+
state: spentStatus?.spent ? "spent" : "settled",
|
|
9108
|
+
commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
|
|
9109
|
+
},
|
|
9110
|
+
createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
|
|
9111
|
+
script: scriptHex
|
|
9112
|
+
});
|
|
8183
9113
|
}
|
|
8184
|
-
utxos.push({
|
|
8185
|
-
txid: tx.txid,
|
|
8186
|
-
vout: i,
|
|
8187
|
-
value: Number(vout.value),
|
|
8188
|
-
status: {
|
|
8189
|
-
confirmed: tx.status.confirmed,
|
|
8190
|
-
block_time: tx.status.block_time
|
|
8191
|
-
},
|
|
8192
|
-
isUnrolled: true,
|
|
8193
|
-
virtualStatus: {
|
|
8194
|
-
state: spentStatus?.spent ? "spent" : "settled",
|
|
8195
|
-
commitmentTxIds: spentStatus?.spent ? [spentStatus.txid] : void 0
|
|
8196
|
-
},
|
|
8197
|
-
createdAt: tx.status.confirmed ? new Date(tx.status.block_time * 1e3) : /* @__PURE__ */ new Date(0),
|
|
8198
|
-
script: hex.encode(this.boardingTapscript.pkScript)
|
|
8199
|
-
});
|
|
8200
9114
|
}
|
|
8201
9115
|
}
|
|
8202
9116
|
}
|
|
@@ -8226,48 +9140,176 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8226
9140
|
};
|
|
8227
9141
|
}
|
|
8228
9142
|
/**
|
|
8229
|
-
*
|
|
9143
|
+
* The set of boarding tapscripts whose on-chain UTXOs belong to this
|
|
9144
|
+
* wallet — the current display tapscript plus every historical boarding
|
|
9145
|
+
* address it has used. Under per-derivation rotation (plan §6-II) a wallet
|
|
9146
|
+
* can hold unspent boarding UTXOs at several addresses at once, so fund
|
|
9147
|
+
* discovery / spending must enumerate them all, not just the current one
|
|
9148
|
+
* (plan §6-III.1). Deduplicated by scriptPubKey.
|
|
9149
|
+
*
|
|
9150
|
+
* Always includes the index-0 baseline (identity x-only key), which covers
|
|
9151
|
+
* the degenerate equal-delay case where the index-0 boarding row is
|
|
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.
|
|
8230
9160
|
*/
|
|
8231
|
-
async
|
|
8232
|
-
const
|
|
8233
|
-
const
|
|
8234
|
-
const
|
|
8235
|
-
|
|
9161
|
+
async getBoardingTapscripts(allowedSigners) {
|
|
9162
|
+
const byScript = /* @__PURE__ */ new Map();
|
|
9163
|
+
const add = (s) => byScript.set(hex.encode(s.pkScript), s);
|
|
9164
|
+
const boardingCsv = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
9165
|
+
add(
|
|
9166
|
+
new DefaultVtxo.Script({
|
|
9167
|
+
pubKey: await this.identity.xOnlyPublicKey(),
|
|
9168
|
+
serverPubKey: this.boardingTapscript.options.serverPubKey,
|
|
9169
|
+
csvTimelock: boardingCsv
|
|
9170
|
+
})
|
|
9171
|
+
);
|
|
9172
|
+
add(this.boardingTapscript);
|
|
9173
|
+
const serverPubKeyHex = hex.encode(this.boardingTapscript.options.serverPubKey);
|
|
9174
|
+
const allowed = allowedSigners ?? /* @__PURE__ */ new Set([toXOnlySignerHex(serverPubKeyHex)]);
|
|
9175
|
+
const boardingContracts = await this.contractRepository.getContracts({
|
|
9176
|
+
type: ["boarding"]
|
|
8236
9177
|
});
|
|
8237
|
-
|
|
8238
|
-
|
|
9178
|
+
for (const c of boardingContracts) {
|
|
9179
|
+
if (!allowed.has(toXOnlySignerHex(c.params.serverPubKey))) continue;
|
|
9180
|
+
try {
|
|
9181
|
+
add(BoardingContractHandler.createScript(c.params));
|
|
9182
|
+
} catch (e) {
|
|
9183
|
+
console.warn("Skipping malformed boarding contract", c.script, e);
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
return [...byScript.values()];
|
|
9187
|
+
}
|
|
9188
|
+
/**
|
|
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}).
|
|
9202
|
+
*/
|
|
9203
|
+
async getBoardingUtxosForSigners(allowedSigners) {
|
|
9204
|
+
const tapscripts = await this.getBoardingTapscripts(allowedSigners);
|
|
9205
|
+
const groups = [];
|
|
9206
|
+
for (const tapscript of tapscripts) {
|
|
9207
|
+
const address = tapscript.onchainAddress(this.network);
|
|
9208
|
+
const coins = await this.onchainProvider.getCoins(address);
|
|
9209
|
+
const utxos = coins.map((utxo) => extendCoinWithTapscript(tapscript, utxo));
|
|
9210
|
+
await this.walletRepository.saveUtxos(address, utxos);
|
|
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
|
+
});
|
|
9222
|
+
}
|
|
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);
|
|
8239
9244
|
}
|
|
8240
9245
|
/**
|
|
8241
9246
|
* Subscribe to onchain and offchain notifications for newly received funds.
|
|
8242
9247
|
*
|
|
9248
|
+
* The onchain watcher tracks the full boarding-address set (current +
|
|
9249
|
+
* historical rotated). When boarding rotates *after* subscribing — e.g.
|
|
9250
|
+
* rotate-on-board allocates a fresh address via
|
|
9251
|
+
* {@link getNewBoardingAddress} — the watcher automatically re-subscribes
|
|
9252
|
+
* to widen its set, so a deposit to the new address fires a notification
|
|
9253
|
+
* within the same session (no watcher re-init required). The re-subscribe
|
|
9254
|
+
* is driven by {@link onBoardingRotation}; static / `auto` / readonly
|
|
9255
|
+
* wallets never rotate boarding, so it never fires for them.
|
|
9256
|
+
*
|
|
8243
9257
|
* @param eventCallback - Callback invoked when matching funds are detected
|
|
8244
9258
|
* @returns A function that stops the subscriptions
|
|
8245
9259
|
*/
|
|
8246
9260
|
async notifyIncomingFunds(eventCallback) {
|
|
8247
9261
|
const arkAddress = await this.getAddress();
|
|
8248
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
8249
9262
|
let onchainStopFunc;
|
|
8250
9263
|
let indexerStopFunc;
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
(
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
9264
|
+
let boardingRotationStopFunc;
|
|
9265
|
+
let stopped = false;
|
|
9266
|
+
let onchainChain = Promise.resolve();
|
|
9267
|
+
const subscribeOnchain = () => {
|
|
9268
|
+
onchainChain = onchainChain.then(async () => {
|
|
9269
|
+
if (stopped || !this.onchainProvider) return;
|
|
9270
|
+
const boardingAddresses = await this.getBoardingAddresses();
|
|
9271
|
+
if (boardingAddresses.length === 0) return;
|
|
9272
|
+
const boardingAddressSet = new Set(boardingAddresses);
|
|
9273
|
+
const previousStop = onchainStopFunc;
|
|
9274
|
+
const stop = await this.onchainProvider.watchAddresses(
|
|
9275
|
+
boardingAddresses,
|
|
9276
|
+
(txs) => {
|
|
9277
|
+
const coins = txs.flatMap((tx) => {
|
|
9278
|
+
const { txid, status } = tx;
|
|
9279
|
+
const matched = [];
|
|
9280
|
+
tx.vout.forEach((v, vout) => {
|
|
9281
|
+
if (boardingAddressSet.has(v.scriptpubkey_address)) {
|
|
9282
|
+
matched.push({
|
|
9283
|
+
txid,
|
|
9284
|
+
vout,
|
|
9285
|
+
value: Number(v.value),
|
|
9286
|
+
status
|
|
9287
|
+
});
|
|
9288
|
+
}
|
|
9289
|
+
});
|
|
9290
|
+
return matched;
|
|
9291
|
+
});
|
|
9292
|
+
eventCallback({
|
|
9293
|
+
type: "utxo",
|
|
9294
|
+
coins
|
|
9295
|
+
});
|
|
9296
|
+
}
|
|
9297
|
+
);
|
|
9298
|
+
if (stopped) {
|
|
9299
|
+
stop();
|
|
9300
|
+
return;
|
|
8268
9301
|
}
|
|
8269
|
-
|
|
8270
|
-
|
|
9302
|
+
onchainStopFunc = stop;
|
|
9303
|
+
previousStop?.();
|
|
9304
|
+
}).catch((e) => {
|
|
9305
|
+
console.warn("Failed to (re)subscribe boarding-funds watcher", e);
|
|
9306
|
+
});
|
|
9307
|
+
return onchainChain;
|
|
9308
|
+
};
|
|
9309
|
+
boardingRotationStopFunc = this.onBoardingRotation(() => {
|
|
9310
|
+
void subscribeOnchain();
|
|
9311
|
+
});
|
|
9312
|
+
await subscribeOnchain();
|
|
8271
9313
|
if (this.indexerProvider && arkAddress) {
|
|
8272
9314
|
const cm = await this.getContractManager();
|
|
8273
9315
|
let annotationQueue = Promise.resolve();
|
|
@@ -8296,7 +9338,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8296
9338
|
});
|
|
8297
9339
|
}
|
|
8298
9340
|
const stopFunc = () => {
|
|
9341
|
+
stopped = true;
|
|
9342
|
+
boardingRotationStopFunc?.();
|
|
8299
9343
|
onchainStopFunc?.();
|
|
9344
|
+
onchainStopFunc = void 0;
|
|
8300
9345
|
indexerStopFunc?.();
|
|
8301
9346
|
};
|
|
8302
9347
|
return stopFunc;
|
|
@@ -8402,60 +9447,82 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8402
9447
|
watcherConfig: this.watcherConfig
|
|
8403
9448
|
});
|
|
8404
9449
|
const baselinePubkey = await this.identity.xOnlyPublicKey();
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
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({
|
|
8408
9507
|
pubKey: baselinePubkey,
|
|
8409
|
-
serverPubKey
|
|
8410
|
-
csvTimelock
|
|
9508
|
+
serverPubKey,
|
|
9509
|
+
csvTimelock: boardingCsvTimelock
|
|
8411
9510
|
});
|
|
8412
|
-
const
|
|
9511
|
+
const boardingScriptHex = hex.encode(baselineBoarding.pkScript);
|
|
9512
|
+
if (seenBaselineScripts.has(boardingScriptHex)) continue;
|
|
9513
|
+
seenBaselineScripts.add(boardingScriptHex);
|
|
8413
9514
|
await ensureWalletContract(manager, {
|
|
8414
|
-
type: "
|
|
9515
|
+
type: "boarding",
|
|
8415
9516
|
params: {
|
|
8416
|
-
pubKey: hex.encode(
|
|
8417
|
-
serverPubKey: hex.encode(
|
|
8418
|
-
csvTimelock:
|
|
9517
|
+
pubKey: hex.encode(baselineBoarding.options.pubKey),
|
|
9518
|
+
serverPubKey: hex.encode(serverPubKey),
|
|
9519
|
+
csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
|
|
8419
9520
|
},
|
|
8420
|
-
script:
|
|
8421
|
-
address:
|
|
9521
|
+
script: boardingScriptHex,
|
|
9522
|
+
address: baselineBoarding.address(this.network.hrp, serverPubKey).encode(),
|
|
8422
9523
|
state: "active"
|
|
8423
9524
|
});
|
|
8424
|
-
if (this.offchainTapscript instanceof DelegateVtxo.Script) {
|
|
8425
|
-
const delegateScript = new DelegateVtxo.Script({
|
|
8426
|
-
pubKey: baselinePubkey,
|
|
8427
|
-
serverPubKey: this.offchainTapscript.options.serverPubKey,
|
|
8428
|
-
delegatePubKey: this.offchainTapscript.options.delegatePubKey,
|
|
8429
|
-
csvTimelock
|
|
8430
|
-
});
|
|
8431
|
-
const delegateScriptHex = hex.encode(delegateScript.pkScript);
|
|
8432
|
-
await manager.createContract({
|
|
8433
|
-
type: "delegate",
|
|
8434
|
-
params: {
|
|
8435
|
-
pubKey: hex.encode(delegateScript.options.pubKey),
|
|
8436
|
-
serverPubKey: hex.encode(delegateScript.options.serverPubKey),
|
|
8437
|
-
delegatePubKey: hex.encode(delegateScript.options.delegatePubKey),
|
|
8438
|
-
csvTimelock: csvTimelockStr
|
|
8439
|
-
},
|
|
8440
|
-
script: delegateScriptHex,
|
|
8441
|
-
address: delegateScript.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
8442
|
-
state: "active"
|
|
8443
|
-
});
|
|
8444
|
-
}
|
|
8445
9525
|
}
|
|
8446
|
-
const boardingScriptHex = hex.encode(this.boardingTapscript.pkScript);
|
|
8447
|
-
const boardingCsvTimelock = this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
8448
|
-
await ensureWalletContract(manager, {
|
|
8449
|
-
type: "boarding",
|
|
8450
|
-
params: {
|
|
8451
|
-
pubKey: hex.encode(this.boardingTapscript.options.pubKey),
|
|
8452
|
-
serverPubKey: hex.encode(this.boardingTapscript.options.serverPubKey),
|
|
8453
|
-
csvTimelock: timelockToSequence(boardingCsvTimelock).toString()
|
|
8454
|
-
},
|
|
8455
|
-
script: boardingScriptHex,
|
|
8456
|
-
address: this.boardingTapscript.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
8457
|
-
state: "active"
|
|
8458
|
-
});
|
|
8459
9526
|
return manager;
|
|
8460
9527
|
}
|
|
8461
9528
|
/** Dispose wallet-owned managers and release background resources. */
|
|
@@ -8488,7 +9555,6 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8488
9555
|
walletContractTimelocks
|
|
8489
9556
|
);
|
|
8490
9557
|
this.arkProvider = arkProvider;
|
|
8491
|
-
this.serverUnrollScript = serverUnrollScript;
|
|
8492
9558
|
this.forfeitOutputScript = forfeitOutputScript;
|
|
8493
9559
|
this.forfeitPubkey = forfeitPubkey;
|
|
8494
9560
|
this.identity = identity;
|
|
@@ -8509,6 +9575,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8509
9575
|
this.settlementConfig = { ...DEFAULT_SETTLEMENT_CONFIG };
|
|
8510
9576
|
}
|
|
8511
9577
|
this._delegateManager = delegateProvider ? new DelegateManagerImpl(delegateProvider, arkProvider, identity) : void 0;
|
|
9578
|
+
this._serverUnrollScript = serverUnrollScript;
|
|
8512
9579
|
this._receiveRotator = receiveRotator;
|
|
8513
9580
|
this._descriptorProvider = descriptorProvider;
|
|
8514
9581
|
this._signerRouter = new InputSignerRouter({
|
|
@@ -8534,6 +9601,43 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8534
9601
|
* the contract manager is up first.
|
|
8535
9602
|
*/
|
|
8536
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
|
+
}
|
|
8537
9641
|
_receiveRotatorInstalled = false;
|
|
8538
9642
|
/**
|
|
8539
9643
|
* Descriptor-aware signer used by {@link _signerRouter} to sign
|
|
@@ -8553,6 +9657,230 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8553
9657
|
setOffchainTapscriptForRotation(tapscript) {
|
|
8554
9658
|
this._offchainTapscript = tapscript;
|
|
8555
9659
|
}
|
|
9660
|
+
/**
|
|
9661
|
+
* @internal Sole write path for `boardingTapscript` after construction.
|
|
9662
|
+
* Called by {@link Wallet.getNewBoardingAddress} once the rotated
|
|
9663
|
+
* boarding contract has been persisted. External code must treat
|
|
9664
|
+
* `boardingTapscript` as read-only.
|
|
9665
|
+
*/
|
|
9666
|
+
setBoardingTapscriptForRotation(tapscript) {
|
|
9667
|
+
this._boardingTapscript = tapscript;
|
|
9668
|
+
this.notifyBoardingRotation();
|
|
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();
|
|
9708
|
+
/**
|
|
9709
|
+
* Allocate and return a *fresh* on-chain boarding address, rotating the
|
|
9710
|
+
* wallet's current boarding tapscript to a new HD index.
|
|
9711
|
+
*
|
|
9712
|
+
* This is the explicit boarding allocator — the analogue of dotnet's
|
|
9713
|
+
* `GetNextContract(NextContractPurpose.Boarding)`. Unlike
|
|
9714
|
+
* {@link getBoardingAddress} (a stable read of the current display
|
|
9715
|
+
* address that never burns an index), each call here:
|
|
9716
|
+
*
|
|
9717
|
+
* - allocates the next index from the shared HD stream (so boarding and
|
|
9718
|
+
* L2 receive interleave on one monotonic index);
|
|
9719
|
+
* - builds the boarding tapscript at that index with the boarding-exit
|
|
9720
|
+
* CSV;
|
|
9721
|
+
* - persists an `active` `boarding` contract tagged
|
|
9722
|
+
* {@link WALLET_RECEIVE_SOURCE} (with its `signingDescriptor`) so the
|
|
9723
|
+
* ContractWatcher monitors it, boot can restore it as the current
|
|
9724
|
+
* boarding address, and descriptor-aware signing can recover the
|
|
9725
|
+
* per-index key;
|
|
9726
|
+
* - swaps the wallet's current `boardingTapscript`.
|
|
9727
|
+
*
|
|
9728
|
+
* Gated by `walletMode`: a static / `auto` wallet has no descriptor
|
|
9729
|
+
* provider and keeps a single index-0 boarding address for its lifetime,
|
|
9730
|
+
* so this returns the existing {@link getBoardingAddress} unchanged
|
|
9731
|
+
* (no rotation, no index burned).
|
|
9732
|
+
*/
|
|
9733
|
+
async getNewBoardingAddress() {
|
|
9734
|
+
const provider = this._descriptorProvider;
|
|
9735
|
+
if (!provider) {
|
|
9736
|
+
return this.getBoardingAddress();
|
|
9737
|
+
}
|
|
9738
|
+
const descriptor = await provider.getNextSigningDescriptor();
|
|
9739
|
+
const pubKey = deriveDescriptorLeafPubKey(descriptor);
|
|
9740
|
+
const newBoarding = new DefaultVtxo.Script({
|
|
9741
|
+
...this._boardingTapscript.options,
|
|
9742
|
+
pubKey
|
|
9743
|
+
});
|
|
9744
|
+
const csvTimelock = newBoarding.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
9745
|
+
const manager = await this.getContractManager();
|
|
9746
|
+
await manager.createContract({
|
|
9747
|
+
type: "boarding",
|
|
9748
|
+
params: {
|
|
9749
|
+
pubKey: hex.encode(pubKey),
|
|
9750
|
+
serverPubKey: hex.encode(newBoarding.options.serverPubKey),
|
|
9751
|
+
csvTimelock: timelockToSequence(csvTimelock).toString()
|
|
9752
|
+
},
|
|
9753
|
+
script: hex.encode(newBoarding.pkScript),
|
|
9754
|
+
address: newBoarding.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
9755
|
+
state: "active",
|
|
9756
|
+
metadata: {
|
|
9757
|
+
source: WALLET_RECEIVE_SOURCE,
|
|
9758
|
+
signingDescriptor: descriptor
|
|
9759
|
+
}
|
|
9760
|
+
});
|
|
9761
|
+
this.setBoardingTapscriptForRotation(newBoarding);
|
|
9762
|
+
return newBoarding.onchainAddress(this.network);
|
|
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
|
+
}
|
|
8556
9884
|
/**
|
|
8557
9885
|
* Async mutex that serializes all operations submitting VTXOs to the Arkade
|
|
8558
9886
|
* server (`settle`, `send`, `sendBitcoin`). This prevents VtxoManager's
|
|
@@ -8637,12 +9965,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8637
9965
|
const staticDescriptor = hd ? void 0 : `tr(${hex.encode(await this.identity.xOnlyPublicKey())})`;
|
|
8638
9966
|
const materialize = (index) => hd ? provider.materializeDescriptorAt(index) : staticDescriptor;
|
|
8639
9967
|
const delegatePubKey = this.offchainTapscript instanceof DelegateVtxo.Script ? this.offchainTapscript.options.delegatePubKey : void 0;
|
|
9968
|
+
const arkInfo = await this.arkProvider.getInfo();
|
|
9969
|
+
const currentSignerPubKey = toXOnlyPubKey(hex.decode(arkInfo.signerPubkey));
|
|
9970
|
+
const deprecatedSignerPubKeys = arkInfo.deprecatedSigners.map(
|
|
9971
|
+
(s) => toXOnlyPubKey(hex.decode(s.pubkey))
|
|
9972
|
+
);
|
|
8640
9973
|
const deps = {
|
|
8641
9974
|
indexerProvider: this.indexerProvider,
|
|
8642
9975
|
onchainProvider: this.onchainProvider,
|
|
8643
9976
|
network: { hrp: this.network.hrp },
|
|
8644
|
-
|
|
9977
|
+
// Full network for the boarding on-chain (P2TR) probe — the
|
|
9978
|
+
// `{ hrp }` shape above lacks the `bech32` data
|
|
9979
|
+
// `VtxoScript.onchainAddress` needs (plan §6-I.1).
|
|
9980
|
+
onchainNetwork: this.network,
|
|
9981
|
+
serverPubKey: currentSignerPubKey,
|
|
9982
|
+
deprecatedSignerPubKeys,
|
|
8645
9983
|
csvTimelocks: this.walletContractTimelocks,
|
|
9984
|
+
// Boarding-exit CSV so the boarding handler can build its
|
|
9985
|
+
// candidate script (distinct from the unilateral-exit matrix).
|
|
9986
|
+
boardingTimelock: this.boardingTapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK,
|
|
8646
9987
|
delegatePubKey
|
|
8647
9988
|
};
|
|
8648
9989
|
const result = await manager.scanContracts({
|
|
@@ -8700,6 +10041,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8700
10041
|
}
|
|
8701
10042
|
async dispose() {
|
|
8702
10043
|
await this._restoreInFlight?.catch(() => void 0);
|
|
10044
|
+
this._serverInfoUnsub?.();
|
|
10045
|
+
this._serverInfoUnsub = void 0;
|
|
10046
|
+
await this._serverInfoInFlight?.catch(() => void 0);
|
|
8703
10047
|
let rotatorError;
|
|
8704
10048
|
try {
|
|
8705
10049
|
await this._receiveRotator?.dispose();
|
|
@@ -8774,6 +10118,25 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8774
10118
|
boot?.rotator,
|
|
8775
10119
|
boot?.provider
|
|
8776
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
|
+
}
|
|
10130
|
+
if (boot?.provider) {
|
|
10131
|
+
const resolvedBoarding = await resolveBoardingBootTapscript(
|
|
10132
|
+
setup.contractRepository,
|
|
10133
|
+
setup.serverPubKey,
|
|
10134
|
+
setup.boardingTapscript
|
|
10135
|
+
);
|
|
10136
|
+
if (resolvedBoarding !== setup.boardingTapscript) {
|
|
10137
|
+
wallet.setBoardingTapscriptForRotation(resolvedBoarding);
|
|
10138
|
+
}
|
|
10139
|
+
}
|
|
8777
10140
|
await wallet.getVtxoManager();
|
|
8778
10141
|
return wallet;
|
|
8779
10142
|
}
|
|
@@ -8796,7 +10159,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8796
10159
|
*/
|
|
8797
10160
|
async toReadonly() {
|
|
8798
10161
|
const readonlyIdentity = hasToReadonly(this.identity) ? await this.identity.toReadonly() : this.identity;
|
|
8799
|
-
|
|
10162
|
+
const readonly = new ReadonlyWallet(
|
|
8800
10163
|
readonlyIdentity,
|
|
8801
10164
|
this.network,
|
|
8802
10165
|
this.onchainProvider,
|
|
@@ -8811,6 +10174,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8811
10174
|
this.watcherConfig,
|
|
8812
10175
|
this.walletContractTimelocks
|
|
8813
10176
|
);
|
|
10177
|
+
readonly._deprecatedSigners = new Map(this._deprecatedSigners);
|
|
10178
|
+
return readonly;
|
|
8814
10179
|
}
|
|
8815
10180
|
/** Returns the delegate manager when delegation support is configured. */
|
|
8816
10181
|
async getDelegateManager() {
|
|
@@ -8836,10 +10201,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8836
10201
|
if (params.selectedVtxos && params.selectedVtxos.length > 0) {
|
|
8837
10202
|
return this._withTxLock(async () => {
|
|
8838
10203
|
const offchainTapscript = this.offchainTapscript;
|
|
8839
|
-
const
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
);
|
|
10204
|
+
const serverPubKey = this.arkServerPublicKey;
|
|
10205
|
+
const serverUnrollScript = this.serverUnrollScript;
|
|
10206
|
+
const arkAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
|
|
8843
10207
|
const selectedVtxoSum = params.selectedVtxos.map((v) => v.value).reduce((a, b) => a + b, 0);
|
|
8844
10208
|
if (selectedVtxoSum < params.amount) {
|
|
8845
10209
|
throw new Error("Selected VTXOs do not cover specified amount");
|
|
@@ -8864,25 +10228,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8864
10228
|
amount: BigInt(selected.changeAmount)
|
|
8865
10229
|
});
|
|
8866
10230
|
}
|
|
8867
|
-
this.
|
|
8868
|
-
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
arkTxid,
|
|
8876
|
-
signedCheckpointTxs,
|
|
8877
|
-
params.amount,
|
|
8878
|
-
selected.changeAmount,
|
|
8879
|
-
selected.changeAmount > 0n ? outputs.length - 1 : 0,
|
|
8880
|
-
offchainTapscript
|
|
8881
|
-
);
|
|
8882
|
-
return arkTxid;
|
|
8883
|
-
} finally {
|
|
8884
|
-
this._removePendingSpends(selected.inputs);
|
|
8885
|
-
}
|
|
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
|
+
});
|
|
8886
10239
|
});
|
|
8887
10240
|
}
|
|
8888
10241
|
return this.send({
|
|
@@ -8912,8 +10265,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8912
10265
|
}
|
|
8913
10266
|
}
|
|
8914
10267
|
}
|
|
10268
|
+
const offchainAddress = await this.getAddress();
|
|
10269
|
+
const offchainPkScript = ArkAddress.decode(offchainAddress).pkScript;
|
|
10270
|
+
const offchainOutputScript = hex.encode(offchainPkScript);
|
|
8915
10271
|
if (!params) {
|
|
8916
|
-
const { fees } = await this.arkProvider.getInfo();
|
|
10272
|
+
const { fees, vtxoMaxAmount } = await this.arkProvider.getInfo();
|
|
8917
10273
|
const estimator = new Estimator(fees.intentFee);
|
|
8918
10274
|
let amount = 0;
|
|
8919
10275
|
const exitScript = CSVMultisigTapscript.decode(
|
|
@@ -8941,7 +10297,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8941
10297
|
}
|
|
8942
10298
|
const vtxos = await this.getVtxos({ withRecoverable: true });
|
|
8943
10299
|
const filteredVtxos = [];
|
|
8944
|
-
for (const vtxo of vtxos) {
|
|
10300
|
+
for (const vtxo of byValueDescending(vtxos)) {
|
|
10301
|
+
if (filteredVtxos.length >= MAX_VTXOS_PER_SETTLEMENT) {
|
|
10302
|
+
break;
|
|
10303
|
+
}
|
|
8945
10304
|
const inputFee = estimator.evalOffchainInput({
|
|
8946
10305
|
amount: BigInt(vtxo.value),
|
|
8947
10306
|
type: vtxo.virtualStatus.state === "swept" ? "recoverable" : "vtxo",
|
|
@@ -8952,20 +10311,31 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
8952
10311
|
if (inputFee.satoshis >= vtxo.value) {
|
|
8953
10312
|
continue;
|
|
8954
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
|
+
}
|
|
8955
10325
|
filteredVtxos.push(vtxo);
|
|
8956
|
-
amount +=
|
|
10326
|
+
amount += net;
|
|
8957
10327
|
}
|
|
8958
10328
|
const inputs = [...filteredBoardingUtxos, ...filteredVtxos];
|
|
8959
10329
|
if (inputs.length === 0) {
|
|
8960
10330
|
throw new Error("No inputs found");
|
|
8961
10331
|
}
|
|
8962
10332
|
const output = {
|
|
8963
|
-
address:
|
|
10333
|
+
address: offchainAddress,
|
|
8964
10334
|
amount: BigInt(amount)
|
|
8965
10335
|
};
|
|
8966
10336
|
const outputFee = estimator.evalOffchainOutput({
|
|
8967
10337
|
amount: output.amount,
|
|
8968
|
-
script:
|
|
10338
|
+
script: offchainOutputScript
|
|
8969
10339
|
});
|
|
8970
10340
|
output.amount -= BigInt(outputFee.satoshis);
|
|
8971
10341
|
if (output.amount <= this.dustAmount) {
|
|
@@ -9005,8 +10375,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9005
10375
|
}
|
|
9006
10376
|
}
|
|
9007
10377
|
let outputAssets;
|
|
9008
|
-
const
|
|
9009
|
-
const assetOutputIndex = findDestinationOutputIndex(outputs, destinationScript);
|
|
10378
|
+
const assetOutputIndex = findDestinationOutputIndex(outputs, offchainPkScript);
|
|
9010
10379
|
if (assetInputs.size > 0) {
|
|
9011
10380
|
if (assetOutputIndex === -1) {
|
|
9012
10381
|
throw new Error("Cannot assign assets: no output matches the destination address");
|
|
@@ -9074,6 +10443,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9074
10443
|
eventCallback: eventCallback ? (event) => Promise.resolve(eventCallback(event)) : void 0
|
|
9075
10444
|
});
|
|
9076
10445
|
await this.updateDbAfterSettle(params.inputs, commitmentTxid);
|
|
10446
|
+
await this.maybeRotateBoardingAfterBoard(params.inputs);
|
|
9077
10447
|
return commitmentTxid;
|
|
9078
10448
|
} catch (error) {
|
|
9079
10449
|
const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
|
|
@@ -9091,6 +10461,41 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9091
10461
|
});
|
|
9092
10462
|
}
|
|
9093
10463
|
}
|
|
10464
|
+
/**
|
|
10465
|
+
* Rotate the boarding address after a board (rotate-on-board trigger).
|
|
10466
|
+
*
|
|
10467
|
+
* Mirrors {@link WalletReceiveRotator}'s L2 rotation, but driven by a
|
|
10468
|
+
* board instead of a `vtxo_received` event: when a settle consumes at
|
|
10469
|
+
* least one boarding (on-chain) UTXO, the current boarding address has
|
|
10470
|
+
* served its purpose, so we allocate a fresh one via
|
|
10471
|
+
* {@link getNewBoardingAddress}. A settle that consumed only VTXOs (a
|
|
10472
|
+
* renewal / offboard) is not a board and leaves the boarding address
|
|
10473
|
+
* untouched.
|
|
10474
|
+
*
|
|
10475
|
+
* Boarding inputs are the non-VTXO coins (no `virtualStatus`), the same
|
|
10476
|
+
* discriminator {@link handleSettlementFinalizationEvent} uses; the
|
|
10477
|
+
* `typeof` guard skips arknote string inputs before the `in` test.
|
|
10478
|
+
*
|
|
10479
|
+
* No-ops for static / `auto` wallets (no descriptor provider — boarding
|
|
10480
|
+
* stays on its fixed index-0 address). Best-effort and non-fatal: the
|
|
10481
|
+
* settle has already committed and its txid must be returned, so a
|
|
10482
|
+
* rotation failure is logged and swallowed rather than thrown. Funds at
|
|
10483
|
+
* the retired boarding address remain discoverable — the old `boarding`
|
|
10484
|
+
* contract stays active and {@link getBoardingUtxos} fans out over the
|
|
10485
|
+
* full historical boarding set.
|
|
10486
|
+
*/
|
|
10487
|
+
async maybeRotateBoardingAfterBoard(inputs) {
|
|
10488
|
+
if (!this._descriptorProvider) return;
|
|
10489
|
+
const consumedBoarding = inputs.some(
|
|
10490
|
+
(input) => typeof input !== "string" && !("virtualStatus" in input)
|
|
10491
|
+
);
|
|
10492
|
+
if (!consumedBoarding) return;
|
|
10493
|
+
try {
|
|
10494
|
+
await this.getNewBoardingAddress();
|
|
10495
|
+
} catch (e) {
|
|
10496
|
+
console.warn("Failed to rotate boarding address after board", e);
|
|
10497
|
+
}
|
|
10498
|
+
}
|
|
9094
10499
|
async handleSettlementFinalizationEvent(event, inputs, forfeitOutputScript, connectorsGraph) {
|
|
9095
10500
|
const signedForfeits = [];
|
|
9096
10501
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
@@ -9301,6 +10706,19 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9301
10706
|
}
|
|
9302
10707
|
return jobs;
|
|
9303
10708
|
}
|
|
10709
|
+
/**
|
|
10710
|
+
* @internal Sign an on-chain boarding exit / sweep transaction, routing
|
|
10711
|
+
* each input to the correct key by its `witnessUtxo.script`: the identity
|
|
10712
|
+
* for index-0 / static boarding, the per-index descriptor for a rotated
|
|
10713
|
+
* boarding UTXO (plan §6-III.3). Used by
|
|
10714
|
+
* {@link VtxoManager.sweepExpiredBoardingUtxos}; without it, the
|
|
10715
|
+
* unilateral exit of a rotated boarding UTXO would be signed with the
|
|
10716
|
+
* wrong (index-0) key and rejected.
|
|
10717
|
+
*/
|
|
10718
|
+
async signOnchainBoardingTx(tx) {
|
|
10719
|
+
const signed = await this._signerRouter.sign(tx, this.inputSigningJobsFromWitnessUtxos(tx));
|
|
10720
|
+
return signed;
|
|
10721
|
+
}
|
|
9304
10722
|
async safeRegisterIntent(intent, inputs) {
|
|
9305
10723
|
try {
|
|
9306
10724
|
return await this.arkProvider.registerIntent(intent);
|
|
@@ -9496,12 +10914,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9496
10914
|
throw new Error("At least one receiver is required");
|
|
9497
10915
|
}
|
|
9498
10916
|
const offchainTapscript = this.offchainTapscript;
|
|
9499
|
-
const
|
|
10917
|
+
const serverPubKey = this.arkServerPublicKey;
|
|
10918
|
+
const serverUnrollScript = this.serverUnrollScript;
|
|
10919
|
+
const outputAddress = offchainTapscript.address(this.network.hrp, serverPubKey);
|
|
9500
10920
|
const address = outputAddress.encode();
|
|
9501
10921
|
const recipients = validateRecipients(args, Number(this.dustAmount));
|
|
9502
|
-
const
|
|
10922
|
+
const allVirtualCoins = await this.getVtxos({
|
|
9503
10923
|
withRecoverable: false
|
|
9504
10924
|
});
|
|
10925
|
+
const pendingRecovery = await this.pendingRecoveryOutpoints();
|
|
10926
|
+
const virtualCoins = pendingRecovery.size ? allVirtualCoins.filter((c) => !pendingRecovery.has(`${c.txid}:${c.vout}`)) : allVirtualCoins;
|
|
9505
10927
|
const assetChanges = /* @__PURE__ */ new Map();
|
|
9506
10928
|
let selectedCoins = [];
|
|
9507
10929
|
let btcAmountToSelect = 0;
|
|
@@ -9623,33 +11045,128 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9623
11045
|
outputs.push(Extension.create([assetPacket]).txOut());
|
|
9624
11046
|
}
|
|
9625
11047
|
const sentAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
|
|
9626
|
-
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);
|
|
9627
11068
|
try {
|
|
9628
11069
|
const { arkTxid, signedCheckpointTxs } = await this.buildAndSubmitOffchainTx(
|
|
9629
|
-
|
|
9630
|
-
outputs
|
|
11070
|
+
inputs,
|
|
11071
|
+
outputs,
|
|
11072
|
+
persist.serverUnrollScript
|
|
9631
11073
|
);
|
|
9632
11074
|
await this.updateDbAfterOffchainTx(
|
|
9633
|
-
|
|
11075
|
+
inputs,
|
|
9634
11076
|
arkTxid,
|
|
9635
11077
|
signedCheckpointTxs,
|
|
9636
|
-
sentAmount,
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
offchainTapscript,
|
|
9640
|
-
|
|
11078
|
+
persist.sentAmount,
|
|
11079
|
+
persist.changeAmount,
|
|
11080
|
+
persist.changeVout,
|
|
11081
|
+
persist.offchainTapscript,
|
|
11082
|
+
persist.serverPubKey,
|
|
11083
|
+
persist.changeAssets,
|
|
11084
|
+
persist.recordSentHistory ?? true
|
|
9641
11085
|
);
|
|
9642
11086
|
return arkTxid;
|
|
9643
11087
|
} finally {
|
|
9644
|
-
this._removePendingSpends(
|
|
9645
|
-
}
|
|
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
|
+
});
|
|
9646
11163
|
}
|
|
9647
11164
|
/**
|
|
9648
11165
|
* Build an offchain transaction from the given inputs and outputs,
|
|
9649
11166
|
* sign it, submit to the Arkade provider, and finalize.
|
|
9650
11167
|
* @returns The Arkade transaction id and server-signed checkpoint PSBTs (for bookkeeping)
|
|
9651
11168
|
*/
|
|
9652
|
-
async buildAndSubmitOffchainTx(inputs, outputs) {
|
|
11169
|
+
async buildAndSubmitOffchainTx(inputs, outputs, serverUnrollScript = this.serverUnrollScript) {
|
|
9653
11170
|
const offchainTx = buildOffchainTx(
|
|
9654
11171
|
inputs.map((input) => {
|
|
9655
11172
|
return {
|
|
@@ -9658,7 +11175,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9658
11175
|
};
|
|
9659
11176
|
}),
|
|
9660
11177
|
outputs,
|
|
9661
|
-
|
|
11178
|
+
serverUnrollScript
|
|
9662
11179
|
);
|
|
9663
11180
|
const arkTxJobs = inputs.map((input, index) => ({
|
|
9664
11181
|
index,
|
|
@@ -9732,14 +11249,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9732
11249
|
return { arkTxid, signedCheckpointTxs };
|
|
9733
11250
|
}
|
|
9734
11251
|
// mark virtual outputs as spent, save change outputs if any.
|
|
9735
|
-
// `offchainTapscript`
|
|
9736
|
-
// `_txLock` before any `await`; deriving both the
|
|
9737
|
-
// metadata and `primaryAddress` from
|
|
9738
|
-
// record matches the pkScript the server saw on the inbound
|
|
9739
|
-
// transaction, even if `
|
|
9740
|
-
// `this.
|
|
9741
|
-
async updateDbAfterOffchainTx(inputs, arkTxid, signedCheckpointTxs, sentAmount, changeAmount, changeVout, offchainTapscript, changeAssets) {
|
|
9742
|
-
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();
|
|
9743
11260
|
try {
|
|
9744
11261
|
const spentVtxos = [];
|
|
9745
11262
|
const commitmentTxIds = /* @__PURE__ */ new Set();
|
|
@@ -9846,19 +11363,21 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9846
11363
|
[changeVtxo]
|
|
9847
11364
|
);
|
|
9848
11365
|
}
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
|
|
9860
|
-
|
|
9861
|
-
|
|
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
|
+
}
|
|
9862
11381
|
} catch (e) {
|
|
9863
11382
|
console.warn("error saving offchain tx to repository", e);
|
|
9864
11383
|
throw e;
|
|
@@ -9867,10 +11386,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9867
11386
|
// mark virtual outputs as spent/settled, remove boarding inputs
|
|
9868
11387
|
async updateDbAfterSettle(inputs, commitmentTxid) {
|
|
9869
11388
|
try {
|
|
9870
|
-
const boardingAddress = await this.getBoardingAddress();
|
|
9871
11389
|
const spentVtxos = [];
|
|
9872
11390
|
const inputArkTxIds = /* @__PURE__ */ new Set();
|
|
9873
|
-
const
|
|
11391
|
+
const boardingRemovalsByAddress = /* @__PURE__ */ new Map();
|
|
9874
11392
|
const isVtxo = (input) => "virtualStatus" in input;
|
|
9875
11393
|
const vtxoInputs = inputs.filter(isVtxo);
|
|
9876
11394
|
const cm = await this.getContractManager();
|
|
@@ -9892,7 +11410,20 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9892
11410
|
isSpent: true
|
|
9893
11411
|
});
|
|
9894
11412
|
} else {
|
|
9895
|
-
|
|
11413
|
+
let sourceAddress;
|
|
11414
|
+
try {
|
|
11415
|
+
sourceAddress = VtxoScript.decode(input.tapTree).onchainAddress(
|
|
11416
|
+
this.network
|
|
11417
|
+
);
|
|
11418
|
+
} catch {
|
|
11419
|
+
sourceAddress = this.boardingTapscript.onchainAddress(this.network);
|
|
11420
|
+
}
|
|
11421
|
+
let set = boardingRemovalsByAddress.get(sourceAddress);
|
|
11422
|
+
if (!set) {
|
|
11423
|
+
set = /* @__PURE__ */ new Set();
|
|
11424
|
+
boardingRemovalsByAddress.set(sourceAddress, set);
|
|
11425
|
+
}
|
|
11426
|
+
set.add(`${input.txid}:${input.vout}`);
|
|
9896
11427
|
}
|
|
9897
11428
|
}
|
|
9898
11429
|
if (spentVtxos.length > 0) {
|
|
@@ -9924,14 +11455,12 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
9924
11455
|
);
|
|
9925
11456
|
}
|
|
9926
11457
|
}
|
|
9927
|
-
|
|
9928
|
-
const currentUtxos = await this.walletRepository.getUtxos(
|
|
9929
|
-
const filtered = currentUtxos.filter(
|
|
9930
|
-
|
|
9931
|
-
);
|
|
9932
|
-
await this.walletRepository.deleteUtxos(boardingAddress);
|
|
11458
|
+
for (const [address, toRemove] of boardingRemovalsByAddress) {
|
|
11459
|
+
const currentUtxos = await this.walletRepository.getUtxos(address);
|
|
11460
|
+
const filtered = currentUtxos.filter((u) => !toRemove.has(`${u.txid}:${u.vout}`));
|
|
11461
|
+
await this.walletRepository.deleteUtxos(address);
|
|
9933
11462
|
if (filtered.length > 0) {
|
|
9934
|
-
await this.walletRepository.saveUtxos(
|
|
11463
|
+
await this.walletRepository.saveUtxos(address, filtered);
|
|
9935
11464
|
}
|
|
9936
11465
|
}
|
|
9937
11466
|
} catch (e) {
|
|
@@ -10706,6 +12235,82 @@ var DelegateNotConfiguredError = class extends Error {
|
|
|
10706
12235
|
};
|
|
10707
12236
|
var DelegatorNotConfiguredError = DelegateNotConfiguredError;
|
|
10708
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
|
+
});
|
|
10709
12314
|
var WalletMessageHandler = class {
|
|
10710
12315
|
messageTag;
|
|
10711
12316
|
wallet;
|
|
@@ -10787,7 +12392,9 @@ var WalletMessageHandler = class {
|
|
|
10787
12392
|
// page-side PING / MESSAGE_BUS_NOT_INITIALIZED path triggered by concurrent
|
|
10788
12393
|
// short requests (GET_STATUS, GET_BALANCE, ...).
|
|
10789
12394
|
isLongRunning(message) {
|
|
10790
|
-
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
|
|
10791
12398
|
// step until it hits gapLimit consecutive unused indices. The bus
|
|
10792
12399
|
// deadline must not race the scan; liveness stays covered by PING.
|
|
10793
12400
|
message.type === "RESTORE_WALLET";
|
|
@@ -11153,6 +12760,36 @@ var WalletMessageHandler = class {
|
|
|
11153
12760
|
payload: { txid }
|
|
11154
12761
|
});
|
|
11155
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
|
+
}
|
|
11156
12793
|
case "RESTORE_WALLET": {
|
|
11157
12794
|
const wallet = this.requireWallet();
|
|
11158
12795
|
try {
|
|
@@ -11186,9 +12823,10 @@ var WalletMessageHandler = class {
|
|
|
11186
12823
|
await this.onWalletInitialized();
|
|
11187
12824
|
}
|
|
11188
12825
|
async handleGetBalance() {
|
|
11189
|
-
const [boardingUtxos, allVtxos] = await Promise.all([
|
|
12826
|
+
const [boardingUtxos, allVtxos, pendingOutpoints] = await Promise.all([
|
|
11190
12827
|
this.getAllBoardingUtxos(),
|
|
11191
|
-
this.getVtxosFromRepo()
|
|
12828
|
+
this.getVtxosFromRepo(),
|
|
12829
|
+
this.readonlyWallet ? this.readonlyWallet.pendingRecoveryOutpoints() : Promise.resolve(/* @__PURE__ */ new Set())
|
|
11192
12830
|
]);
|
|
11193
12831
|
let confirmed = 0;
|
|
11194
12832
|
let unconfirmed = 0;
|
|
@@ -11204,8 +12842,11 @@ var WalletMessageHandler = class {
|
|
|
11204
12842
|
let settled = 0;
|
|
11205
12843
|
let preconfirmed = 0;
|
|
11206
12844
|
let recoverable = 0;
|
|
12845
|
+
let pendingRecovery = 0;
|
|
11207
12846
|
for (const vtxo of spendableVtxos) {
|
|
11208
|
-
if (vtxo.
|
|
12847
|
+
if (pendingOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
|
|
12848
|
+
pendingRecovery += vtxo.value;
|
|
12849
|
+
} else if (vtxo.virtualStatus.state === "settled") {
|
|
11209
12850
|
settled += vtxo.value;
|
|
11210
12851
|
} else if (vtxo.virtualStatus.state === "preconfirmed") {
|
|
11211
12852
|
preconfirmed += vtxo.value;
|
|
@@ -11217,7 +12858,7 @@ var WalletMessageHandler = class {
|
|
|
11217
12858
|
}
|
|
11218
12859
|
}
|
|
11219
12860
|
const totalBoarding = confirmed + unconfirmed;
|
|
11220
|
-
const totalOffchain = settled + preconfirmed + recoverable;
|
|
12861
|
+
const totalOffchain = settled + preconfirmed + recoverable + pendingRecovery;
|
|
11221
12862
|
const assetBalances = /* @__PURE__ */ new Map();
|
|
11222
12863
|
for (const vtxo of spendableVtxos) {
|
|
11223
12864
|
if (vtxo.assets) {
|
|
@@ -11241,6 +12882,7 @@ var WalletMessageHandler = class {
|
|
|
11241
12882
|
preconfirmed,
|
|
11242
12883
|
available: settled + preconfirmed,
|
|
11243
12884
|
recoverable,
|
|
12885
|
+
pendingRecovery,
|
|
11244
12886
|
total: totalBoarding + totalOffchain,
|
|
11245
12887
|
assets
|
|
11246
12888
|
};
|
|
@@ -11331,9 +12973,7 @@ var WalletMessageHandler = class {
|
|
|
11331
12973
|
);
|
|
11332
12974
|
}
|
|
11333
12975
|
if (funds.type === "utxo") {
|
|
11334
|
-
const utxos =
|
|
11335
|
-
const boardingAddress = await this.readonlyWallet.getBoardingAddress();
|
|
11336
|
-
await this.walletRepository?.saveUtxos(boardingAddress, utxos);
|
|
12976
|
+
const utxos = await this.readonlyWallet.getBoardingUtxos();
|
|
11337
12977
|
this.scheduleForNextTick(
|
|
11338
12978
|
() => this.tagged({
|
|
11339
12979
|
type: "UTXO_UPDATE",
|
|
@@ -11362,13 +13002,16 @@ var WalletMessageHandler = class {
|
|
|
11362
13002
|
return;
|
|
11363
13003
|
}
|
|
11364
13004
|
const vtxos = await this.getVtxosFromRepo();
|
|
11365
|
-
const
|
|
11366
|
-
const
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
13005
|
+
const boardingAddresses = await this.readonlyWallet.getBoardingAddresses();
|
|
13006
|
+
const fresh = await this.readonlyWallet.getBoardingUtxos();
|
|
13007
|
+
const freshKeys = new Set(fresh.map((u) => `${u.txid}:${u.vout}`));
|
|
13008
|
+
for (const addr of boardingAddresses) {
|
|
13009
|
+
const cached = await this.walletRepository.getUtxos(addr);
|
|
13010
|
+
const kept = cached.filter((u) => freshKeys.has(`${u.txid}:${u.vout}`));
|
|
13011
|
+
if (kept.length === cached.length) continue;
|
|
13012
|
+
await this.walletRepository.deleteUtxos(addr);
|
|
13013
|
+
if (kept.length > 0) await this.walletRepository.saveUtxos(addr, kept);
|
|
13014
|
+
}
|
|
11372
13015
|
const address = await this.readonlyWallet.getAddress();
|
|
11373
13016
|
const txs = await this.buildTransactionHistoryFromCache(vtxos);
|
|
11374
13017
|
if (txs) await this.walletRepository.saveTransactions(address, txs);
|
|
@@ -11625,6 +13268,7 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
11625
13268
|
GET_EXPIRING_VTXOS: 2e4,
|
|
11626
13269
|
GET_EXPIRED_BOARDING_UTXOS: 2e4,
|
|
11627
13270
|
GET_RECOVERABLE_BALANCE: 2e4,
|
|
13271
|
+
GET_DEPRECATED_SIGNER_STATUS: 2e4,
|
|
11628
13272
|
RELOAD_WALLET: 2e4,
|
|
11629
13273
|
// Transactions — need more headroom.
|
|
11630
13274
|
// SETTLE / RECOVER_VTXOS / RENEW_VTXOS go through the streaming path and
|
|
@@ -11640,6 +13284,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
11640
13284
|
RECOVER_VTXOS: 5e4,
|
|
11641
13285
|
RENEW_VTXOS: 5e4,
|
|
11642
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,
|
|
11643
13290
|
// RESTORE_WALLET is a streaming/long-running path (sendMessageWithEvents)
|
|
11644
13291
|
// like SETTLE; the value here is kept for type completeness and is never
|
|
11645
13292
|
// enforced as an inactivity deadline.
|
|
@@ -11665,6 +13312,7 @@ var DEDUPABLE_REQUEST_TYPES = /* @__PURE__ */ new Set([
|
|
|
11665
13312
|
"GET_DELEGATE_INFO",
|
|
11666
13313
|
"GET_RECOVERABLE_BALANCE",
|
|
11667
13314
|
"GET_EXPIRED_BOARDING_UTXOS",
|
|
13315
|
+
"GET_DEPRECATED_SIGNER_STATUS",
|
|
11668
13316
|
"GET_VTXOS",
|
|
11669
13317
|
"GET_CONTRACTS",
|
|
11670
13318
|
"GET_CONTRACTS_WITH_VTXOS",
|
|
@@ -12787,6 +14435,42 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
|
|
|
12787
14435
|
throw new Error(`Failed to sweep expired boarding utxos: ${e}`);
|
|
12788
14436
|
}
|
|
12789
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
|
+
},
|
|
12790
14474
|
async dispose() {
|
|
12791
14475
|
return;
|
|
12792
14476
|
}
|
|
@@ -13692,13 +15376,19 @@ function isHeaderSubscribeResult(v) {
|
|
|
13692
15376
|
const obj = v;
|
|
13693
15377
|
return typeof obj.height === "number" && typeof obj.hex === "string";
|
|
13694
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
|
+
}
|
|
13695
15387
|
function isMissingHeightError(err) {
|
|
13696
|
-
|
|
13697
|
-
return msg.toLowerCase().includes("missingheight");
|
|
15388
|
+
return errorText(err).toLowerCase().includes("missingheight");
|
|
13698
15389
|
}
|
|
13699
15390
|
function isTxNotInBlockError(err) {
|
|
13700
|
-
const
|
|
13701
|
-
const normalized = msg.toLowerCase();
|
|
15391
|
+
const normalized = errorText(err).toLowerCase();
|
|
13702
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");
|
|
13703
15393
|
}
|
|
13704
15394
|
function childTxidFromHex(txHex) {
|
|
@@ -14158,6 +15848,6 @@ function isArkContract(str) {
|
|
|
14158
15848
|
return str.startsWith(ARKCONTRACT_PREFIX + "=");
|
|
14159
15849
|
}
|
|
14160
15850
|
|
|
14161
|
-
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 };
|
|
14162
|
-
//# sourceMappingURL=chunk-
|
|
14163
|
-
//# 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
|