@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.
- package/README.md +100 -0
- package/dist/adapters/expo.cjs +4 -4
- package/dist/adapters/expo.d.cts +2 -2
- package/dist/adapters/expo.d.ts +2 -2
- package/dist/adapters/expo.js +2 -2
- package/dist/adapters/indexedDB.cjs +3 -3
- package/dist/adapters/indexedDB.js +2 -2
- package/dist/{ark-I8jfe5VE.d.cts → ark-C-PjOngm.d.cts} +89 -1
- package/dist/{ark-I8jfe5VE.d.ts → ark-C-PjOngm.d.ts} +89 -1
- package/dist/{asyncStorageTaskQueue-D1FfHfgK.d.cts → asyncStorageTaskQueue-BmBPvTyh.d.cts} +1 -1
- package/dist/{asyncStorageTaskQueue-CjoL-35w.d.ts → asyncStorageTaskQueue-CHpI3moJ.d.ts} +1 -1
- package/dist/{chunk-VKKLOT2E.js → chunk-BOA4SHCD.js} +3 -3
- package/dist/{chunk-VKKLOT2E.js.map → chunk-BOA4SHCD.js.map} +1 -1
- package/dist/{chunk-PLJZRIFZ.cjs → chunk-HZ7VIN7P.cjs} +11 -11
- package/dist/{chunk-PLJZRIFZ.cjs.map → chunk-HZ7VIN7P.cjs.map} +1 -1
- package/dist/{chunk-JIWCE2IK.cjs → chunk-OFQGANH2.cjs} +7 -7
- package/dist/{chunk-JIWCE2IK.cjs.map → chunk-OFQGANH2.cjs.map} +1 -1
- package/dist/{chunk-US274KZO.js → chunk-QZ6HZZZK.js} +3 -3
- package/dist/{chunk-US274KZO.js.map → chunk-QZ6HZZZK.js.map} +1 -1
- package/dist/{chunk-QGNTDGMN.cjs → chunk-SRCWBFSZ.cjs} +1945 -357
- package/dist/chunk-SRCWBFSZ.cjs.map +1 -0
- package/dist/{chunk-B4R36PRN.js → chunk-TGRJ5WAX.js} +1868 -285
- package/dist/chunk-TGRJ5WAX.js.map +1 -0
- package/dist/{chunk-3AQNLQPM.cjs → chunk-UHOHLN3C.cjs} +119 -69
- package/dist/chunk-UHOHLN3C.cjs.map +1 -0
- package/dist/{chunk-763XHDAJ.js → chunk-YBQV7XBC.js} +119 -70
- package/dist/chunk-YBQV7XBC.js.map +1 -0
- package/dist/contracts/handlers/index.d.cts +3 -3
- package/dist/contracts/handlers/index.d.ts +3 -3
- package/dist/{delegate-CgjVjYOP.d.ts → delegate-BKNqva1j.d.ts} +1 -1
- package/dist/{delegate-BRqbh2fW.d.cts → delegate-DOEobYwH.d.cts} +1 -1
- package/dist/{index-DHuXlDEg.d.cts → index-BlW4I30l.d.cts} +2 -2
- package/dist/{index-DvWNB_rl.d.ts → index-Cm2fGeox.d.ts} +2 -2
- package/dist/index.cjs +150 -126
- package/dist/index.d.cts +275 -19
- package/dist/index.d.ts +275 -19
- package/dist/index.js +2 -2
- package/dist/repositories/realm/index.cjs +17 -17
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/realm/index.js +3 -3
- package/dist/repositories/sqlite/index.cjs +16 -16
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.js +3 -3
- package/dist/{taskRunner-Djlhfgi1.d.ts → taskRunner-BngWSFMi.d.ts} +1 -1
- package/dist/{taskRunner-BSnslNmq.d.cts → taskRunner-DOiyRuJa.d.cts} +1 -1
- package/dist/wallet/expo/background.cjs +12 -12
- package/dist/wallet/expo/background.d.cts +3 -3
- package/dist/wallet/expo/background.d.ts +3 -3
- package/dist/wallet/expo/background.js +4 -4
- package/dist/wallet/expo/index.cjs +21 -12
- package/dist/wallet/expo/index.cjs.map +1 -1
- package/dist/wallet/expo/index.d.cts +11 -4
- package/dist/wallet/expo/index.d.ts +11 -4
- package/dist/wallet/expo/index.js +12 -3
- package/dist/wallet/expo/index.js.map +1 -1
- package/dist/{wallet-BsqRU8KM.d.cts → wallet-B3awP1Nj.d.ts} +105 -3
- package/dist/{wallet-D9z7wssl.d.ts → wallet-DBjumS42.d.cts} +105 -3
- package/dist/worker/expo/index.cjs +7 -7
- package/dist/worker/expo/index.d.cts +4 -4
- package/dist/worker/expo/index.d.ts +4 -4
- package/dist/worker/expo/index.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-3AQNLQPM.cjs.map +0 -1
- package/dist/chunk-763XHDAJ.js.map +0 -1
- package/dist/chunk-B4R36PRN.js.map +0 -1
- 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-
|
|
2
|
-
import { isMainnetDescriptor, descriptorIsOurs,
|
|
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
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
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
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
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
|
-
|
|
4267
|
-
|
|
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/
|
|
7800
|
-
|
|
7801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8390
|
-
|
|
8391
|
-
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
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
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
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
|
-
|
|
16784
|
-
if (
|
|
16785
|
-
|
|
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
|
|
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-
|
|
16852
|
-
//# sourceMappingURL=chunk-
|
|
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
|