@arkade-os/sdk 0.4.43 → 0.4.45

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.
Files changed (68) hide show
  1. package/README.md +100 -0
  2. package/dist/adapters/expo.cjs +4 -4
  3. package/dist/adapters/expo.d.cts +2 -2
  4. package/dist/adapters/expo.d.ts +2 -2
  5. package/dist/adapters/expo.js +2 -2
  6. package/dist/adapters/indexedDB.cjs +3 -3
  7. package/dist/adapters/indexedDB.js +2 -2
  8. package/dist/{ark-I8jfe5VE.d.cts → ark-C-PjOngm.d.cts} +89 -1
  9. package/dist/{ark-I8jfe5VE.d.ts → ark-C-PjOngm.d.ts} +89 -1
  10. package/dist/{asyncStorageTaskQueue-D1FfHfgK.d.cts → asyncStorageTaskQueue-BmBPvTyh.d.cts} +1 -1
  11. package/dist/{asyncStorageTaskQueue-CjoL-35w.d.ts → asyncStorageTaskQueue-CHpI3moJ.d.ts} +1 -1
  12. package/dist/{chunk-VKKLOT2E.js → chunk-BOA4SHCD.js} +3 -3
  13. package/dist/{chunk-VKKLOT2E.js.map → chunk-BOA4SHCD.js.map} +1 -1
  14. package/dist/{chunk-PLJZRIFZ.cjs → chunk-HZ7VIN7P.cjs} +11 -11
  15. package/dist/{chunk-PLJZRIFZ.cjs.map → chunk-HZ7VIN7P.cjs.map} +1 -1
  16. package/dist/{chunk-JIWCE2IK.cjs → chunk-OFQGANH2.cjs} +7 -7
  17. package/dist/{chunk-JIWCE2IK.cjs.map → chunk-OFQGANH2.cjs.map} +1 -1
  18. package/dist/{chunk-US274KZO.js → chunk-QZ6HZZZK.js} +3 -3
  19. package/dist/{chunk-US274KZO.js.map → chunk-QZ6HZZZK.js.map} +1 -1
  20. package/dist/{chunk-QGNTDGMN.cjs → chunk-SRCWBFSZ.cjs} +1945 -357
  21. package/dist/chunk-SRCWBFSZ.cjs.map +1 -0
  22. package/dist/{chunk-B4R36PRN.js → chunk-TGRJ5WAX.js} +1868 -285
  23. package/dist/chunk-TGRJ5WAX.js.map +1 -0
  24. package/dist/{chunk-3AQNLQPM.cjs → chunk-UHOHLN3C.cjs} +119 -69
  25. package/dist/chunk-UHOHLN3C.cjs.map +1 -0
  26. package/dist/{chunk-763XHDAJ.js → chunk-YBQV7XBC.js} +119 -70
  27. package/dist/chunk-YBQV7XBC.js.map +1 -0
  28. package/dist/contracts/handlers/index.d.cts +3 -3
  29. package/dist/contracts/handlers/index.d.ts +3 -3
  30. package/dist/{delegate-CgjVjYOP.d.ts → delegate-BKNqva1j.d.ts} +1 -1
  31. package/dist/{delegate-BRqbh2fW.d.cts → delegate-DOEobYwH.d.cts} +1 -1
  32. package/dist/{index-DHuXlDEg.d.cts → index-BlW4I30l.d.cts} +2 -2
  33. package/dist/{index-DvWNB_rl.d.ts → index-Cm2fGeox.d.ts} +2 -2
  34. package/dist/index.cjs +150 -126
  35. package/dist/index.d.cts +275 -19
  36. package/dist/index.d.ts +275 -19
  37. package/dist/index.js +2 -2
  38. package/dist/repositories/realm/index.cjs +17 -17
  39. package/dist/repositories/realm/index.d.cts +2 -2
  40. package/dist/repositories/realm/index.d.ts +2 -2
  41. package/dist/repositories/realm/index.js +3 -3
  42. package/dist/repositories/sqlite/index.cjs +16 -16
  43. package/dist/repositories/sqlite/index.d.cts +2 -2
  44. package/dist/repositories/sqlite/index.d.ts +2 -2
  45. package/dist/repositories/sqlite/index.js +3 -3
  46. package/dist/{taskRunner-Djlhfgi1.d.ts → taskRunner-BngWSFMi.d.ts} +1 -1
  47. package/dist/{taskRunner-BSnslNmq.d.cts → taskRunner-DOiyRuJa.d.cts} +1 -1
  48. package/dist/wallet/expo/background.cjs +12 -12
  49. package/dist/wallet/expo/background.d.cts +3 -3
  50. package/dist/wallet/expo/background.d.ts +3 -3
  51. package/dist/wallet/expo/background.js +4 -4
  52. package/dist/wallet/expo/index.cjs +21 -12
  53. package/dist/wallet/expo/index.cjs.map +1 -1
  54. package/dist/wallet/expo/index.d.cts +11 -4
  55. package/dist/wallet/expo/index.d.ts +11 -4
  56. package/dist/wallet/expo/index.js +12 -3
  57. package/dist/wallet/expo/index.js.map +1 -1
  58. package/dist/{wallet-BsqRU8KM.d.cts → wallet-B3awP1Nj.d.ts} +105 -3
  59. package/dist/{wallet-D9z7wssl.d.ts → wallet-DBjumS42.d.cts} +105 -3
  60. package/dist/worker/expo/index.cjs +7 -7
  61. package/dist/worker/expo/index.d.cts +4 -4
  62. package/dist/worker/expo/index.d.ts +4 -4
  63. package/dist/worker/expo/index.js +3 -3
  64. package/package.json +1 -1
  65. package/dist/chunk-3AQNLQPM.cjs.map +0 -1
  66. package/dist/chunk-763XHDAJ.js.map +0 -1
  67. package/dist/chunk-B4R36PRN.js.map +0 -1
  68. package/dist/chunk-QGNTDGMN.cjs.map +0 -1
@@ -1,6 +1,6 @@
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-763XHDAJ.js';
2
- import { isMainnetDescriptor, descriptorIsOurs, contractHandlers, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-YNEDJTX2.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-BE5MUB7T.js';
1
+ import { craftToSpendTx, Transaction, OP_RETURN_EMPTY_PKSCRIPT, baseFetch, Intent, getArkPsbtFields, CosignerPublicKey, setArkPsbtField, VtxoTaprootTree, maybeArkError, ProviderUnavailableError, FetchError, isFetchTimeoutError, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-YBQV7XBC.js';
2
+ import { contractHandlers, isMainnetDescriptor, descriptorIsOurs, DEFAULT_PAGE_SIZE, DelegateVtxo, WALLET_RECEIVE_SOURCE, deriveDescriptorLeafPubKey, DefaultVtxo, BoardingContractHandler } from './chunk-YNEDJTX2.js';
3
+ import { VtxoScript, sequenceToTimelock, timelockToSequence, DEFAULT_NETWORK, DEFAULT_NETWORK_NAME, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, ArkAddress, CSVMultisigTapscript, getSequence, getNetwork, MultisigTapscript, networks as networks$1, DEFAULT_ARKADE_SERVER_URL } from './chunk-BE5MUB7T.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';
@@ -1215,6 +1215,114 @@ var isExplorerTransaction = (tx) => {
1215
1215
  (vout) => typeof vout.scriptpubkey_address === "string" && typeof vout.value === "number"
1216
1216
  ) && typeof tx.status === "object" && typeof tx.status.confirmed === "boolean";
1217
1217
  };
1218
+ var getVarIntSize = (n) => {
1219
+ if (n < 253) return 1;
1220
+ if (n <= 65535) return 3;
1221
+ if (n <= 4294967295) return 5;
1222
+ return 9;
1223
+ };
1224
+ var TxWeightEstimator = class _TxWeightEstimator {
1225
+ static P2PKH_SCRIPT_SIG_SIZE = 1 + 73 + 1 + 33;
1226
+ static INPUT_SIZE = 32 + 4 + 1 + 4;
1227
+ static BASE_CONTROL_BLOCK_SIZE = 1 + 32;
1228
+ static OUTPUT_SIZE = 8 + 1;
1229
+ static P2WPKH_OUTPUT_SIZE = 1 + 1 + 20;
1230
+ static BASE_TX_SIZE = 8 + 2;
1231
+ // Version + LockTime
1232
+ static WITNESS_HEADER_SIZE = 2;
1233
+ // Flag + Marker
1234
+ static WITNESS_SCALE_FACTOR = 4;
1235
+ static P2TR_OUTPUT_SIZE = 1 + 1 + 32;
1236
+ hasWitness;
1237
+ inputCount;
1238
+ outputCount;
1239
+ inputSize;
1240
+ inputWitnessSize;
1241
+ outputSize;
1242
+ constructor(hasWitness, inputCount, outputCount, inputSize, inputWitnessSize, outputSize) {
1243
+ this.hasWitness = hasWitness;
1244
+ this.inputCount = inputCount;
1245
+ this.outputCount = outputCount;
1246
+ this.inputSize = inputSize;
1247
+ this.inputWitnessSize = inputWitnessSize;
1248
+ this.outputSize = outputSize;
1249
+ }
1250
+ static create() {
1251
+ return new _TxWeightEstimator(false, 0, 0, 0, 0, 0);
1252
+ }
1253
+ addP2AInput() {
1254
+ this.inputCount++;
1255
+ this.inputSize += _TxWeightEstimator.INPUT_SIZE;
1256
+ return this;
1257
+ }
1258
+ addKeySpendInput(isDefault = true) {
1259
+ this.inputCount++;
1260
+ this.inputWitnessSize += 64 + 1 + (isDefault ? 0 : 1);
1261
+ this.inputSize += _TxWeightEstimator.INPUT_SIZE;
1262
+ this.hasWitness = true;
1263
+ return this;
1264
+ }
1265
+ addP2PKHInput() {
1266
+ this.inputCount++;
1267
+ this.inputWitnessSize++;
1268
+ this.inputSize += _TxWeightEstimator.INPUT_SIZE + _TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE;
1269
+ return this;
1270
+ }
1271
+ addTapscriptInput(leafWitnessSize, leafScriptSize, leafControlBlockSize) {
1272
+ const controlBlockWitnessSize = 1 + _TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE + 1 + leafScriptSize + 1 + leafControlBlockSize;
1273
+ this.inputCount++;
1274
+ this.inputWitnessSize += leafWitnessSize + 1 + controlBlockWitnessSize;
1275
+ this.inputSize += _TxWeightEstimator.INPUT_SIZE;
1276
+ this.hasWitness = true;
1277
+ return this;
1278
+ }
1279
+ addP2WPKHOutput() {
1280
+ this.outputCount++;
1281
+ this.outputSize += _TxWeightEstimator.OUTPUT_SIZE + _TxWeightEstimator.P2WPKH_OUTPUT_SIZE;
1282
+ return this;
1283
+ }
1284
+ addP2TROutput() {
1285
+ this.outputCount++;
1286
+ this.outputSize += _TxWeightEstimator.OUTPUT_SIZE + _TxWeightEstimator.P2TR_OUTPUT_SIZE;
1287
+ return this;
1288
+ }
1289
+ /**
1290
+ * Adds an output given a raw script.
1291
+ * Cost = 8 bytes (amount) + varint(scriptLen) + scriptLen
1292
+ */
1293
+ addOutputScript(script) {
1294
+ this.outputCount++;
1295
+ this.outputSize += 8 + getVarIntSize(script.length) + script.length;
1296
+ return this;
1297
+ }
1298
+ /**
1299
+ * Adds an output by decoding the address to get the exact script size.
1300
+ */
1301
+ addOutputAddress(address, network) {
1302
+ const payment = Address(network).decode(address);
1303
+ const script = OutScript.encode(payment);
1304
+ return this.addOutputScript(script);
1305
+ }
1306
+ vsize() {
1307
+ const inputCount = getVarIntSize(this.inputCount);
1308
+ const outputCount = getVarIntSize(this.outputCount);
1309
+ const txSizeStripped = _TxWeightEstimator.BASE_TX_SIZE + inputCount + this.inputSize + outputCount + this.outputSize;
1310
+ let weight = txSizeStripped * _TxWeightEstimator.WITNESS_SCALE_FACTOR;
1311
+ if (this.hasWitness) {
1312
+ weight += _TxWeightEstimator.WITNESS_HEADER_SIZE + this.inputWitnessSize;
1313
+ }
1314
+ return vsize(weight);
1315
+ }
1316
+ };
1317
+ var vsize = (weight) => {
1318
+ const value = BigInt(Math.ceil(weight / TxWeightEstimator.WITNESS_SCALE_FACTOR));
1319
+ return {
1320
+ value,
1321
+ fee: (feeRate) => feeRate * value
1322
+ };
1323
+ };
1324
+
1325
+ // src/utils/anchor.ts
1218
1326
  var ANCHOR_VALUE = 0n;
1219
1327
  var ANCHOR_PKSCRIPT = new Uint8Array([81, 2, 78, 115]);
1220
1328
  var P2A = {
@@ -1240,6 +1348,37 @@ function findP2AOutput(tx) {
1240
1348
  }
1241
1349
  throw new Error("P2A output not found");
1242
1350
  }
1351
+ var CHILD_DUST_AMOUNT = 546;
1352
+ function buildAnchorChild(params) {
1353
+ const { parent, feeRate, fundingCoins, changeAddress, tapInternalKey, network } = params;
1354
+ const child = new Transaction({ version: 3, allowLegacyWitnessUtxo: true });
1355
+ child.addInput(findP2AOutput(parent));
1356
+ const estimator = TxWeightEstimator.create().addP2AInput();
1357
+ for (const _ of fundingCoins) {
1358
+ estimator.addKeySpendInput(true);
1359
+ }
1360
+ estimator.addOutputAddress(changeAddress, network);
1361
+ const childVsize = Number(estimator.vsize().value);
1362
+ const fee = Math.ceil(feeRate * (parent.vsize + childVsize));
1363
+ let total = 0n;
1364
+ for (const coin of fundingCoins) {
1365
+ total += BigInt(coin.value);
1366
+ child.addInput({
1367
+ txid: coin.txid,
1368
+ index: coin.vout,
1369
+ witnessUtxo: { script: params.changeScript, amount: BigInt(coin.value) },
1370
+ tapInternalKey
1371
+ });
1372
+ }
1373
+ const change = total + P2A.amount - BigInt(fee);
1374
+ if (change < BigInt(CHILD_DUST_AMOUNT)) {
1375
+ throw new Error(
1376
+ `insufficient funding for anchor child: need change >= ${CHILD_DUST_AMOUNT}, got ${change}`
1377
+ );
1378
+ }
1379
+ child.addOutputAddress(changeAddress, change, network);
1380
+ return { child, fee };
1381
+ }
1243
1382
 
1244
1383
  // src/forfeit.ts
1245
1384
  function buildForfeitTx(inputs, forfeitPkScript, txLocktime) {
@@ -2154,112 +2293,6 @@ function isValidArkAddress(address) {
2154
2293
  return false;
2155
2294
  }
2156
2295
  }
2157
- var getVarIntSize = (n) => {
2158
- if (n < 253) return 1;
2159
- if (n <= 65535) return 3;
2160
- if (n <= 4294967295) return 5;
2161
- return 9;
2162
- };
2163
- var TxWeightEstimator = class _TxWeightEstimator {
2164
- static P2PKH_SCRIPT_SIG_SIZE = 1 + 73 + 1 + 33;
2165
- static INPUT_SIZE = 32 + 4 + 1 + 4;
2166
- static BASE_CONTROL_BLOCK_SIZE = 1 + 32;
2167
- static OUTPUT_SIZE = 8 + 1;
2168
- static P2WPKH_OUTPUT_SIZE = 1 + 1 + 20;
2169
- static BASE_TX_SIZE = 8 + 2;
2170
- // Version + LockTime
2171
- static WITNESS_HEADER_SIZE = 2;
2172
- // Flag + Marker
2173
- static WITNESS_SCALE_FACTOR = 4;
2174
- static P2TR_OUTPUT_SIZE = 1 + 1 + 32;
2175
- hasWitness;
2176
- inputCount;
2177
- outputCount;
2178
- inputSize;
2179
- inputWitnessSize;
2180
- outputSize;
2181
- constructor(hasWitness, inputCount, outputCount, inputSize, inputWitnessSize, outputSize) {
2182
- this.hasWitness = hasWitness;
2183
- this.inputCount = inputCount;
2184
- this.outputCount = outputCount;
2185
- this.inputSize = inputSize;
2186
- this.inputWitnessSize = inputWitnessSize;
2187
- this.outputSize = outputSize;
2188
- }
2189
- static create() {
2190
- return new _TxWeightEstimator(false, 0, 0, 0, 0, 0);
2191
- }
2192
- addP2AInput() {
2193
- this.inputCount++;
2194
- this.inputSize += _TxWeightEstimator.INPUT_SIZE;
2195
- return this;
2196
- }
2197
- addKeySpendInput(isDefault = true) {
2198
- this.inputCount++;
2199
- this.inputWitnessSize += 64 + 1 + (isDefault ? 0 : 1);
2200
- this.inputSize += _TxWeightEstimator.INPUT_SIZE;
2201
- this.hasWitness = true;
2202
- return this;
2203
- }
2204
- addP2PKHInput() {
2205
- this.inputCount++;
2206
- this.inputWitnessSize++;
2207
- this.inputSize += _TxWeightEstimator.INPUT_SIZE + _TxWeightEstimator.P2PKH_SCRIPT_SIG_SIZE;
2208
- return this;
2209
- }
2210
- addTapscriptInput(leafWitnessSize, leafScriptSize, leafControlBlockSize) {
2211
- const controlBlockWitnessSize = 1 + _TxWeightEstimator.BASE_CONTROL_BLOCK_SIZE + 1 + leafScriptSize + 1 + leafControlBlockSize;
2212
- this.inputCount++;
2213
- this.inputWitnessSize += leafWitnessSize + 1 + controlBlockWitnessSize;
2214
- this.inputSize += _TxWeightEstimator.INPUT_SIZE;
2215
- this.hasWitness = true;
2216
- return this;
2217
- }
2218
- addP2WPKHOutput() {
2219
- this.outputCount++;
2220
- this.outputSize += _TxWeightEstimator.OUTPUT_SIZE + _TxWeightEstimator.P2WPKH_OUTPUT_SIZE;
2221
- return this;
2222
- }
2223
- addP2TROutput() {
2224
- this.outputCount++;
2225
- this.outputSize += _TxWeightEstimator.OUTPUT_SIZE + _TxWeightEstimator.P2TR_OUTPUT_SIZE;
2226
- return this;
2227
- }
2228
- /**
2229
- * Adds an output given a raw script.
2230
- * Cost = 8 bytes (amount) + varint(scriptLen) + scriptLen
2231
- */
2232
- addOutputScript(script) {
2233
- this.outputCount++;
2234
- this.outputSize += 8 + getVarIntSize(script.length) + script.length;
2235
- return this;
2236
- }
2237
- /**
2238
- * Adds an output by decoding the address to get the exact script size.
2239
- */
2240
- addOutputAddress(address, network) {
2241
- const payment = Address(network).decode(address);
2242
- const script = OutScript.encode(payment);
2243
- return this.addOutputScript(script);
2244
- }
2245
- vsize() {
2246
- const inputCount = getVarIntSize(this.inputCount);
2247
- const outputCount = getVarIntSize(this.outputCount);
2248
- const txSizeStripped = _TxWeightEstimator.BASE_TX_SIZE + inputCount + this.inputSize + outputCount + this.outputSize;
2249
- let weight = txSizeStripped * _TxWeightEstimator.WITNESS_SCALE_FACTOR;
2250
- if (this.hasWitness) {
2251
- weight += _TxWeightEstimator.WITNESS_HEADER_SIZE + this.inputWitnessSize;
2252
- }
2253
- return vsize(weight);
2254
- }
2255
- };
2256
- var vsize = (weight) => {
2257
- const value = BigInt(Math.ceil(weight / TxWeightEstimator.WITNESS_SCALE_FACTOR));
2258
- return {
2259
- value,
2260
- fee: (feeRate) => feeRate * value
2261
- };
2262
- };
2263
2296
  var AmountVariableName = "amount";
2264
2297
  var ExpiryVariableName = "expiry";
2265
2298
  var BirthVariableName = "birth";
@@ -4254,17 +4287,603 @@ function wrapHandlerWithIntentPersistence(base, deps) {
4254
4287
  }
4255
4288
  };
4256
4289
  }
4257
- var TxTree = class {
4258
- constructor(root, children = /* @__PURE__ */ new Map()) {
4259
- this.root = root;
4260
- this.children = children;
4290
+
4291
+ // src/repositories/virtualTxRepository.ts
4292
+ var ChainedTxType = /* @__PURE__ */ ((ChainedTxType2) => {
4293
+ ChainedTxType2[ChainedTxType2["Unspecified"] = 0] = "Unspecified";
4294
+ ChainedTxType2[ChainedTxType2["Commitment"] = 1] = "Commitment";
4295
+ ChainedTxType2[ChainedTxType2["Ark"] = 2] = "Ark";
4296
+ ChainedTxType2[ChainedTxType2["Tree"] = 3] = "Tree";
4297
+ ChainedTxType2[ChainedTxType2["Checkpoint"] = 4] = "Checkpoint";
4298
+ return ChainedTxType2;
4299
+ })(ChainedTxType || {});
4300
+ function mergeChainedTxType(incoming, prev) {
4301
+ if (incoming !== 0 /* Unspecified */) return incoming;
4302
+ return prev ?? 0 /* Unspecified */;
4303
+ }
4304
+
4305
+ // src/wallet/exit/chain.ts
4306
+ var isCommitment = (t) => t === "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */ || t === "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED" /* UNSPECIFIED */;
4307
+ async function buildExitDag(params) {
4308
+ const byTxid = /* @__PURE__ */ new Map();
4309
+ for (const vtxo of params.vtxos) {
4310
+ const outpoint = `${vtxo.txid}:${vtxo.vout}`;
4311
+ const chain = await params.chain.getVtxoChain({ txid: vtxo.txid, vout: vtxo.vout });
4312
+ for (const chainTx of chain) {
4313
+ const existing = byTxid.get(chainTx.txid);
4314
+ if (existing) {
4315
+ existing.forVtxos.add(outpoint);
4316
+ } else {
4317
+ byTxid.set(chainTx.txid, { ...chainTx, forVtxos: /* @__PURE__ */ new Set([outpoint]) });
4318
+ }
4319
+ }
4261
4320
  }
4262
- static create(chunks) {
4263
- if (chunks.length === 0) {
4264
- throw new Error("empty chunks");
4321
+ const pending = new Map(
4322
+ [...byTxid.values()].filter((tx) => !isCommitment(tx.type)).map((tx) => [tx.txid, tx])
4323
+ );
4324
+ const inDegree = /* @__PURE__ */ new Map();
4325
+ for (const tx of pending.values()) {
4326
+ const parents = tx.spends.filter((p) => pending.has(p));
4327
+ inDegree.set(tx.txid, parents.length);
4328
+ }
4329
+ const order = [];
4330
+ const queue = [...pending.values()].filter((tx) => inDegree.get(tx.txid) === 0).sort((a, b) => a.txid.localeCompare(b.txid));
4331
+ while (queue.length > 0) {
4332
+ const tx = queue.shift();
4333
+ order.push(tx);
4334
+ const children = [...pending.values()].filter((c) => c.spends.includes(tx.txid)).sort((a, b) => a.txid.localeCompare(b.txid));
4335
+ for (const child of children) {
4336
+ const deg = inDegree.get(child.txid) - 1;
4337
+ inDegree.set(child.txid, deg);
4338
+ if (deg === 0) queue.push(child);
4339
+ }
4340
+ }
4341
+ if (order.length !== pending.size) {
4342
+ throw new Error("inconsistent vtxo chain from indexer (cycle detected)");
4343
+ }
4344
+ const nodes = [];
4345
+ for (const tx of order) {
4346
+ let confirmed = false;
4347
+ try {
4348
+ const status = await params.onchain.getTxStatus(tx.txid);
4349
+ confirmed = status.confirmed;
4350
+ } catch {
4351
+ confirmed = false;
4352
+ }
4353
+ const expires = Number(tx.expiresAt);
4354
+ nodes.push({
4355
+ txid: tx.txid,
4356
+ type: tx.type,
4357
+ expiresAt: Number.isFinite(expires) && expires > 0 ? expires : void 0,
4358
+ forVtxos: [...tx.forVtxos].sort(),
4359
+ confirmed
4360
+ });
4361
+ }
4362
+ return nodes;
4363
+ }
4364
+ function topoSortByDeps(items, idOf, depsOf) {
4365
+ const produced = new Set(items.map(idOf));
4366
+ const emitted = /* @__PURE__ */ new Set();
4367
+ const ordered = [];
4368
+ let remaining = items;
4369
+ while (remaining.length > 0) {
4370
+ const next = [];
4371
+ let progress = false;
4372
+ for (const item of remaining) {
4373
+ if (depsOf(item).every((d) => !produced.has(d) || emitted.has(d))) {
4374
+ ordered.push(item);
4375
+ emitted.add(idOf(item));
4376
+ progress = true;
4377
+ } else {
4378
+ next.push(item);
4379
+ }
4265
4380
  }
4266
- const chunksByTxid = /* @__PURE__ */ new Map();
4267
- for (const chunk of chunks) {
4381
+ if (!progress) {
4382
+ throw new Error("topological sort: cycle or unsatisfiable dependency");
4383
+ }
4384
+ remaining = next;
4385
+ }
4386
+ return ordered;
4387
+ }
4388
+
4389
+ // src/wallet/exit/capture.ts
4390
+ var DEFAULT_MIN_EXIT_WORTH_SATS = 1e3;
4391
+ var DEFAULT_EXIT_CAPTURE_MODE = "lite";
4392
+ function chainTxTypeToChainedExit(t) {
4393
+ switch (t) {
4394
+ case "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */:
4395
+ return 1 /* Commitment */;
4396
+ case "INDEXER_CHAINED_TX_TYPE_ARK" /* ARK */:
4397
+ return 2 /* Ark */;
4398
+ case "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */:
4399
+ return 3 /* Tree */;
4400
+ case "INDEXER_CHAINED_TX_TYPE_CHECKPOINT" /* CHECKPOINT */:
4401
+ return 4 /* Checkpoint */;
4402
+ default:
4403
+ return 0 /* Unspecified */;
4404
+ }
4405
+ }
4406
+ function orderAncestryFirst(chain) {
4407
+ const ids = new Set(chain.map((c) => c.txid));
4408
+ return topoSortByDeps(
4409
+ chain,
4410
+ (c) => c.txid,
4411
+ (c) => c.spends.filter((s) => ids.has(s))
4412
+ );
4413
+ }
4414
+ async function captureExitBranch(params) {
4415
+ const { resolver, repository, vtxo, value, mode, minExitWorthSats } = params;
4416
+ if (value < minExitWorthSats) return;
4417
+ if (await repository.hasBranch(vtxo)) return;
4418
+ const ordered = orderAncestryFirst(await resolver.getVtxoChain(vtxo));
4419
+ const psbtByTxid = /* @__PURE__ */ new Map();
4420
+ if (mode === "full") {
4421
+ const nonCommitment = ordered.filter((c) => chainTxTypeToChainedExit(c.type) !== 1 /* Commitment */).map((c) => c.txid);
4422
+ if (nonCommitment.length > 0) {
4423
+ for (const psbt of await resolver.getVirtualTxs(nonCommitment)) {
4424
+ psbtByTxid.set(Transaction.fromPSBT(base64.decode(psbt)).id, psbt);
4425
+ }
4426
+ }
4427
+ }
4428
+ const virtualTxs = ordered.map((c) => {
4429
+ const expires = Number(c.expiresAt);
4430
+ return {
4431
+ txid: c.txid,
4432
+ psbt: psbtByTxid.get(c.txid) ?? null,
4433
+ expiresAt: Number.isFinite(expires) && expires > 0 ? expires : null,
4434
+ type: chainTxTypeToChainedExit(c.type)
4435
+ };
4436
+ });
4437
+ const branch = ordered.map((c, i) => ({
4438
+ vtxoTxid: vtxo.txid,
4439
+ vtxoVout: vtxo.vout,
4440
+ virtualTxid: c.txid,
4441
+ position: i
4442
+ }));
4443
+ await repository.upsertVirtualTxs(virtualTxs);
4444
+ await repository.setBranch(vtxo, branch);
4445
+ }
4446
+ async function pruneExitBranches(repository, outpoints) {
4447
+ for (const outpoint of outpoints) {
4448
+ try {
4449
+ await repository.pruneForSpentVtxo(outpoint);
4450
+ } catch {
4451
+ }
4452
+ }
4453
+ }
4454
+ var IndexerExitDataSource = class {
4455
+ constructor(indexer) {
4456
+ this.indexer = indexer;
4457
+ }
4458
+ name = "indexer";
4459
+ async getVtxoChain(vtxo) {
4460
+ const chain = [];
4461
+ let pageIndex = 0;
4462
+ let hasMore = true;
4463
+ while (hasMore) {
4464
+ const { chain: page, page: meta } = await this.indexer.getVtxoChain(
4465
+ { txid: vtxo.txid, vout: vtxo.vout },
4466
+ { pageIndex, pageSize: DEFAULT_PAGE_SIZE }
4467
+ );
4468
+ chain.push(...page);
4469
+ hasMore = meta ? page.length === DEFAULT_PAGE_SIZE : false;
4470
+ pageIndex++;
4471
+ }
4472
+ return chain;
4473
+ }
4474
+ async getVirtualTxs(txids) {
4475
+ const out = /* @__PURE__ */ new Map();
4476
+ let pageIndex = 0;
4477
+ let hasMore = true;
4478
+ while (hasMore) {
4479
+ const { txs, page } = await this.indexer.getVirtualTxs(txids, {
4480
+ pageIndex,
4481
+ pageSize: DEFAULT_PAGE_SIZE
4482
+ });
4483
+ for (const psbt of txs) {
4484
+ out.set(Transaction.fromPSBT(base64.decode(psbt)).id, psbt);
4485
+ }
4486
+ hasMore = page ? txs.length === DEFAULT_PAGE_SIZE : false;
4487
+ pageIndex++;
4488
+ }
4489
+ return out;
4490
+ }
4491
+ };
4492
+ function chainedTxTypeToChainTxType(t) {
4493
+ switch (t) {
4494
+ case 1 /* Commitment */:
4495
+ return "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */;
4496
+ case 2 /* Ark */:
4497
+ return "INDEXER_CHAINED_TX_TYPE_ARK" /* ARK */;
4498
+ case 3 /* Tree */:
4499
+ return "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */;
4500
+ case 4 /* Checkpoint */:
4501
+ return "INDEXER_CHAINED_TX_TYPE_CHECKPOINT" /* CHECKPOINT */;
4502
+ default:
4503
+ return "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED" /* UNSPECIFIED */;
4504
+ }
4505
+ }
4506
+ function psbtInputTxids(psbtBase64) {
4507
+ const tx = Transaction.fromPSBT(base64.decode(psbtBase64));
4508
+ const ids = [];
4509
+ for (let i = 0; i < tx.inputsLength; i++) {
4510
+ const txid = tx.getInput(i).txid;
4511
+ if (txid) ids.push(hex.encode(txid));
4512
+ }
4513
+ return ids;
4514
+ }
4515
+ var RepositoryExitDataSource = class {
4516
+ constructor(repo) {
4517
+ this.repo = repo;
4518
+ }
4519
+ name = "repository";
4520
+ async getVtxoChain(vtxo) {
4521
+ const branch = await this.repo.getBranch(vtxo);
4522
+ if (branch.length === 0) return null;
4523
+ const chain = [];
4524
+ for (const v of branch) {
4525
+ const type = chainedTxTypeToChainTxType(v.type);
4526
+ let spends = [];
4527
+ if (v.type !== 1 /* Commitment */) {
4528
+ if (!v.psbt) return null;
4529
+ spends = psbtInputTxids(v.psbt);
4530
+ }
4531
+ chain.push({
4532
+ txid: v.txid,
4533
+ type,
4534
+ expiresAt: v.expiresAt != null ? String(v.expiresAt) : "",
4535
+ spends
4536
+ });
4537
+ }
4538
+ return chain;
4539
+ }
4540
+ async getVirtualTxs(txids) {
4541
+ const out = /* @__PURE__ */ new Map();
4542
+ for (const txid of txids) {
4543
+ const stored = await this.repo.getVirtualTx(txid);
4544
+ if (stored?.psbt) out.set(txid, stored.psbt);
4545
+ }
4546
+ return out;
4547
+ }
4548
+ };
4549
+
4550
+ // src/wallet/exit/resolver.ts
4551
+ var OrderedExitChainResolver = class {
4552
+ constructor(sources, persist) {
4553
+ this.sources = sources;
4554
+ this.persist = persist;
4555
+ }
4556
+ async getVtxoChain(vtxo) {
4557
+ let lastErr;
4558
+ for (const source of this.sources) {
4559
+ try {
4560
+ const chain = await source.getVtxoChain(vtxo);
4561
+ if (chain) return chain;
4562
+ } catch (e) {
4563
+ lastErr = e;
4564
+ }
4565
+ }
4566
+ if (lastErr) throw lastErr;
4567
+ throw new Error(`no exit-data source resolved the chain for ${vtxo.txid}:${vtxo.vout}`);
4568
+ }
4569
+ async getVirtualTxs(txids) {
4570
+ const found = /* @__PURE__ */ new Map();
4571
+ let remaining = txids;
4572
+ for (const [i, source] of this.sources.entries()) {
4573
+ if (remaining.length === 0) break;
4574
+ let got;
4575
+ try {
4576
+ got = await source.getVirtualTxs(remaining);
4577
+ } catch {
4578
+ continue;
4579
+ }
4580
+ const fresh = [];
4581
+ for (const [txid, psbt] of got) {
4582
+ if (found.has(txid)) continue;
4583
+ found.set(txid, psbt);
4584
+ fresh.push({ txid, psbt, expiresAt: null, type: 0 /* Unspecified */ });
4585
+ }
4586
+ if (i > 0 && this.persist && fresh.length > 0) {
4587
+ try {
4588
+ await this.persist.upsertVirtualTxs(fresh);
4589
+ } catch {
4590
+ }
4591
+ }
4592
+ remaining = txids.filter((t) => !found.has(t));
4593
+ }
4594
+ return [...found.values()];
4595
+ }
4596
+ };
4597
+ function createExitChainResolver(params) {
4598
+ const sources = [];
4599
+ if (params.repository) sources.push(new RepositoryExitDataSource(params.repository));
4600
+ if (params.extraSources) sources.push(...params.extraSources);
4601
+ sources.push(new IndexerExitDataSource(params.indexer));
4602
+ return new OrderedExitChainResolver(sources, params.repository);
4603
+ }
4604
+
4605
+ // src/providers/availability.ts
4606
+ function isRetryableProviderError(error) {
4607
+ return error instanceof ProviderUnavailableError || error instanceof FetchError || isFetchTimeoutError(error);
4608
+ }
4609
+
4610
+ // src/utils/syncCursors.ts
4611
+ var SAFETY_LAG_MS = 3e4;
4612
+ var OVERLAP_MS = 24 * 60 * 60 * 1e3;
4613
+ var walletStateLocks = /* @__PURE__ */ new WeakMap();
4614
+ async function updateWalletState(repo, updater) {
4615
+ const prev = walletStateLocks.get(repo) ?? Promise.resolve();
4616
+ const op = prev.then(async () => {
4617
+ const state = await repo.getWalletState() ?? {};
4618
+ await repo.saveWalletState(updater(state));
4619
+ });
4620
+ walletStateLocks.set(
4621
+ repo,
4622
+ op.catch(() => {
4623
+ })
4624
+ );
4625
+ return op;
4626
+ }
4627
+ var CURSOR_MIGRATED_KEY = "vtxoCursorMigrated";
4628
+ function hasMigrationMarker(state) {
4629
+ return state?.settings?.[CURSOR_MIGRATED_KEY] === true;
4630
+ }
4631
+ async function getSyncCursor(repo) {
4632
+ const state = await repo.getWalletState();
4633
+ if (!hasMigrationMarker(state)) return 0;
4634
+ return state?.lastSyncTime ?? 0;
4635
+ }
4636
+ async function advanceSyncCursor(repo, lastUpdatedAt) {
4637
+ await updateWalletState(repo, (state) => {
4638
+ const current = hasMigrationMarker(state) ? state.lastSyncTime ?? 0 : 0;
4639
+ return {
4640
+ ...state,
4641
+ lastSyncTime: Math.max(current, lastUpdatedAt),
4642
+ settings: {
4643
+ ...state.settings ?? {},
4644
+ [CURSOR_MIGRATED_KEY]: true
4645
+ }
4646
+ };
4647
+ });
4648
+ }
4649
+ async function clearSyncCursor(repo) {
4650
+ await updateWalletState(repo, (state) => {
4651
+ const { [CURSOR_MIGRATED_KEY]: _, ...restSettings } = state.settings ?? {};
4652
+ return {
4653
+ ...state,
4654
+ lastSyncTime: void 0,
4655
+ settings: restSettings
4656
+ };
4657
+ });
4658
+ }
4659
+ function computeSyncWindow(cursor) {
4660
+ const after = Math.max(0, cursor - OVERLAP_MS);
4661
+ return { after };
4662
+ }
4663
+ function cursorCutoff(requestStartedAt) {
4664
+ return (requestStartedAt ?? Date.now()) - SAFETY_LAG_MS;
4665
+ }
4666
+
4667
+ // src/wallet/arkInfoSnapshot.ts
4668
+ var ARK_INFO_SNAPSHOT_KEY = "arkInfoSnapshot";
4669
+ var MalformedArkInfoSnapshotError = class extends Error {
4670
+ constructor(message, options) {
4671
+ super(message, { cause: options?.cause });
4672
+ this.name = "MalformedArkInfoSnapshotError";
4673
+ }
4674
+ };
4675
+ var CURRENT_VERSION = 1;
4676
+ function serializeArkInfoSnapshot(info, savedAt) {
4677
+ return {
4678
+ version: CURRENT_VERSION,
4679
+ savedAt,
4680
+ source: "arkade.getInfo",
4681
+ arkInfo: {
4682
+ network: info.network,
4683
+ signerPubkey: info.signerPubkey,
4684
+ checkpointTapscript: info.checkpointTapscript,
4685
+ forfeitAddress: info.forfeitAddress,
4686
+ forfeitPubkey: info.forfeitPubkey,
4687
+ unilateralExitDelay: info.unilateralExitDelay.toString(),
4688
+ boardingExitDelay: info.boardingExitDelay.toString(),
4689
+ sessionDuration: info.sessionDuration.toString(),
4690
+ dust: info.dust.toString(),
4691
+ fees: info.fees,
4692
+ digest: info.digest,
4693
+ version: info.version,
4694
+ vtxoMinAmount: info.vtxoMinAmount.toString(),
4695
+ vtxoMaxAmount: info.vtxoMaxAmount.toString(),
4696
+ utxoMinAmount: info.utxoMinAmount.toString(),
4697
+ utxoMaxAmount: info.utxoMaxAmount.toString(),
4698
+ deprecatedSigners: info.deprecatedSigners.map((s) => ({
4699
+ pubkey: s.pubkey,
4700
+ cutoffDate: s.cutoffDate.toString()
4701
+ })),
4702
+ scheduledSession: info.scheduledSession ? {
4703
+ duration: info.scheduledSession.duration.toString(),
4704
+ fees: info.scheduledSession.fees,
4705
+ nextEndTime: info.scheduledSession.nextEndTime.toString(),
4706
+ nextStartTime: info.scheduledSession.nextStartTime.toString(),
4707
+ period: info.scheduledSession.period.toString()
4708
+ } : void 0
4709
+ }
4710
+ };
4711
+ }
4712
+ function hydrateArkInfo(snapshot) {
4713
+ const a = snapshot.arkInfo;
4714
+ return {
4715
+ boardingExitDelay: BigInt(a.boardingExitDelay),
4716
+ checkpointTapscript: a.checkpointTapscript,
4717
+ deprecatedSigners: a.deprecatedSigners.map((s) => ({
4718
+ cutoffDate: BigInt(s.cutoffDate),
4719
+ pubkey: s.pubkey
4720
+ })),
4721
+ digest: a.digest,
4722
+ dust: BigInt(a.dust),
4723
+ fees: a.fees,
4724
+ forfeitAddress: a.forfeitAddress,
4725
+ forfeitPubkey: a.forfeitPubkey,
4726
+ network: a.network,
4727
+ scheduledSession: a.scheduledSession ? {
4728
+ duration: BigInt(a.scheduledSession.duration),
4729
+ fees: a.scheduledSession.fees,
4730
+ nextEndTime: BigInt(a.scheduledSession.nextEndTime),
4731
+ nextStartTime: BigInt(a.scheduledSession.nextStartTime),
4732
+ period: BigInt(a.scheduledSession.period)
4733
+ } : void 0,
4734
+ serviceStatus: {},
4735
+ sessionDuration: BigInt(a.sessionDuration),
4736
+ signerPubkey: a.signerPubkey,
4737
+ unilateralExitDelay: BigInt(a.unilateralExitDelay),
4738
+ utxoMaxAmount: BigInt(a.utxoMaxAmount),
4739
+ utxoMinAmount: BigInt(a.utxoMinAmount),
4740
+ version: a.version,
4741
+ vtxoMaxAmount: BigInt(a.vtxoMaxAmount),
4742
+ vtxoMinAmount: BigInt(a.vtxoMinAmount)
4743
+ };
4744
+ }
4745
+ var DECIMAL_STRING = /^-?\d+$/;
4746
+ function assertString(value, path) {
4747
+ if (typeof value !== "string") {
4748
+ throw new MalformedArkInfoSnapshotError(`${path} must be a string`);
4749
+ }
4750
+ }
4751
+ function assertDecimalString(value, path) {
4752
+ assertString(value, path);
4753
+ if (!DECIMAL_STRING.test(value)) {
4754
+ throw new MalformedArkInfoSnapshotError(`${path} must be a decimal integer string`);
4755
+ }
4756
+ }
4757
+ function assertFeeInfo(value, path) {
4758
+ if (typeof value !== "object" || value === null) {
4759
+ throw new MalformedArkInfoSnapshotError(`${path} must be an object`);
4760
+ }
4761
+ const fees = value;
4762
+ assertString(fees.txFeeRate, `${path}.txFeeRate`);
4763
+ if (typeof fees.intentFee !== "object" || fees.intentFee === null) {
4764
+ throw new MalformedArkInfoSnapshotError(`${path}.intentFee must be an object`);
4765
+ }
4766
+ }
4767
+ function parseStoredArkInfoSnapshot(raw) {
4768
+ if (typeof raw !== "object" || raw === null) {
4769
+ throw new MalformedArkInfoSnapshotError("snapshot must be an object");
4770
+ }
4771
+ const snap = raw;
4772
+ if (snap.version !== CURRENT_VERSION) {
4773
+ throw new MalformedArkInfoSnapshotError(
4774
+ `unsupported snapshot version: ${String(snap.version)}`
4775
+ );
4776
+ }
4777
+ if (typeof snap.savedAt !== "number") {
4778
+ throw new MalformedArkInfoSnapshotError("savedAt must be a number");
4779
+ }
4780
+ if (snap.source !== "arkade.getInfo") {
4781
+ throw new MalformedArkInfoSnapshotError(`unexpected source: ${String(snap.source)}`);
4782
+ }
4783
+ if (typeof snap.arkInfo !== "object" || snap.arkInfo === null) {
4784
+ throw new MalformedArkInfoSnapshotError("arkInfo must be an object");
4785
+ }
4786
+ const a = snap.arkInfo;
4787
+ for (const field of [
4788
+ "network",
4789
+ "signerPubkey",
4790
+ "checkpointTapscript",
4791
+ "forfeitAddress",
4792
+ "forfeitPubkey",
4793
+ "digest",
4794
+ "version"
4795
+ ]) {
4796
+ assertString(a[field], `arkInfo.${field}`);
4797
+ }
4798
+ for (const field of [
4799
+ "unilateralExitDelay",
4800
+ "boardingExitDelay",
4801
+ "sessionDuration",
4802
+ "dust",
4803
+ "vtxoMinAmount",
4804
+ "vtxoMaxAmount",
4805
+ "utxoMinAmount",
4806
+ "utxoMaxAmount"
4807
+ ]) {
4808
+ assertDecimalString(a[field], `arkInfo.${field}`);
4809
+ }
4810
+ assertFeeInfo(a.fees, "arkInfo.fees");
4811
+ if (!Array.isArray(a.deprecatedSigners)) {
4812
+ throw new MalformedArkInfoSnapshotError("arkInfo.deprecatedSigners must be an array");
4813
+ }
4814
+ a.deprecatedSigners.forEach((s, i) => {
4815
+ if (typeof s !== "object" || s === null) {
4816
+ throw new MalformedArkInfoSnapshotError(
4817
+ `arkInfo.deprecatedSigners[${i}] must be an object`
4818
+ );
4819
+ }
4820
+ const signer = s;
4821
+ assertString(signer.pubkey, `arkInfo.deprecatedSigners[${i}].pubkey`);
4822
+ assertDecimalString(signer.cutoffDate, `arkInfo.deprecatedSigners[${i}].cutoffDate`);
4823
+ });
4824
+ if (a.scheduledSession !== void 0) {
4825
+ if (typeof a.scheduledSession !== "object" || a.scheduledSession === null) {
4826
+ throw new MalformedArkInfoSnapshotError("arkInfo.scheduledSession must be an object");
4827
+ }
4828
+ const s = a.scheduledSession;
4829
+ for (const field of ["duration", "nextEndTime", "nextStartTime", "period"]) {
4830
+ assertDecimalString(s[field], `arkInfo.scheduledSession.${field}`);
4831
+ }
4832
+ assertFeeInfo(s.fees, "arkInfo.scheduledSession.fees");
4833
+ }
4834
+ return raw;
4835
+ }
4836
+ async function loadArkInfoSnapshot(repo) {
4837
+ const state = await repo.getWalletState();
4838
+ const raw = state?.settings?.[ARK_INFO_SNAPSHOT_KEY];
4839
+ if (raw === void 0 || raw === null) return null;
4840
+ return parseStoredArkInfoSnapshot(raw);
4841
+ }
4842
+ async function saveArkInfoSnapshot(repo, info, savedAt) {
4843
+ const snapshot = serializeArkInfoSnapshot(info, savedAt);
4844
+ await updateWalletState(repo, (state) => ({
4845
+ ...state,
4846
+ settings: {
4847
+ ...state.settings ?? {},
4848
+ [ARK_INFO_SNAPSHOT_KEY]: snapshot
4849
+ }
4850
+ }));
4851
+ }
4852
+ async function resolveArkInfo(arkProvider, walletRepository) {
4853
+ let info;
4854
+ try {
4855
+ info = await arkProvider.getInfo();
4856
+ } catch (err) {
4857
+ if (!isRetryableProviderError(err)) throw err;
4858
+ const snapshot = await loadArkInfoSnapshot(walletRepository);
4859
+ if (!snapshot) {
4860
+ throw new ProviderUnavailableError(
4861
+ "Arkade server info is unavailable and no cached snapshot exists",
4862
+ { cause: err }
4863
+ );
4864
+ }
4865
+ return { info: hydrateArkInfo(snapshot), source: "cache", lastOnlineAt: snapshot.savedAt };
4866
+ }
4867
+ return { info, source: "live" };
4868
+ }
4869
+ async function saveValidatedArkInfoSnapshot(repo, info, savedAt) {
4870
+ try {
4871
+ await saveArkInfoSnapshot(repo, info, savedAt);
4872
+ } catch (err) {
4873
+ console.warn("Failed to persist Ark info snapshot", err);
4874
+ }
4875
+ }
4876
+ var TxTree = class {
4877
+ constructor(root, children = /* @__PURE__ */ new Map()) {
4878
+ this.root = root;
4879
+ this.children = children;
4880
+ }
4881
+ static create(chunks) {
4882
+ if (chunks.length === 0) {
4883
+ throw new Error("empty chunks");
4884
+ }
4885
+ const chunksByTxid = /* @__PURE__ */ new Map();
4886
+ for (const chunk of chunks) {
4268
4887
  const decodedChunk = decodeNode(chunk);
4269
4888
  const txid = decodedChunk.tx.id;
4270
4889
  chunksByTxid.set(txid, decodedChunk);
@@ -5363,20 +5982,6 @@ function isAnnotated(v) {
5363
5982
  return v.tapTree !== void 0 && v.forfeitTapLeafScript !== void 0 && v.intentTapLeafScript !== void 0;
5364
5983
  }
5365
5984
 
5366
- // src/repositories/virtualTxRepository.ts
5367
- var ChainedTxType = /* @__PURE__ */ ((ChainedTxType2) => {
5368
- ChainedTxType2[ChainedTxType2["Unspecified"] = 0] = "Unspecified";
5369
- ChainedTxType2[ChainedTxType2["Commitment"] = 1] = "Commitment";
5370
- ChainedTxType2[ChainedTxType2["Ark"] = 2] = "Ark";
5371
- ChainedTxType2[ChainedTxType2["Tree"] = 3] = "Tree";
5372
- ChainedTxType2[ChainedTxType2["Checkpoint"] = 4] = "Checkpoint";
5373
- return ChainedTxType2;
5374
- })(ChainedTxType || {});
5375
- function mergeChainedTxType(incoming, prev) {
5376
- if (incoming !== 0 /* Unspecified */) return incoming;
5377
- return prev ?? 0 /* Unspecified */;
5378
- }
5379
-
5380
5985
  // src/repositories/inMemory/intentRepository.ts
5381
5986
  var InMemoryIntentRepository = class {
5382
5987
  version = 1;
@@ -7796,66 +8401,9 @@ function dedupeOutpoints(outpoints) {
7796
8401
  return out;
7797
8402
  }
7798
8403
 
7799
- // src/utils/syncCursors.ts
7800
- var SAFETY_LAG_MS = 3e4;
7801
- var OVERLAP_MS = 24 * 60 * 60 * 1e3;
7802
- var walletStateLocks = /* @__PURE__ */ new WeakMap();
7803
- async function updateWalletState(repo, updater) {
7804
- const prev = walletStateLocks.get(repo) ?? Promise.resolve();
7805
- const op = prev.then(async () => {
7806
- const state = await repo.getWalletState() ?? {};
7807
- await repo.saveWalletState(updater(state));
7808
- });
7809
- walletStateLocks.set(
7810
- repo,
7811
- op.catch(() => {
7812
- })
7813
- );
7814
- return op;
7815
- }
7816
- var CURSOR_MIGRATED_KEY = "vtxoCursorMigrated";
7817
- function hasMigrationMarker(state) {
7818
- return state?.settings?.[CURSOR_MIGRATED_KEY] === true;
7819
- }
7820
- async function getSyncCursor(repo) {
7821
- const state = await repo.getWalletState();
7822
- if (!hasMigrationMarker(state)) return 0;
7823
- return state?.lastSyncTime ?? 0;
7824
- }
7825
- async function advanceSyncCursor(repo, lastUpdatedAt) {
7826
- await updateWalletState(repo, (state) => {
7827
- const current = hasMigrationMarker(state) ? state.lastSyncTime ?? 0 : 0;
7828
- return {
7829
- ...state,
7830
- lastSyncTime: Math.max(current, lastUpdatedAt),
7831
- settings: {
7832
- ...state.settings ?? {},
7833
- [CURSOR_MIGRATED_KEY]: true
7834
- }
7835
- };
7836
- });
7837
- }
7838
- async function clearSyncCursor(repo) {
7839
- await updateWalletState(repo, (state) => {
7840
- const { [CURSOR_MIGRATED_KEY]: _, ...restSettings } = state.settings ?? {};
7841
- return {
7842
- ...state,
7843
- lastSyncTime: void 0,
7844
- settings: restSettings
7845
- };
7846
- });
7847
- }
7848
- function computeSyncWindow(cursor) {
7849
- const after = Math.max(0, cursor - OVERLAP_MS);
7850
- return { after };
7851
- }
7852
- function cursorCutoff(requestStartedAt) {
7853
- return (requestStartedAt ?? Date.now()) - SAFETY_LAG_MS;
7854
- }
7855
-
7856
- // src/contracts/contractManager.ts
7857
- function areCoalescibleContractTypes(a, b) {
7858
- return a === "default" && b === "boarding" || a === "boarding" && b === "default";
8404
+ // src/contracts/contractManager.ts
8405
+ function areCoalescibleContractTypes(a, b) {
8406
+ return a === "default" && b === "boarding" || a === "boarding" && b === "default";
7859
8407
  }
7860
8408
  var SCAN_MAX_INDEX = 1e4;
7861
8409
  var DEFAULT_SCAN_BATCH = 10;
@@ -7865,6 +8413,10 @@ var ContractManager = class _ContractManager {
7865
8413
  initialized = false;
7866
8414
  eventCallbacks = /* @__PURE__ */ new Set();
7867
8415
  stopWatcherFn;
8416
+ /** `undefined` while online; the failure reason once a sync degrades. */
8417
+ syncDegradedReason;
8418
+ /** Epoch-ms of the last successful provider sync, if any. */
8419
+ lastSyncedAt;
7868
8420
  constructor(config) {
7869
8421
  this.config = config;
7870
8422
  this.watcher = new ContractWatcher({
@@ -7887,6 +8439,26 @@ var ContractManager = class _ContractManager {
7887
8439
  await cm.initialize();
7888
8440
  return cm;
7889
8441
  }
8442
+ /**
8443
+ * Latest provider-sync health. See {@link ContractSyncState}. Degradation is
8444
+ * recorded by {@link initialize}, {@link getContractsWithVtxos}, and
8445
+ * {@link createContract}; it flips back to `online` on the next successful
8446
+ * sync. Purely a freshness signal — not a source of truth for wallet data.
8447
+ */
8448
+ getSyncState() {
8449
+ return this.syncDegradedReason === void 0 ? { mode: "online", lastSyncedAt: this.lastSyncedAt } : {
8450
+ mode: "degraded",
8451
+ reason: this.syncDegradedReason,
8452
+ lastSyncedAt: this.lastSyncedAt
8453
+ };
8454
+ }
8455
+ markSyncOnline() {
8456
+ this.syncDegradedReason = void 0;
8457
+ this.lastSyncedAt = Date.now();
8458
+ }
8459
+ markSyncDegraded(err) {
8460
+ this.syncDegradedReason = err instanceof Error ? err.message : String(err);
8461
+ }
7890
8462
  async initialize() {
7891
8463
  if (this.initialized) {
7892
8464
  return;
@@ -7895,7 +8467,13 @@ var ContractManager = class _ContractManager {
7895
8467
  for (const contract of contracts) {
7896
8468
  await this.watcher.addContract(contract);
7897
8469
  }
7898
- await this.reconcileWatched();
8470
+ try {
8471
+ await this.reconcileWatched();
8472
+ this.markSyncOnline();
8473
+ } catch (err) {
8474
+ if (!isRetryableProviderError(err)) throw err;
8475
+ this.markSyncDegraded(err);
8476
+ }
7899
8477
  this.initialized = true;
7900
8478
  this.stopWatcherFn = await this.watcher.startWatching((event) => {
7901
8479
  this.handleContractEvent(event).catch((error) => {
@@ -7949,7 +8527,13 @@ var ContractManager = class _ContractManager {
7949
8527
  async createContract(params) {
7950
8528
  const { contract, persisted } = await this.upsertContract(params);
7951
8529
  if (persisted) {
7952
- await this.fetchContractVxosFromIndexer([contract]);
8530
+ try {
8531
+ await this.fetchContractVxosFromIndexer([contract]);
8532
+ this.markSyncOnline();
8533
+ } catch (err) {
8534
+ if (!isRetryableProviderError(err)) throw err;
8535
+ this.markSyncDegraded(err);
8536
+ }
7953
8537
  await this.watcher.addContract(contract);
7954
8538
  }
7955
8539
  return contract;
@@ -8148,7 +8732,13 @@ var ContractManager = class _ContractManager {
8148
8732
  }
8149
8733
  async getContractsWithVtxos(filter, pageSize) {
8150
8734
  const contracts = await this.getContracts(filter);
8151
- await this.syncContracts({ contracts, pageSize });
8735
+ try {
8736
+ await this.syncContracts({ contracts, pageSize });
8737
+ this.markSyncOnline();
8738
+ } catch (err) {
8739
+ if (!isRetryableProviderError(err)) throw err;
8740
+ this.markSyncDegraded(err);
8741
+ }
8152
8742
  const vtxos = await this.getVtxosForContracts(contracts);
8153
8743
  return contracts.map((contract) => ({
8154
8744
  contract,
@@ -8386,15 +8976,33 @@ var ContractManager = class _ContractManager {
8386
8976
  * Handle events from the watcher.
8387
8977
  */
8388
8978
  async handleContractEvent(event) {
8389
- switch (event.type) {
8390
- // Delta-sync only the changed virtual outputs for this contract.
8391
- case "vtxo_received":
8392
- case "vtxo_spent":
8393
- await this.syncContracts({ contracts: [event.contract] });
8394
- break;
8395
- case "connection_reset":
8396
- await this.reconcileWatched();
8397
- break;
8979
+ try {
8980
+ switch (event.type) {
8981
+ // Delta-sync only the changed virtual outputs for this contract.
8982
+ case "vtxo_received":
8983
+ await this.syncContracts({ contracts: [event.contract] });
8984
+ this.markSyncOnline();
8985
+ break;
8986
+ case "vtxo_spent":
8987
+ await this.syncContracts({ contracts: [event.contract] });
8988
+ this.markSyncOnline();
8989
+ if (this.config.onVtxosSpent) {
8990
+ try {
8991
+ await this.config.onVtxosSpent(
8992
+ event.vtxos.map((v) => ({ txid: v.txid, vout: v.vout }))
8993
+ );
8994
+ } catch {
8995
+ }
8996
+ }
8997
+ break;
8998
+ case "connection_reset":
8999
+ await this.reconcileWatched();
9000
+ this.markSyncOnline();
9001
+ break;
9002
+ }
9003
+ } catch (err) {
9004
+ if (!isRetryableProviderError(err)) throw err;
9005
+ this.markSyncDegraded(err);
8398
9006
  }
8399
9007
  this.emitEvent(event);
8400
9008
  }
@@ -8476,6 +9084,12 @@ var ContractManager = class _ContractManager {
8476
9084
  contract,
8477
9085
  filtered
8478
9086
  );
9087
+ if (this.config.onVtxosPersisted) {
9088
+ try {
9089
+ await this.config.onVtxosPersisted(contract, filtered);
9090
+ } catch {
9091
+ }
9092
+ }
8479
9093
  }
8480
9094
  }
8481
9095
  async fetchContractVxosFromIndexer(contracts, pageSize, syncWindow) {
@@ -8496,6 +9110,15 @@ var ContractManager = class _ContractManager {
8496
9110
  contract,
8497
9111
  filtered
8498
9112
  );
9113
+ if (this.config.onVtxosPersisted) {
9114
+ try {
9115
+ await this.config.onVtxosPersisted(
9116
+ contract,
9117
+ filtered
9118
+ );
9119
+ } catch {
9120
+ }
9121
+ }
8499
9122
  }
8500
9123
  }
8501
9124
  return result;
@@ -9369,6 +9992,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
9369
9992
  * (ContractManager isn't given it); `undefined` ⇒ no-op.
9370
9993
  */
9371
9994
  virtualTxRepository;
9995
+ /** Opt-in exit-data capture settings; see {@link StorageConfig.exitDataCapture}. */
9996
+ exitDataCapture;
9372
9997
  _assetManager;
9373
9998
  walletContractTimelocks;
9374
9999
  // Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
@@ -9411,6 +10036,72 @@ var ReadonlyWallet = class _ReadonlyWallet {
9411
10036
  * construction-time snapshot for their lifetime.
9412
10037
  */
9413
10038
  _arkServerPublicKey;
10039
+ /**
10040
+ * Whether this wallet was constructed from live operator server-info
10041
+ * (`"live"`) or from a cached snapshot because the operator was unreachable
10042
+ * (`"cache"`). Freshness signal for {@link getProviderConnectionState}.
10043
+ */
10044
+ _serverInfoSource = "live";
10045
+ /**
10046
+ * Epoch-ms of the last known live operator contact: construction time on the
10047
+ * `live` boot path, the cached snapshot's `savedAt` on the `cache` path.
10048
+ */
10049
+ _serverInfoLastOnlineAt;
10050
+ /** @see {@link _serverInfoSource} */
10051
+ get serverInfoSource() {
10052
+ return this._serverInfoSource;
10053
+ }
10054
+ /**
10055
+ * Composed provider-connection freshness: the boot server-info source
10056
+ * (Arkade) combined with the contract-manager's indexer-sync health, if the
10057
+ * manager has been initialized. Reads no live provider state — it never
10058
+ * forces a `ContractManager` to construct — so it is safe for readonly
10059
+ * callers that only use address/balance APIs.
10060
+ *
10061
+ * - Boot fell back to a cached snapshot → degraded on `arkade` (`cache`).
10062
+ * - Otherwise, if the contract manager has degraded to repository data →
10063
+ * degraded on `indexer` (`repository`).
10064
+ * - Otherwise online.
10065
+ *
10066
+ * This only describes sync freshness; wallet balances/VTXOs are always read
10067
+ * from the repository regardless of this state.
10068
+ */
10069
+ getProviderConnectionState() {
10070
+ if (this._serverInfoSource === "cache") {
10071
+ return {
10072
+ mode: "degraded",
10073
+ source: "cache",
10074
+ provider: "arkade",
10075
+ reason: "constructed from cached server-info; operator was unreachable at boot",
10076
+ lastOnlineAt: this._serverInfoLastOnlineAt
10077
+ };
10078
+ }
10079
+ const sync = this._contractManager?.getSyncState();
10080
+ if (sync?.mode === "degraded") {
10081
+ return {
10082
+ mode: "degraded",
10083
+ source: "repository",
10084
+ provider: "indexer",
10085
+ reason: sync.reason,
10086
+ lastOnlineAt: sync.lastSyncedAt ?? this._serverInfoLastOnlineAt
10087
+ };
10088
+ }
10089
+ return {
10090
+ mode: "online",
10091
+ source: "live",
10092
+ lastOnlineAt: sync?.lastSyncedAt ?? this._serverInfoLastOnlineAt ?? 0
10093
+ };
10094
+ }
10095
+ /**
10096
+ * The contract-manager's current provider-sync health **without forcing it
10097
+ * to initialize** — reads the already-constructed manager, or reports
10098
+ * `online` when none exists yet. Unlike {@link getContractManager}, this
10099
+ * never triggers a remote sync, so it is safe on a pure diagnostics path
10100
+ * (e.g. the service-worker sync-state message).
10101
+ */
10102
+ getContractSyncState() {
10103
+ return this._contractManager?.getSyncState() ?? { mode: "online" };
10104
+ }
9414
10105
  /**
9415
10106
  * x-only hex of the operator's deprecated signer keys (from
9416
10107
  * `ArkInfo.deprecatedSigners`), cached for the OFFLINE read/watch paths.
@@ -9545,7 +10236,13 @@ var ReadonlyWallet = class _ReadonlyWallet {
9545
10236
  }
9546
10237
  indexerProvider = new RestIndexerProvider(indexerUrl);
9547
10238
  }
9548
- const info = await arkProvider.getInfo();
10239
+ const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
10240
+ const contractRepository = config.storage?.contractRepository ?? new IndexedDBContractRepository();
10241
+ const {
10242
+ info,
10243
+ source: serverInfoSource,
10244
+ lastOnlineAt: serverInfoLastOnlineAt
10245
+ } = await resolveArkInfo(arkProvider, walletRepository);
9549
10246
  const network = getNetwork(info.network);
9550
10247
  if ("descriptor" in config.identity) {
9551
10248
  const descriptor = config.identity.descriptor;
@@ -9599,8 +10296,6 @@ var ReadonlyWallet = class _ReadonlyWallet {
9599
10296
  serverPubKey: hex.encode(serverPubKey),
9600
10297
  csvTimelock: timelockToSequence(boardingTimelock).toString()
9601
10298
  });
9602
- const walletRepository = config.storage?.walletRepository ?? new IndexedDBWalletRepository();
9603
- const contractRepository = config.storage?.contractRepository ?? new IndexedDBContractRepository();
9604
10299
  return {
9605
10300
  arkProvider,
9606
10301
  indexerProvider,
@@ -9614,6 +10309,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
9614
10309
  walletRepository,
9615
10310
  contractRepository,
9616
10311
  info,
10312
+ serverInfoSource,
10313
+ serverInfoLastOnlineAt,
9617
10314
  delegateProvider: config.delegateProvider || config.delegatorProvider,
9618
10315
  /** @deprecated alias for `delegateProvider` */
9619
10316
  delegatorProvider: config.delegateProvider || config.delegatorProvider,
@@ -9649,6 +10346,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
9649
10346
  );
9650
10347
  wallet.intentRepository = config.storage?.intentRepository;
9651
10348
  wallet.virtualTxRepository = config.storage?.virtualTxRepository;
10349
+ wallet.exitDataCapture = config.storage?.exitDataCapture;
10350
+ wallet._serverInfoSource = setup.serverInfoSource;
10351
+ if (setup.serverInfoSource === "live") {
10352
+ const now = Date.now();
10353
+ await saveValidatedArkInfoSnapshot(setup.walletRepository, setup.info, now);
10354
+ wallet._serverInfoLastOnlineAt = now;
10355
+ } else {
10356
+ wallet._serverInfoLastOnlineAt = setup.serverInfoLastOnlineAt;
10357
+ }
9652
10358
  wallet.refreshDeprecatedSigners(setup.info);
9653
10359
  return wallet;
9654
10360
  }
@@ -9779,7 +10485,10 @@ var ReadonlyWallet = class _ReadonlyWallet {
9779
10485
  const response = await contractManager.getContractsWithVtxos();
9780
10486
  const allVtxos = response.flatMap((_) => _.vtxos);
9781
10487
  const { boardingTxs, commitmentsToIgnore } = await this.getBoardingTxs();
9782
- const getTxCreatedAt = (txid) => this.indexerProvider.getVtxos({ outpoints: [{ txid, vout: 0 }] }).then((res) => res.vtxos[0]?.createdAt.getTime());
10488
+ const getTxCreatedAt = (txid) => this.indexerProvider.getVtxos({ outpoints: [{ txid, vout: 0 }] }).then((res) => res.vtxos[0]?.createdAt.getTime()).catch((err) => {
10489
+ if (isRetryableProviderError(err)) return void 0;
10490
+ throw err;
10491
+ });
9783
10492
  return buildTransactionHistory(allVtxos, boardingTxs, commitmentsToIgnore, getTxCreatedAt);
9784
10493
  }
9785
10494
  /**
@@ -10198,11 +10907,39 @@ var ReadonlyWallet = class _ReadonlyWallet {
10198
10907
  }
10199
10908
  }
10200
10909
  async initializeContractManager() {
10910
+ const virtualTxRepository = this.virtualTxRepository;
10911
+ let onVtxosPersisted;
10912
+ let onVtxosSpent;
10913
+ if (virtualTxRepository) {
10914
+ const capture = this.exitDataCapture;
10915
+ const resolver = createExitChainResolver({
10916
+ indexer: this.indexerProvider,
10917
+ repository: virtualTxRepository,
10918
+ extraSources: capture?.sources
10919
+ });
10920
+ onVtxosPersisted = async (_contract, vtxos) => {
10921
+ for (const v of vtxos) {
10922
+ if (v.virtualStatus.state === "spent") continue;
10923
+ await captureExitBranch({
10924
+ resolver,
10925
+ repository: virtualTxRepository,
10926
+ vtxo: { txid: v.txid, vout: v.vout },
10927
+ value: v.value,
10928
+ mode: capture?.mode ?? DEFAULT_EXIT_CAPTURE_MODE,
10929
+ minExitWorthSats: capture?.minExitWorthSats ?? DEFAULT_MIN_EXIT_WORTH_SATS
10930
+ }).catch(() => {
10931
+ });
10932
+ }
10933
+ };
10934
+ onVtxosSpent = (vtxos) => pruneExitBranches(virtualTxRepository, vtxos);
10935
+ }
10201
10936
  const manager = await ContractManager.create({
10202
10937
  indexerProvider: this.indexerProvider,
10203
10938
  contractRepository: this.contractRepository,
10204
10939
  walletRepository: this.walletRepository,
10205
10940
  intentRepository: this.intentRepository,
10941
+ onVtxosPersisted,
10942
+ onVtxosSpent,
10206
10943
  watcherConfig: this.watcherConfig
10207
10944
  });
10208
10945
  const baselinePubkey = await this.identity.xOnlyPublicKey();
@@ -10877,6 +11614,14 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10877
11614
  boot?.rotator,
10878
11615
  boot?.provider
10879
11616
  );
11617
+ wallet._serverInfoSource = setup.serverInfoSource;
11618
+ if (setup.serverInfoSource === "live") {
11619
+ const now = Date.now();
11620
+ await saveValidatedArkInfoSnapshot(setup.walletRepository, setup.info, now);
11621
+ wallet._serverInfoLastOnlineAt = now;
11622
+ } else {
11623
+ wallet._serverInfoLastOnlineAt = setup.serverInfoLastOnlineAt;
11624
+ }
10880
11625
  wallet.refreshDeprecatedSigners(setup.info);
10881
11626
  {
10882
11627
  const ap = setup.arkProvider;
@@ -10898,6 +11643,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
10898
11643
  }
10899
11644
  wallet.intentRepository = config.storage?.intentRepository;
10900
11645
  wallet.virtualTxRepository = config.storage?.virtualTxRepository;
11646
+ wallet.exitDataCapture = config.storage?.exitDataCapture;
10901
11647
  await wallet.getVtxoManager();
10902
11648
  return wallet;
10903
11649
  }
@@ -13415,10 +14161,18 @@ var WalletMessageHandler = class {
13415
14161
  type: "WALLET_STATUS",
13416
14162
  payload: {
13417
14163
  walletInitialized: true,
13418
- xOnlyPublicKey: pubKey
14164
+ xOnlyPublicKey: pubKey,
14165
+ providerConnectionState: this.readonlyWallet.getProviderConnectionState()
13419
14166
  }
13420
14167
  });
13421
14168
  }
14169
+ case "GET_CONTRACT_SYNC_STATE": {
14170
+ return this.tagged({
14171
+ id,
14172
+ type: "CONTRACT_SYNC_STATE",
14173
+ payload: { syncState: this.readonlyWallet.getContractSyncState() }
14174
+ });
14175
+ }
13422
14176
  case "CLEAR": {
13423
14177
  await this.clear();
13424
14178
  return this.tagged({
@@ -14181,6 +14935,7 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
14181
14935
  GET_BALANCE: 1e4,
14182
14936
  GET_BOARDING_ADDRESS: 1e4,
14183
14937
  GET_STATUS: 1e4,
14938
+ GET_CONTRACT_SYNC_STATE: 1e4,
14184
14939
  GET_DELEGATE_INFO: 1e4,
14185
14940
  IS_CONTRACT_MANAGER_WATCHING: 1e4,
14186
14941
  // Medium reads — may involve indexer queries
@@ -14789,6 +15544,19 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
14789
15544
  throw new Error(`Failed to get status: ${error}`);
14790
15545
  }
14791
15546
  }
15547
+ /**
15548
+ * Wallet-level provider-connection freshness, delegated to the worker via
15549
+ * `GET_STATUS`. Async by necessity (the worker boundary is asynchronous); no
15550
+ * synchronous variant is offered because it would have the same transport
15551
+ * mismatch as the contract-manager proxy's cached `getSyncState()`.
15552
+ */
15553
+ async getProviderConnectionState() {
15554
+ const { providerConnectionState } = await this.getStatus();
15555
+ if (!providerConnectionState) {
15556
+ throw new Error("Worker did not report provider connection state");
15557
+ }
15558
+ return providerConnectionState;
15559
+ }
14792
15560
  async getActivityHistory() {
14793
15561
  return buildActivities(await this.getTransactionHistory(), this.activity.all());
14794
15562
  }
@@ -14843,6 +15611,32 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
14843
15611
  return wallet.sendMessage(message);
14844
15612
  };
14845
15613
  const messageTag = this.messageTag;
15614
+ const fetchSyncState = async () => {
15615
+ const message = {
15616
+ type: "GET_CONTRACT_SYNC_STATE",
15617
+ id: getRandomId(),
15618
+ tag: messageTag
15619
+ };
15620
+ const response = await sendContractMessage(message);
15621
+ return response.payload.syncState;
15622
+ };
15623
+ const UNKNOWN_STATE = {
15624
+ mode: "degraded",
15625
+ reason: "contract sync state unavailable from the worker"
15626
+ };
15627
+ let syncState = UNKNOWN_STATE;
15628
+ let everProbed = false;
15629
+ const refreshSyncState = async () => {
15630
+ try {
15631
+ syncState = await fetchSyncState();
15632
+ everProbed = true;
15633
+ } catch {
15634
+ if (!everProbed) {
15635
+ syncState = UNKNOWN_STATE;
15636
+ }
15637
+ }
15638
+ };
15639
+ await refreshSyncState();
14846
15640
  const manager = {
14847
15641
  async createContract(params) {
14848
15642
  const message = {
@@ -14853,6 +15647,7 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
14853
15647
  };
14854
15648
  try {
14855
15649
  const response = await sendContractMessage(message);
15650
+ await refreshSyncState();
14856
15651
  return response.payload.contract;
14857
15652
  } catch (e) {
14858
15653
  throw new Error("Failed to create contract");
@@ -14881,11 +15676,15 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
14881
15676
  };
14882
15677
  try {
14883
15678
  const response = await sendContractMessage(message);
15679
+ await refreshSyncState();
14884
15680
  return response.payload.contracts;
14885
15681
  } catch (e) {
14886
15682
  throw new Error("Failed to get contracts with vtxos");
14887
15683
  }
14888
15684
  },
15685
+ getSyncState() {
15686
+ return syncState;
15687
+ },
14889
15688
  async annotateVtxos(vtxos) {
14890
15689
  if (vtxos.length === 0) return [];
14891
15690
  const message = {
@@ -14994,7 +15793,11 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
14994
15793
  tag: messageTag,
14995
15794
  payload: opts
14996
15795
  };
14997
- await sendContractMessage(message);
15796
+ try {
15797
+ await sendContractMessage(message);
15798
+ } finally {
15799
+ await refreshSyncState();
15800
+ }
14998
15801
  },
14999
15802
  async refreshOutpoints(outpoints) {
15000
15803
  const message = {
@@ -15003,7 +15806,11 @@ var ServiceWorkerReadonlyWallet = class _ServiceWorkerReadonlyWallet {
15003
15806
  tag: messageTag,
15004
15807
  payload: { outpoints }
15005
15808
  };
15006
- await sendContractMessage(message);
15809
+ try {
15810
+ await sendContractMessage(message);
15811
+ } finally {
15812
+ await refreshSyncState();
15813
+ }
15007
15814
  },
15008
15815
  scanContracts() {
15009
15816
  return Promise.reject(
@@ -15584,42 +16391,11 @@ var OnchainWallet = class _OnchainWallet {
15584
16391
  * @see send
15585
16392
  */
15586
16393
  async bumpP2A(parent) {
15587
- const parentVsize = parent.vsize;
15588
- let child = new Transaction({
15589
- version: 3,
15590
- allowLegacyWitnessUtxo: true
15591
- });
15592
- child.addInput(findP2AOutput(parent));
15593
- const childVsize = TxWeightEstimator.create().addKeySpendInput(true).addP2AInput().addOutputAddress(this.address, this.network).vsize().value;
15594
- const packageVSize = parentVsize + Number(childVsize);
15595
16394
  let feeRate = await this.provider.getFeeRate();
15596
16395
  if (!feeRate || feeRate < _OnchainWallet.MIN_FEE_RATE) {
15597
16396
  feeRate = _OnchainWallet.MIN_FEE_RATE;
15598
16397
  }
15599
- const fee = Math.ceil(feeRate * packageVSize);
15600
- if (!fee) {
15601
- throw new Error(
15602
- `invalid fee, got ${fee} with vsize ${packageVSize}, feeRate ${feeRate}`
15603
- );
15604
- }
15605
- const coins = await this.getCoins();
15606
- const selected = selectCoins(coins, fee, true);
15607
- for (const input of selected.inputs) {
15608
- child.addInput({
15609
- txid: input.txid,
15610
- index: input.vout,
15611
- witnessUtxo: {
15612
- script: this.onchainP2TR.script,
15613
- amount: BigInt(input.value)
15614
- },
15615
- tapInternalKey: this.onchainP2TR.tapInternalKey
15616
- });
15617
- }
15618
- child.addOutputAddress(this.address, P2A.amount + selected.changeAmount, this.network);
15619
- child = await this.identity.sign(child);
15620
- for (let i = 1; i < child.inputsLength; i++) {
15621
- child.finalizeIdx(i);
15622
- }
16398
+ const child = await this.buildBumpPackage(parent, feeRate, await this.getCoins());
15623
16399
  try {
15624
16400
  await this.provider.broadcastTransaction(parent.hex, child.hex);
15625
16401
  } catch (error) {
@@ -15628,6 +16404,59 @@ var OnchainWallet = class _OnchainWallet {
15628
16404
  return [parent.hex, child.hex];
15629
16405
  }
15630
16406
  }
16407
+ /**
16408
+ * Build and sign a CPFP fee child for a parent tx (given as raw hex)
16409
+ * carrying a P2A anchor, funding it from this wallet's **confirmed**
16410
+ * coins, and return the 1P1C package hexes WITHOUT broadcasting.
16411
+ *
16412
+ * This is the graph-mode fee source ({@link ExitFeeWallet}): the exit
16413
+ * executor calls it to bump each transported virtual tx at execution
16414
+ * time, so funding can be deferred rather than pre-signed.
16415
+ *
16416
+ * @param parentHex - Finalized parent transaction, raw network hex
16417
+ * @param feeRate - sat/vB floor for the package (raised to MIN_FEE_RATE)
16418
+ * @returns Tuple of parent hex (unchanged) and signed child hex
16419
+ * @throws If the parent has no anchor, or funding cannot be selected/signed
16420
+ */
16421
+ async bumpAnchor(parentHex, feeRate) {
16422
+ const parent = Transaction.fromRaw(hex.decode(parentHex));
16423
+ const coins = (await this.getCoins()).filter((c) => c.status.confirmed);
16424
+ const child = await this.buildBumpPackage(parent, feeRate, coins);
16425
+ return [parent.hex, child.hex];
16426
+ }
16427
+ /**
16428
+ * Shared core of {@link bumpP2A} and {@link bumpAnchor}: probe the package
16429
+ * fee with a single-input child, select coins for it, then build and sign
16430
+ * with the actual selection (the fee grows per extra input).
16431
+ */
16432
+ async buildBumpPackage(parent, feeRate, coins) {
16433
+ let rate = feeRate;
16434
+ if (!rate || rate < _OnchainWallet.MIN_FEE_RATE) {
16435
+ rate = _OnchainWallet.MIN_FEE_RATE;
16436
+ }
16437
+ const probeVsize = TxWeightEstimator.create().addKeySpendInput(true).addP2AInput().addOutputAddress(this.address, this.network).vsize().value;
16438
+ const probeFee = Math.ceil(rate * (parent.vsize + Number(probeVsize)));
16439
+ if (!probeFee) {
16440
+ throw new Error(
16441
+ `invalid fee, got ${probeFee} with vsize ${parent.vsize + Number(probeVsize)}, feeRate ${rate}`
16442
+ );
16443
+ }
16444
+ const selected = selectCoins(coins, probeFee, true);
16445
+ const { child } = buildAnchorChild({
16446
+ parent,
16447
+ feeRate: rate,
16448
+ fundingCoins: selected.inputs,
16449
+ changeAddress: this.address,
16450
+ changeScript: this.onchainP2TR.script,
16451
+ tapInternalKey: this.onchainP2TR.tapInternalKey,
16452
+ network: this.network
16453
+ });
16454
+ const signed = await this.identity.sign(child);
16455
+ for (let i = 1; i < signed.inputsLength; i++) {
16456
+ signed.finalizeIdx(i);
16457
+ }
16458
+ return signed;
16459
+ }
15631
16460
  };
15632
16461
  function selectCoins(coins, targetAmount, forceChange = false) {
15633
16462
  if (isNaN(targetAmount)) {
@@ -16518,7 +17347,85 @@ function craftBIP322ToSignSimple(toSpend, pkScript) {
16518
17347
  });
16519
17348
  return tx;
16520
17349
  }
16521
- function chainTxTypeToChainedExit(t) {
17350
+ function finalizeVirtualTx(type, psbtBase64) {
17351
+ const tx = Transaction.fromPSBT(base64.decode(psbtBase64));
17352
+ if (type === "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */) {
17353
+ const input = tx.getInput(0);
17354
+ if (!input) {
17355
+ throw new Error("Input not found");
17356
+ }
17357
+ const tapKeySig = input.tapKeySig;
17358
+ if (!tapKeySig) {
17359
+ throw new Error("Tap key sig not found");
17360
+ }
17361
+ tx.updateInput(0, {
17362
+ finalScriptWitness: [tapKeySig]
17363
+ });
17364
+ return tx;
17365
+ }
17366
+ tx.finalize();
17367
+ return tx;
17368
+ }
17369
+ var ExitPathError = class extends Error {
17370
+ constructor(reason, message) {
17371
+ super(message);
17372
+ this.reason = reason;
17373
+ this.name = "ExitPathError";
17374
+ }
17375
+ };
17376
+ function delayWeight(sequence) {
17377
+ const t = sequenceToTimelock(sequence);
17378
+ return t.type === "blocks" ? Number(t.value) : Number(t.value) / 600;
17379
+ }
17380
+ async function resolveUnilateralPath(params) {
17381
+ const { vtxo, scriptHex, contractRepository } = params;
17382
+ const contract = contractRepository ? (await contractRepository.getContracts({ script: scriptHex }))[0] : void 0;
17383
+ if (contract) {
17384
+ const handler = contractHandlers.get(contract.type);
17385
+ if (!handler) {
17386
+ throw new ExitPathError(
17387
+ "no-handler",
17388
+ `no contract handler registered for type '${contract.type}'`
17389
+ );
17390
+ }
17391
+ const script = handler.createScript(contract.params);
17392
+ const paths = handler.getAllSpendingPaths(script, contract, {
17393
+ collaborative: false,
17394
+ currentTime: params.currentTime,
17395
+ walletDescriptor: params.walletDescriptor,
17396
+ walletPubKey: params.walletPubKeyHex
17397
+ }).filter((p) => p.sequence !== void 0);
17398
+ if (paths.length === 0) {
17399
+ throw new ExitPathError(
17400
+ "no-unilateral-path",
17401
+ `no unilateral path for vtxo ${vtxo.txid}:${vtxo.vout} (type '${contract.type}')`
17402
+ );
17403
+ }
17404
+ paths.sort((a, b) => delayWeight(a.sequence) - delayWeight(b.sequence));
17405
+ return { selection: paths[0], label: `${contract.type}:unilateral` };
17406
+ }
17407
+ const decoded = VtxoScript.decode(vtxo.tapTree);
17408
+ let best;
17409
+ for (const exit of decoded.exitPaths()) {
17410
+ const leaf = decoded.findLeaf(hex.encode(exit.script));
17411
+ if (!leaf) continue;
17412
+ const sequence = timelockToSequence(exit.params.timelock);
17413
+ const weight = delayWeight(sequence);
17414
+ if (!best || weight < best.weight) {
17415
+ best = { selection: { leaf, sequence }, weight };
17416
+ }
17417
+ }
17418
+ if (!best) {
17419
+ throw new ExitPathError(
17420
+ "no-unilateral-path",
17421
+ `no exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`
17422
+ );
17423
+ }
17424
+ return { selection: best.selection, label: "default:exit" };
17425
+ }
17426
+
17427
+ // src/wallet/unroll.ts
17428
+ function chainTxTypeToChainedExit2(t) {
16522
17429
  switch (t) {
16523
17430
  case "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */:
16524
17431
  return 1 /* Commitment */;
@@ -16581,7 +17488,7 @@ var Unroll;
16581
17488
  txid: next.txid,
16582
17489
  psbt,
16583
17490
  expiresAt: null,
16584
- type: chainTxTypeToChainedExit(next.type)
17491
+ type: chainTxTypeToChainedExit2(next.type)
16585
17492
  };
16586
17493
  try {
16587
17494
  await repo.upsertVirtualTxs([cached]);
@@ -16627,22 +17534,7 @@ var Unroll;
16627
17534
  if (!virtualTxBase64) {
16628
17535
  throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
16629
17536
  }
16630
- const tx = Transaction.fromPSBT(base64.decode(virtualTxBase64));
16631
- if (nextTxToBroadcast.type === "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */) {
16632
- const input = tx.getInput(0);
16633
- if (!input) {
16634
- throw new Error("Input not found");
16635
- }
16636
- const tapKeySig = input.tapKeySig;
16637
- if (!tapKeySig) {
16638
- throw new Error("Tap key sig not found");
16639
- }
16640
- tx.updateInput(0, {
16641
- finalScriptWitness: [tapKeySig]
16642
- });
16643
- } else {
16644
- tx.finalize();
16645
- }
17537
+ const tx = finalizeVirtualTx(nextTxToBroadcast.type, virtualTxBase64);
16646
17538
  const pkg = await this.bumper.bumpP2A(tx);
16647
17539
  return {
16648
17540
  type: 0 /* UNROLL */,
@@ -16696,20 +17588,21 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
16696
17588
  if (!txStatus.confirmed) {
16697
17589
  throw new Error(`tx ${vtxo.txid} is not confirmed`);
16698
17590
  }
16699
- const exit = availableExitPath(
16700
- { height: txStatus.blockHeight, time: txStatus.blockTime },
16701
- chainTip,
16702
- vtxo
16703
- );
16704
- if (!exit) {
17591
+ const resolved = await resolveUnilateralPath({
17592
+ vtxo,
17593
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
17594
+ contractRepository: wallet.contractRepository,
17595
+ walletPubKeyHex: hex.encode(await wallet.identity.xOnlyPublicKey()),
17596
+ currentTime: Date.now()
17597
+ });
17598
+ const spendingLeaf = resolved.selection.leaf;
17599
+ const sequence = resolved.selection.sequence;
17600
+ const timelock = sequenceToTimelock(sequence);
17601
+ const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
17602
+ if (!elapsed) {
16705
17603
  throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
16706
17604
  }
16707
- const spendingLeaf = VtxoScript.decode(vtxo.tapTree).findLeaf(hex.encode(exit.script));
16708
- if (!spendingLeaf) {
16709
- throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
16710
- }
16711
17605
  totalAmount += BigInt(vtxo.value);
16712
- const sequence = timelockToSequence(exit.params.timelock);
16713
17606
  inputs.push({
16714
17607
  txid: vtxo.txid,
16715
17608
  index: vtxo.vout,
@@ -16773,21 +17666,711 @@ function doWait(onchainProvider, txid) {
16773
17666
  });
16774
17667
  };
16775
17668
  }
16776
- function availableExitPath(confirmedAt, current, vtxo) {
16777
- const exits = VtxoScript.decode(vtxo.tapTree).exitPaths();
16778
- for (const exit of exits) {
16779
- if (exit.params.timelock.type === "blocks") {
16780
- if (current.height >= confirmedAt.height + Number(exit.params.timelock.value)) {
16781
- return exit;
17669
+ function extraWitnessSize(path) {
17670
+ return (path.extraWitness ?? []).reduce((sum, item) => sum + 1 + item.length, 0);
17671
+ }
17672
+ function sweepFeeFor(path, outputAddress, network, feeRate) {
17673
+ const [controlBlock, scriptWithVersion] = path.leaf;
17674
+ const estimator = TxWeightEstimator.create().addTapscriptInput(
17675
+ 64 + extraWitnessSize(path),
17676
+ scriptWithVersion.length,
17677
+ TaprootControlBlock.encode(controlBlock).length
17678
+ ).addOutputAddress(outputAddress, network);
17679
+ return Number(estimator.vsize().fee(BigInt(Math.ceil(feeRate))));
17680
+ }
17681
+ async function buildSignedSweep(params) {
17682
+ const { vtxo, path, outputAddress, feeRate, network, identity } = params;
17683
+ const fee = sweepFeeFor(path, outputAddress, network, feeRate);
17684
+ const sendAmount = BigInt(vtxo.value) - BigInt(fee);
17685
+ if (sendAmount < BigInt(DUST_AMOUNT)) {
17686
+ throw new Error(
17687
+ `uneconomic vtxo ${vtxo.txid}:${vtxo.vout}: value ${vtxo.value} - fee ${fee} < dust`
17688
+ );
17689
+ }
17690
+ const tx = new Transaction({ version: 2 });
17691
+ tx.addInput({
17692
+ txid: vtxo.txid,
17693
+ index: vtxo.vout,
17694
+ tapLeafScript: [path.leaf],
17695
+ sequence: path.sequence,
17696
+ witnessUtxo: { amount: BigInt(vtxo.value), script: vtxo.pkScript },
17697
+ sighashType: SigHash.DEFAULT
17698
+ });
17699
+ tx.addOutputAddress(outputAddress, sendAmount, network);
17700
+ const signed = await identity.sign(tx);
17701
+ if (!path.extraWitness || path.extraWitness.length === 0) {
17702
+ signed.finalize();
17703
+ return { tx: signed, fee };
17704
+ }
17705
+ const input = signed.getInput(0);
17706
+ const tapScriptSig = input.tapScriptSig;
17707
+ if (!tapScriptSig || tapScriptSig.length === 0) {
17708
+ throw new Error("exit path requires additional signers");
17709
+ }
17710
+ const [controlBlock] = path.leaf;
17711
+ signed.updateInput(0, {
17712
+ finalScriptWitness: [
17713
+ tapScriptSig[0][1],
17714
+ ...path.extraWitness,
17715
+ scriptFromTapLeafScript(path.leaf),
17716
+ TaprootControlBlock.encode(controlBlock)
17717
+ ]
17718
+ });
17719
+ return { tx: signed, fee };
17720
+ }
17721
+
17722
+ // src/wallet/exit/estimate.ts
17723
+ var CHILD_OUTPUT_DUST = 546;
17724
+ function stepFundingAmount(stepFee) {
17725
+ return stepFee + CHILD_OUTPUT_DUST;
17726
+ }
17727
+ function resolveMode(opts) {
17728
+ return opts.mode ?? "funded";
17729
+ }
17730
+ function resolveNetworkName(opts) {
17731
+ if (opts.networkName) return opts.networkName;
17732
+ const { bech32 } = opts.wallet.network;
17733
+ if (bech32 === "bc") return "bitcoin";
17734
+ if (bech32 === "bcrt") return "regtest";
17735
+ return "testnet";
17736
+ }
17737
+ async function resolveFeeRate(opts) {
17738
+ if (opts.feeRate) return Math.ceil(opts.feeRate);
17739
+ const feeRate = await opts.wallet.onchainProvider.getFeeRate();
17740
+ if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) return OnchainWallet.MIN_FEE_RATE;
17741
+ return Math.ceil(feeRate);
17742
+ }
17743
+ async function selectExitVtxos(opts) {
17744
+ if (!opts.vtxos) return opts.wallet.getVtxos();
17745
+ const res = await opts.wallet.indexerProvider.getVtxos({ outpoints: opts.vtxos });
17746
+ const tapTrees = /* @__PURE__ */ new Map();
17747
+ const out = [];
17748
+ for (const vtxo of res.vtxos) {
17749
+ if (vtxo.isSpent) continue;
17750
+ let tapTree = tapTrees.get(vtxo.script);
17751
+ if (!tapTree) {
17752
+ const [contract] = await opts.wallet.contractRepository.getContracts({
17753
+ script: vtxo.script
17754
+ });
17755
+ if (!contract) {
17756
+ throw new Error(
17757
+ `no contract registered for vtxo script ${vtxo.script} \u2014 register the contract before exiting`
17758
+ );
16782
17759
  }
16783
- } else {
16784
- if (current.time >= confirmedAt.time + Number(exit.params.timelock.value)) {
16785
- return exit;
17760
+ const handler = contractHandlers.get(contract.type);
17761
+ if (!handler) {
17762
+ throw new Error(`no contract handler registered for type '${contract.type}'`);
16786
17763
  }
17764
+ tapTree = handler.createScript(contract.params).encode();
17765
+ tapTrees.set(vtxo.script, tapTree);
16787
17766
  }
17767
+ out.push({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value, tapTree });
16788
17768
  }
16789
- return void 0;
17769
+ return out;
17770
+ }
17771
+ async function computeExitLayout(opts, feeRate) {
17772
+ const { wallet, onchainWallet } = opts;
17773
+ const vtxos = await selectExitVtxos(opts);
17774
+ if (vtxos.length === 0) throw new Error("no vtxos to exit");
17775
+ const resolver = createExitChainResolver({
17776
+ indexer: wallet.indexerProvider,
17777
+ repository: wallet.virtualTxRepository,
17778
+ extraSources: wallet.exitDataCapture?.sources
17779
+ });
17780
+ const dag = await buildExitDag({
17781
+ vtxos,
17782
+ chain: resolver,
17783
+ onchain: wallet.onchainProvider
17784
+ });
17785
+ const pendingNodes = dag.filter((n) => !n.confirmed);
17786
+ const psbts = /* @__PURE__ */ new Map();
17787
+ if (pendingNodes.length > 0) {
17788
+ const txs = await resolver.getVirtualTxs(pendingNodes.map((n) => n.txid));
17789
+ for (const psbt of txs) {
17790
+ psbts.set(Transaction.fromPSBT(base64.decode(psbt)).id, psbt);
17791
+ }
17792
+ }
17793
+ const childVsize = Number(
17794
+ TxWeightEstimator.create().addP2AInput().addKeySpendInput(true).addOutputAddress(onchainWallet.address, wallet.network).vsize().value
17795
+ );
17796
+ const rawSteps = pendingNodes.map((node) => {
17797
+ const psbt = psbts.get(node.txid);
17798
+ if (!psbt) {
17799
+ throw new Error(`indexer did not return virtual tx ${node.txid}`);
17800
+ }
17801
+ const parent = finalizeVirtualTx(node.type, psbt);
17802
+ const stepFee = Math.ceil(feeRate * (parent.vsize + childVsize));
17803
+ return { node, parent, stepFee, funding: stepFundingAmount(stepFee) };
17804
+ });
17805
+ const parentInputTxids = (tx) => {
17806
+ const ids = [];
17807
+ for (let i = 0; i < tx.inputsLength; i++) {
17808
+ const txid = tx.getInput(i).txid;
17809
+ if (txid) ids.push(hex.encode(txid));
17810
+ }
17811
+ return ids;
17812
+ };
17813
+ const steps = topoSortByDeps(
17814
+ rawSteps,
17815
+ (s) => s.parent.id,
17816
+ (s) => parentInputTxids(s.parent)
17817
+ );
17818
+ const walletPubKeyHex = hex.encode(await wallet.identity.xOnlyPublicKey());
17819
+ const infos = [];
17820
+ const sweeps = [];
17821
+ for (const vtxo of vtxos) {
17822
+ const outpoint = `${vtxo.txid}:${vtxo.vout}`;
17823
+ try {
17824
+ const resolved = await resolveUnilateralPath({
17825
+ vtxo,
17826
+ scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
17827
+ contractRepository: wallet.contractRepository,
17828
+ walletPubKeyHex,
17829
+ currentTime: Date.now()
17830
+ });
17831
+ const sweepFee = sweepFeeFor(
17832
+ resolved.selection,
17833
+ opts.sweepAddress,
17834
+ wallet.network,
17835
+ feeRate
17836
+ );
17837
+ if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
17838
+ infos.push({
17839
+ outpoint,
17840
+ value: vtxo.value,
17841
+ skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
17842
+ });
17843
+ continue;
17844
+ }
17845
+ const t = sequenceToTimelock(resolved.selection.sequence);
17846
+ const delay = { type: t.type, value: Number(t.value) };
17847
+ infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
17848
+ sweeps.push({ vtxo, resolved, sweepFee, delay });
17849
+ } catch (e) {
17850
+ if (e instanceof ExitPathError) {
17851
+ infos.push({ outpoint, value: vtxo.value, skipped: e.message });
17852
+ continue;
17853
+ }
17854
+ throw e;
17855
+ }
17856
+ }
17857
+ const coins = (await onchainWallet.getCoins()).filter((c) => c.status.confirmed);
17858
+ const balance = coins.reduce((sum, c) => sum + c.value, 0);
17859
+ const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
17860
+ const splitterFeeFor = (inputCount) => {
17861
+ if (steps.length === 0) return 0;
17862
+ const est = TxWeightEstimator.create();
17863
+ for (let i = 0; i < inputCount; i++) est.addKeySpendInput(true);
17864
+ for (let i = 0; i < steps.length + 1; i++) {
17865
+ est.addOutputAddress(onchainWallet.address, wallet.network);
17866
+ }
17867
+ return Number(est.vsize().fee(BigInt(feeRate)));
17868
+ };
17869
+ const graph = resolveMode(opts) === "graph";
17870
+ let splitterFee = 0;
17871
+ if (!graph) {
17872
+ splitterFee = splitterFeeFor(Math.max(1, coins.length));
17873
+ if (steps.length > 0 && balance < fundingTotal + splitterFee) {
17874
+ splitterFee = splitterFeeFor(coins.length + 1);
17875
+ }
17876
+ }
17877
+ const stepFees = steps.reduce((s, x) => s + x.stepFee, 0);
17878
+ const sweepFees = sweeps.reduce((s, x) => s + x.sweepFee, 0);
17879
+ const totals = {
17880
+ // graph: each unroll step is parent + a live-built child (2 txs), no
17881
+ // splitter. funded: + 1 splitter tx and pre-signed children.
17882
+ txCount: (graph ? 0 : steps.length > 0 ? 1 : 0) + steps.length * 2 + sweeps.length,
17883
+ totalFeeSats: splitterFee + stepFees + sweepFees,
17884
+ // graph funding is what the executor sends to its own fee address:
17885
+ // just the CPFP fees (change recycles); funded also locks the
17886
+ // per-child dust into the splitter.
17887
+ fundingRequiredSats: graph ? stepFees : splitterFee + fundingTotal,
17888
+ recoveredSats: sweeps.reduce((s, x) => s + (x.vtxo.value - x.sweepFee), 0)
17889
+ };
17890
+ const expiries = pendingNodes.map((n) => n.expiresAt).filter((x) => x !== void 0);
17891
+ const validUntil = expiries.length > 0 ? Math.min(...expiries) : void 0;
17892
+ return {
17893
+ vtxos,
17894
+ dag,
17895
+ steps,
17896
+ sweeps,
17897
+ infos,
17898
+ totals,
17899
+ splitterFee,
17900
+ validUntil,
17901
+ balance,
17902
+ coins
17903
+ };
17904
+ }
17905
+ async function estimate(opts) {
17906
+ const feeRate = await resolveFeeRate(opts);
17907
+ const layout = await computeExitLayout(opts, feeRate);
17908
+ if (resolveMode(opts) === "graph") {
17909
+ return {
17910
+ feeRate,
17911
+ fundingAddress: "",
17912
+ currentBalanceSats: 0,
17913
+ shortfallSats: layout.totals.fundingRequiredSats,
17914
+ validUntil: layout.validUntil,
17915
+ totals: layout.totals,
17916
+ vtxos: layout.infos
17917
+ };
17918
+ }
17919
+ return {
17920
+ feeRate,
17921
+ fundingAddress: opts.onchainWallet.address,
17922
+ currentBalanceSats: layout.balance,
17923
+ shortfallSats: Math.max(0, layout.totals.fundingRequiredSats - layout.balance),
17924
+ validUntil: layout.validUntil,
17925
+ totals: layout.totals,
17926
+ vtxos: layout.infos
17927
+ };
17928
+ }
17929
+
17930
+ // src/wallet/exit/executor.ts
17931
+ var Executor = class {
17932
+ constructor(pkg, provider, opts) {
17933
+ this.pkg = pkg;
17934
+ this.provider = provider;
17935
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 5e3;
17936
+ this.feeWallet = opts?.feeWallet;
17937
+ }
17938
+ pollIntervalMs;
17939
+ feeWallet;
17940
+ sleep() {
17941
+ return new Promise((r) => setTimeout(r, this.pollIntervalMs));
17942
+ }
17943
+ async status(txid) {
17944
+ try {
17945
+ return await this.provider.getTxStatus(txid);
17946
+ } catch {
17947
+ return void 0;
17948
+ }
17949
+ }
17950
+ async waitConfirmed(txid) {
17951
+ for (; ; ) {
17952
+ const s = await this.status(txid);
17953
+ if (s?.confirmed) return s;
17954
+ await this.sleep();
17955
+ }
17956
+ }
17957
+ async *[Symbol.asyncIterator]() {
17958
+ const dead = /* @__PURE__ */ new Set();
17959
+ if (this.pkg.validUntil && Date.now() / 1e3 > this.pkg.validUntil) {
17960
+ yield {
17961
+ stepIndex: -1,
17962
+ kind: "broadcast",
17963
+ status: "warning",
17964
+ reason: `package validUntil ${this.pkg.validUntil} has passed; the operator may have swept \u2014 attempting anyway`
17965
+ };
17966
+ }
17967
+ const sweeps = [];
17968
+ for (let i = 0; i < this.pkg.steps.length; i++) {
17969
+ const step = this.pkg.steps[i];
17970
+ if (step.kind === "sweep") {
17971
+ sweeps.push({ index: i, step });
17972
+ continue;
17973
+ }
17974
+ const forVtxos = step.kind === "package" || step.kind === "bump" ? step.forVtxos : void 0;
17975
+ if (forVtxos && forVtxos.every((v) => dead.has(v))) {
17976
+ yield {
17977
+ stepIndex: i,
17978
+ kind: step.kind,
17979
+ status: "skipped",
17980
+ forVtxos,
17981
+ reason: "branch failed earlier"
17982
+ };
17983
+ continue;
17984
+ }
17985
+ const anchorTxid = step.kind === "package" || step.kind === "bump" ? step.parentTxid : step.txid;
17986
+ const existing = await this.status(anchorTxid);
17987
+ if (existing?.confirmed) {
17988
+ yield {
17989
+ stepIndex: i,
17990
+ kind: step.kind,
17991
+ status: "skipped",
17992
+ txid: anchorTxid,
17993
+ forVtxos
17994
+ };
17995
+ continue;
17996
+ }
17997
+ if (!existing) {
17998
+ try {
17999
+ if (step.kind === "package") {
18000
+ await this.provider.broadcastTransaction(step.parentHex, step.childHex);
18001
+ } else if (step.kind === "bump") {
18002
+ if (!this.feeWallet) {
18003
+ throw new Error(
18004
+ "graph package requires a fee wallet (opts.feeWallet) to fund CPFP bumps"
18005
+ );
18006
+ }
18007
+ const [parentHex, childHex] = await this.feeWallet.bumpAnchor(
18008
+ step.parentHex,
18009
+ this.pkg.feeRate
18010
+ );
18011
+ await this.provider.broadcastTransaction(parentHex, childHex);
18012
+ } else {
18013
+ await this.provider.broadcastTransaction(step.hex);
18014
+ }
18015
+ yield {
18016
+ stepIndex: i,
18017
+ kind: step.kind,
18018
+ status: "broadcast",
18019
+ txid: anchorTxid,
18020
+ forVtxos
18021
+ };
18022
+ } catch (e) {
18023
+ const reason = e instanceof Error ? e.message : String(e);
18024
+ if (step.kind === "broadcast") {
18025
+ for (const s of this.pkg.steps) {
18026
+ if (s.kind === "package") s.forVtxos.forEach((v) => dead.add(v));
18027
+ }
18028
+ } else if (step.kind === "package" || step.kind === "bump") {
18029
+ step.forVtxos.forEach((v) => dead.add(v));
18030
+ }
18031
+ yield {
18032
+ stepIndex: i,
18033
+ kind: step.kind,
18034
+ status: "failed",
18035
+ txid: anchorTxid,
18036
+ forVtxos,
18037
+ reason
18038
+ };
18039
+ continue;
18040
+ }
18041
+ }
18042
+ await this.waitConfirmed(anchorTxid);
18043
+ yield {
18044
+ stepIndex: i,
18045
+ kind: step.kind,
18046
+ status: "confirmed",
18047
+ txid: anchorTxid,
18048
+ forVtxos
18049
+ };
18050
+ }
18051
+ for (const { index, step } of sweeps) {
18052
+ if (dead.has(step.vtxo)) {
18053
+ yield {
18054
+ stepIndex: index,
18055
+ kind: "sweep",
18056
+ status: "failed",
18057
+ txid: step.txid,
18058
+ forVtxos: [step.vtxo],
18059
+ reason: "branch failed earlier"
18060
+ };
18061
+ }
18062
+ }
18063
+ const pending = sweeps.filter(({ step }) => !dead.has(step.vtxo));
18064
+ const waitingAnnounced = /* @__PURE__ */ new Set();
18065
+ const done = /* @__PURE__ */ new Set();
18066
+ while (done.size < pending.length) {
18067
+ for (const { index, step } of pending) {
18068
+ if (done.has(index)) continue;
18069
+ const swept = await this.status(step.txid);
18070
+ if (swept?.confirmed) {
18071
+ done.add(index);
18072
+ yield {
18073
+ stepIndex: index,
18074
+ kind: "sweep",
18075
+ status: "confirmed",
18076
+ txid: step.txid,
18077
+ forVtxos: [step.vtxo]
18078
+ };
18079
+ continue;
18080
+ }
18081
+ if (swept) continue;
18082
+ const dep = await this.status(step.dependsOnTxid);
18083
+ if (!dep?.confirmed) continue;
18084
+ const tip = await this.provider.getChainTip();
18085
+ const mature = step.delay.type === "blocks" ? tip.height >= (dep.blockHeight ?? 0) + step.delay.value : tip.time >= (dep.blockTime ?? 0) + step.delay.value;
18086
+ if (!mature) {
18087
+ if (!waitingAnnounced.has(index)) {
18088
+ waitingAnnounced.add(index);
18089
+ yield {
18090
+ stepIndex: index,
18091
+ kind: "sweep",
18092
+ status: "waiting_csv",
18093
+ txid: step.txid,
18094
+ forVtxos: [step.vtxo],
18095
+ ...step.delay.type === "blocks" ? { maturesAtHeight: (dep.blockHeight ?? 0) + step.delay.value } : { maturesAtTime: (dep.blockTime ?? 0) + step.delay.value }
18096
+ };
18097
+ }
18098
+ continue;
18099
+ }
18100
+ try {
18101
+ await this.provider.broadcastTransaction(step.hex);
18102
+ yield {
18103
+ stepIndex: index,
18104
+ kind: "sweep",
18105
+ status: "broadcast",
18106
+ txid: step.txid,
18107
+ forVtxos: [step.vtxo]
18108
+ };
18109
+ } catch (e) {
18110
+ const reason = e instanceof Error ? e.message : String(e);
18111
+ if (/non-?bip68|non-?final|premature/i.test(reason)) {
18112
+ continue;
18113
+ }
18114
+ done.add(index);
18115
+ yield {
18116
+ stepIndex: index,
18117
+ kind: "sweep",
18118
+ status: "failed",
18119
+ txid: step.txid,
18120
+ forVtxos: [step.vtxo],
18121
+ reason
18122
+ };
18123
+ }
18124
+ }
18125
+ if (done.size < pending.length) await this.sleep();
18126
+ }
18127
+ }
18128
+ };
18129
+ async function prepare(opts) {
18130
+ const { wallet, onchainWallet } = opts;
18131
+ const feeRate = await resolveFeeRate(opts);
18132
+ const mode = resolveMode(opts);
18133
+ if (mode === "funded") {
18134
+ const walletKey = await wallet.identity.xOnlyPublicKey();
18135
+ if (hex.encode(onchainWallet.onchainP2TR.tapInternalKey) !== hex.encode(walletKey)) {
18136
+ throw new Error("onchainWallet must share the wallet identity");
18137
+ }
18138
+ }
18139
+ const layout = await computeExitLayout(opts, feeRate);
18140
+ const sweepSteps = [];
18141
+ const activeOutpoints = /* @__PURE__ */ new Set();
18142
+ for (const sweep of layout.sweeps) {
18143
+ const outpoint = `${sweep.vtxo.txid}:${sweep.vtxo.vout}`;
18144
+ try {
18145
+ const { tx } = await buildSignedSweep({
18146
+ vtxo: {
18147
+ txid: sweep.vtxo.txid,
18148
+ vout: sweep.vtxo.vout,
18149
+ value: sweep.vtxo.value,
18150
+ pkScript: VtxoScript.decode(sweep.vtxo.tapTree).pkScript
18151
+ },
18152
+ path: sweep.resolved.selection,
18153
+ outputAddress: opts.sweepAddress,
18154
+ feeRate,
18155
+ network: wallet.network,
18156
+ identity: wallet.identity
18157
+ });
18158
+ activeOutpoints.add(outpoint);
18159
+ sweepSteps.push({
18160
+ kind: "sweep",
18161
+ vtxo: outpoint,
18162
+ txid: tx.id,
18163
+ hex: tx.hex,
18164
+ dependsOnTxid: sweep.vtxo.txid,
18165
+ delay: sweep.delay
18166
+ });
18167
+ } catch (e) {
18168
+ const info = layout.infos.find((i) => i.outpoint === outpoint);
18169
+ if (info) {
18170
+ info.skipped = e instanceof Error ? e.message : String(e);
18171
+ delete info.sweepFee;
18172
+ delete info.path;
18173
+ delete info.delay;
18174
+ }
18175
+ }
18176
+ }
18177
+ const steps = layout.steps.filter((s) => s.node.forVtxos.some((v) => activeOutpoints.has(v)));
18178
+ if (steps.length === 0 && sweepSteps.length === 0) {
18179
+ throw new Error("no exitable vtxos (all skipped)");
18180
+ }
18181
+ if (mode === "graph") {
18182
+ const bumpSteps = steps.map((step) => ({
18183
+ kind: "bump",
18184
+ parentTxid: step.parent.id,
18185
+ parentHex: step.parent.hex,
18186
+ forVtxos: step.node.forVtxos.filter((v) => activeOutpoints.has(v))
18187
+ }));
18188
+ const stepFees2 = steps.reduce((s, x) => s + x.stepFee, 0);
18189
+ const activeInfos2 = layout.infos.filter((i) => !i.skipped);
18190
+ const sweepFees2 = activeInfos2.reduce((s, i) => s + (i.sweepFee ?? 0), 0);
18191
+ const recovered2 = activeInfos2.reduce((s, i) => s + (i.value ?? 0) - (i.sweepFee ?? 0), 0);
18192
+ return {
18193
+ version: 1,
18194
+ mode: "graph",
18195
+ network: resolveNetworkName(opts),
18196
+ createdAt: Math.floor(Date.now() / 1e3),
18197
+ validUntil: layout.validUntil,
18198
+ feeRate,
18199
+ sweepAddress: opts.sweepAddress,
18200
+ totals: {
18201
+ txCount: bumpSteps.length * 2 + sweepSteps.length,
18202
+ totalFeeSats: stepFees2 + sweepFees2,
18203
+ fundingRequiredSats: stepFees2,
18204
+ recoveredSats: recovered2
18205
+ },
18206
+ vtxos: layout.infos,
18207
+ steps: [...bumpSteps, ...sweepSteps]
18208
+ };
18209
+ }
18210
+ const packageSteps = [];
18211
+ let broadcastStep;
18212
+ let splitterFee = 0;
18213
+ if (steps.length > 0) {
18214
+ const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
18215
+ let selected = { inputs: [], changeAmount: 0n };
18216
+ for (let i = 0; i < 10; i++) {
18217
+ const target = fundingTotal + splitterFee;
18218
+ try {
18219
+ selected = selectCoins(layout.coins, target);
18220
+ } catch {
18221
+ throw new Error(
18222
+ `insufficient confirmed onchain funds: need ${target} sats, have ${layout.balance} (deposit the shortfall to ${onchainWallet.address})`
18223
+ );
18224
+ }
18225
+ const est = TxWeightEstimator.create();
18226
+ for (const _ of selected.inputs) est.addKeySpendInput(true);
18227
+ for (const _ of steps) est.addOutputAddress(onchainWallet.address, wallet.network);
18228
+ if (selected.changeAmount >= BigInt(DUST_AMOUNT)) {
18229
+ est.addOutputAddress(onchainWallet.address, wallet.network);
18230
+ }
18231
+ const newFee = Number(est.vsize().fee(BigInt(feeRate)));
18232
+ if (newFee <= splitterFee) break;
18233
+ splitterFee = newFee;
18234
+ }
18235
+ const splitter = new Transaction({ version: 2 });
18236
+ for (const coin of selected.inputs) {
18237
+ splitter.addInput({
18238
+ txid: coin.txid,
18239
+ index: coin.vout,
18240
+ witnessUtxo: {
18241
+ script: onchainWallet.onchainP2TR.script,
18242
+ amount: BigInt(coin.value)
18243
+ },
18244
+ tapInternalKey: onchainWallet.onchainP2TR.tapInternalKey
18245
+ });
18246
+ }
18247
+ for (const step of steps) {
18248
+ splitter.addOutputAddress(onchainWallet.address, BigInt(step.funding), wallet.network);
18249
+ }
18250
+ const inputSum = selected.inputs.reduce((s, c) => s + c.value, 0);
18251
+ const change = BigInt(inputSum - fundingTotal - splitterFee);
18252
+ if (change < 0n) {
18253
+ throw new Error(
18254
+ `insufficient confirmed onchain funds for the splitter fee: need ${fundingTotal + splitterFee} sats, have ${inputSum} (deposit the shortfall to ${onchainWallet.address})`
18255
+ );
18256
+ }
18257
+ if (change >= BigInt(DUST_AMOUNT)) {
18258
+ splitter.addOutputAddress(onchainWallet.address, change, wallet.network);
18259
+ } else if (change > 0n) {
18260
+ splitterFee += Number(change);
18261
+ }
18262
+ const signedSplitter = await wallet.identity.sign(splitter);
18263
+ signedSplitter.finalize();
18264
+ for (let k = 0; k < steps.length; k++) {
18265
+ const step = steps[k];
18266
+ const { child } = buildAnchorChild({
18267
+ parent: step.parent,
18268
+ feeRate,
18269
+ fundingCoins: [{ txid: signedSplitter.id, vout: k, value: step.funding }],
18270
+ changeAddress: onchainWallet.address,
18271
+ changeScript: onchainWallet.onchainP2TR.script,
18272
+ tapInternalKey: onchainWallet.onchainP2TR.tapInternalKey,
18273
+ network: wallet.network
18274
+ });
18275
+ const signedChild = await wallet.identity.sign(child);
18276
+ for (let i = 1; i < signedChild.inputsLength; i++) {
18277
+ signedChild.finalizeIdx(i);
18278
+ }
18279
+ packageSteps.push({
18280
+ kind: "package",
18281
+ parentTxid: step.parent.id,
18282
+ parentHex: step.parent.hex,
18283
+ childTxid: signedChild.id,
18284
+ childHex: signedChild.hex,
18285
+ forVtxos: step.node.forVtxos.filter((v) => activeOutpoints.has(v))
18286
+ });
18287
+ }
18288
+ broadcastStep = { kind: "broadcast", txid: signedSplitter.id, hex: signedSplitter.hex };
18289
+ await onchainWallet.provider.broadcastTransaction(signedSplitter.hex);
18290
+ }
18291
+ const stepFees = steps.reduce((s, x) => s + x.stepFee, 0);
18292
+ const activeInfos = layout.infos.filter((i) => !i.skipped);
18293
+ const sweepFees = activeInfos.reduce((s, i) => s + (i.sweepFee ?? 0), 0);
18294
+ const recovered = activeInfos.reduce((s, i) => s + (i.value ?? 0) - (i.sweepFee ?? 0), 0);
18295
+ const fundingRequired = splitterFee + steps.reduce((s, x) => s + x.funding, 0);
18296
+ return {
18297
+ version: 1,
18298
+ mode: "funded",
18299
+ network: resolveNetworkName(opts),
18300
+ createdAt: Math.floor(Date.now() / 1e3),
18301
+ validUntil: layout.validUntil,
18302
+ feeRate,
18303
+ sweepAddress: opts.sweepAddress,
18304
+ totals: {
18305
+ txCount: (broadcastStep ? 1 : 0) + packageSteps.length * 2 + sweepSteps.length,
18306
+ totalFeeSats: splitterFee + stepFees + sweepFees,
18307
+ fundingRequiredSats: fundingRequired,
18308
+ recoveredSats: recovered
18309
+ },
18310
+ vtxos: layout.infos,
18311
+ steps: [...broadcastStep ? [broadcastStep] : [], ...packageSteps, ...sweepSteps]
18312
+ };
18313
+ }
18314
+
18315
+ // src/wallet/exit/types.ts
18316
+ function serializeExitPackage(pkg) {
18317
+ return JSON.stringify(pkg);
18318
+ }
18319
+ function deserializeExitPackage(json) {
18320
+ const data = JSON.parse(json);
18321
+ if (data?.version !== 1) {
18322
+ throw new Error(`unsupported exit package version: ${data?.version}`);
18323
+ }
18324
+ if (typeof data.network !== "string" || typeof data.sweepAddress !== "string") {
18325
+ throw new Error("invalid exit package: missing network or sweepAddress");
18326
+ }
18327
+ if (typeof data.feeRate !== "number" || typeof data.createdAt !== "number") {
18328
+ throw new Error("invalid exit package: missing feeRate or createdAt");
18329
+ }
18330
+ if (!Array.isArray(data.steps) || !Array.isArray(data.vtxos)) {
18331
+ throw new Error("invalid exit package: steps and vtxos must be arrays");
18332
+ }
18333
+ if (!isValidTotals(data.totals)) {
18334
+ throw new Error("invalid exit package: malformed totals");
18335
+ }
18336
+ for (const step of data.steps) {
18337
+ if (!isValidStep(step)) {
18338
+ throw new Error(`invalid step: ${JSON.stringify(step)}`);
18339
+ }
18340
+ }
18341
+ return data;
16790
18342
  }
18343
+ function isValidDelay(d) {
18344
+ const delay = d;
18345
+ return !!delay && (delay.type === "blocks" || delay.type === "seconds") && typeof delay.value === "number";
18346
+ }
18347
+ function isValidTotals(t) {
18348
+ const totals = t;
18349
+ return !!totals && typeof totals === "object" && typeof totals.txCount === "number" && typeof totals.totalFeeSats === "number" && typeof totals.fundingRequiredSats === "number" && typeof totals.recoveredSats === "number";
18350
+ }
18351
+ function isValidStep(step) {
18352
+ const s = step;
18353
+ if (!s || typeof s !== "object") return false;
18354
+ switch (s.kind) {
18355
+ case "broadcast":
18356
+ return typeof s.txid === "string" && typeof s.hex === "string";
18357
+ case "package":
18358
+ return typeof s.parentTxid === "string" && typeof s.parentHex === "string" && typeof s.childTxid === "string" && typeof s.childHex === "string" && Array.isArray(s.forVtxos);
18359
+ case "bump":
18360
+ return typeof s.parentTxid === "string" && typeof s.parentHex === "string" && Array.isArray(s.forVtxos);
18361
+ case "sweep":
18362
+ return typeof s.vtxo === "string" && typeof s.txid === "string" && typeof s.hex === "string" && typeof s.dependsOnTxid === "string" && isValidDelay(s.delay);
18363
+ default:
18364
+ return false;
18365
+ }
18366
+ }
18367
+
18368
+ // src/wallet/exit/index.ts
18369
+ var UnilateralExit = {
18370
+ estimate,
18371
+ prepare,
18372
+ Executor
18373
+ };
16791
18374
  var ARKCONTRACT_PREFIX = "arkcontract";
16792
18375
  function encodeArkContract(contract) {
16793
18376
  const params = new URLSearchParams();
@@ -16847,6 +18430,6 @@ function isArkContract(str) {
16847
18430
  return str.startsWith(ARKCONTRACT_PREFIX + "=");
16848
18431
  }
16849
18432
 
16850
- export { ActivityRegistry, ArkNote, AssetManager, BIP322, Batch, ChainedTxType, 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, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusInitializingError, 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, assertIntentIdUnique, boardingResolver, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, createDefaultActivityRegistry, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, intentMatchesFilter, intentPageBounds, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, mergeChainedTxType, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
16851
- //# sourceMappingURL=chunk-B4R36PRN.js.map
16852
- //# sourceMappingURL=chunk-B4R36PRN.js.map
18433
+ export { ActivityRegistry, ArkNote, AssetManager, BIP322, Batch, ChainedTxType, 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, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnilateralExit, Unroll, VtxoManager, Wallet2 as Wallet, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, assertIntentIdUnique, boardingResolver, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, createDefaultActivityRegistry, createExitChainResolver, decodeArkContract, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, intentMatchesFilter, intentPageBounds, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isRetryableProviderError, isSpendable, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, mergeChainedTxType, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
18434
+ //# sourceMappingURL=chunk-TGRJ5WAX.js.map
18435
+ //# sourceMappingURL=chunk-TGRJ5WAX.js.map