@arkade-os/sdk 0.4.43 → 0.4.44
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 +65 -0
- package/dist/adapters/expo.cjs +4 -4
- package/dist/adapters/expo.js +2 -2
- package/dist/adapters/indexedDB.cjs +3 -3
- package/dist/adapters/indexedDB.js +2 -2
- package/dist/{asyncStorageTaskQueue-CjoL-35w.d.ts → asyncStorageTaskQueue-BtSOVZFn.d.ts} +2 -2
- package/dist/{asyncStorageTaskQueue-D1FfHfgK.d.cts → asyncStorageTaskQueue-Dap7qUlF.d.cts} +2 -2
- package/dist/{chunk-B4R36PRN.js → chunk-FMI2GKYO.js} +1088 -181
- package/dist/chunk-FMI2GKYO.js.map +1 -0
- package/dist/{chunk-US274KZO.js → chunk-LCQQPOIT.js} +3 -3
- package/dist/{chunk-US274KZO.js.map → chunk-LCQQPOIT.js.map} +1 -1
- package/dist/{chunk-763XHDAJ.js → chunk-LJ35HPJW.js} +3 -3
- package/dist/{chunk-763XHDAJ.js.map → chunk-LJ35HPJW.js.map} +1 -1
- package/dist/{chunk-JIWCE2IK.cjs → chunk-RJ6XVCX4.cjs} +7 -7
- package/dist/{chunk-JIWCE2IK.cjs.map → chunk-RJ6XVCX4.cjs.map} +1 -1
- package/dist/{chunk-3AQNLQPM.cjs → chunk-UBYY2TPK.cjs} +3 -3
- package/dist/{chunk-3AQNLQPM.cjs.map → chunk-UBYY2TPK.cjs.map} +1 -1
- package/dist/{chunk-VKKLOT2E.js → chunk-V7WIZR2B.js} +3 -3
- package/dist/{chunk-VKKLOT2E.js.map → chunk-V7WIZR2B.js.map} +1 -1
- package/dist/{chunk-PLJZRIFZ.cjs → chunk-X37EKDIH.cjs} +11 -11
- package/dist/{chunk-PLJZRIFZ.cjs.map → chunk-X37EKDIH.cjs.map} +1 -1
- package/dist/{chunk-QGNTDGMN.cjs → chunk-XIVCG6EP.cjs} +1163 -253
- package/dist/chunk-XIVCG6EP.cjs.map +1 -0
- package/dist/index.cjs +138 -126
- package/dist/index.d.cts +232 -1
- package/dist/index.d.ts +232 -1
- package/dist/index.js +2 -2
- package/dist/repositories/realm/index.cjs +17 -17
- package/dist/repositories/realm/index.js +3 -3
- package/dist/repositories/sqlite/index.cjs +16 -16
- package/dist/repositories/sqlite/index.js +3 -3
- package/dist/{taskRunner-Djlhfgi1.d.ts → taskRunner-Bptw9UMa.d.ts} +1 -1
- package/dist/{taskRunner-BSnslNmq.d.cts → taskRunner-CeFxKhSH.d.cts} +1 -1
- package/dist/wallet/expo/background.cjs +12 -12
- package/dist/wallet/expo/background.d.cts +2 -2
- package/dist/wallet/expo/background.d.ts +2 -2
- package/dist/wallet/expo/background.js +4 -4
- package/dist/wallet/expo/index.cjs +12 -12
- package/dist/wallet/expo/index.d.cts +1 -1
- package/dist/wallet/expo/index.d.ts +1 -1
- package/dist/wallet/expo/index.js +3 -3
- package/dist/worker/expo/index.cjs +7 -7
- package/dist/worker/expo/index.d.cts +3 -3
- package/dist/worker/expo/index.d.ts +3 -3
- package/dist/worker/expo/index.js +3 -3
- package/package.json +1 -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, AssetRef, AssetId, AssetOutput, AssetGroup, AssetInput, Packet, Metadata, isEventSourceError, RestArkProvider, RestIndexerProvider, ArkError, BufferReader } from './chunk-LJ35HPJW.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";
|
|
@@ -15584,42 +15617,11 @@ var OnchainWallet = class _OnchainWallet {
|
|
|
15584
15617
|
* @see send
|
|
15585
15618
|
*/
|
|
15586
15619
|
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
15620
|
let feeRate = await this.provider.getFeeRate();
|
|
15596
15621
|
if (!feeRate || feeRate < _OnchainWallet.MIN_FEE_RATE) {
|
|
15597
15622
|
feeRate = _OnchainWallet.MIN_FEE_RATE;
|
|
15598
15623
|
}
|
|
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
|
-
}
|
|
15624
|
+
const child = await this.buildBumpPackage(parent, feeRate, await this.getCoins());
|
|
15623
15625
|
try {
|
|
15624
15626
|
await this.provider.broadcastTransaction(parent.hex, child.hex);
|
|
15625
15627
|
} catch (error) {
|
|
@@ -15628,6 +15630,59 @@ var OnchainWallet = class _OnchainWallet {
|
|
|
15628
15630
|
return [parent.hex, child.hex];
|
|
15629
15631
|
}
|
|
15630
15632
|
}
|
|
15633
|
+
/**
|
|
15634
|
+
* Build and sign a CPFP fee child for a parent tx (given as raw hex)
|
|
15635
|
+
* carrying a P2A anchor, funding it from this wallet's **confirmed**
|
|
15636
|
+
* coins, and return the 1P1C package hexes WITHOUT broadcasting.
|
|
15637
|
+
*
|
|
15638
|
+
* This is the graph-mode fee source ({@link ExitFeeWallet}): the exit
|
|
15639
|
+
* executor calls it to bump each transported virtual tx at execution
|
|
15640
|
+
* time, so funding can be deferred rather than pre-signed.
|
|
15641
|
+
*
|
|
15642
|
+
* @param parentHex - Finalized parent transaction, raw network hex
|
|
15643
|
+
* @param feeRate - sat/vB floor for the package (raised to MIN_FEE_RATE)
|
|
15644
|
+
* @returns Tuple of parent hex (unchanged) and signed child hex
|
|
15645
|
+
* @throws If the parent has no anchor, or funding cannot be selected/signed
|
|
15646
|
+
*/
|
|
15647
|
+
async bumpAnchor(parentHex, feeRate) {
|
|
15648
|
+
const parent = Transaction.fromRaw(hex.decode(parentHex));
|
|
15649
|
+
const coins = (await this.getCoins()).filter((c) => c.status.confirmed);
|
|
15650
|
+
const child = await this.buildBumpPackage(parent, feeRate, coins);
|
|
15651
|
+
return [parent.hex, child.hex];
|
|
15652
|
+
}
|
|
15653
|
+
/**
|
|
15654
|
+
* Shared core of {@link bumpP2A} and {@link bumpAnchor}: probe the package
|
|
15655
|
+
* fee with a single-input child, select coins for it, then build and sign
|
|
15656
|
+
* with the actual selection (the fee grows per extra input).
|
|
15657
|
+
*/
|
|
15658
|
+
async buildBumpPackage(parent, feeRate, coins) {
|
|
15659
|
+
let rate = feeRate;
|
|
15660
|
+
if (!rate || rate < _OnchainWallet.MIN_FEE_RATE) {
|
|
15661
|
+
rate = _OnchainWallet.MIN_FEE_RATE;
|
|
15662
|
+
}
|
|
15663
|
+
const probeVsize = TxWeightEstimator.create().addKeySpendInput(true).addP2AInput().addOutputAddress(this.address, this.network).vsize().value;
|
|
15664
|
+
const probeFee = Math.ceil(rate * (parent.vsize + Number(probeVsize)));
|
|
15665
|
+
if (!probeFee) {
|
|
15666
|
+
throw new Error(
|
|
15667
|
+
`invalid fee, got ${probeFee} with vsize ${parent.vsize + Number(probeVsize)}, feeRate ${rate}`
|
|
15668
|
+
);
|
|
15669
|
+
}
|
|
15670
|
+
const selected = selectCoins(coins, probeFee, true);
|
|
15671
|
+
const { child } = buildAnchorChild({
|
|
15672
|
+
parent,
|
|
15673
|
+
feeRate: rate,
|
|
15674
|
+
fundingCoins: selected.inputs,
|
|
15675
|
+
changeAddress: this.address,
|
|
15676
|
+
changeScript: this.onchainP2TR.script,
|
|
15677
|
+
tapInternalKey: this.onchainP2TR.tapInternalKey,
|
|
15678
|
+
network: this.network
|
|
15679
|
+
});
|
|
15680
|
+
const signed = await this.identity.sign(child);
|
|
15681
|
+
for (let i = 1; i < signed.inputsLength; i++) {
|
|
15682
|
+
signed.finalizeIdx(i);
|
|
15683
|
+
}
|
|
15684
|
+
return signed;
|
|
15685
|
+
}
|
|
15631
15686
|
};
|
|
15632
15687
|
function selectCoins(coins, targetAmount, forceChange = false) {
|
|
15633
15688
|
if (isNaN(targetAmount)) {
|
|
@@ -16518,6 +16573,84 @@ function craftBIP322ToSignSimple(toSpend, pkScript) {
|
|
|
16518
16573
|
});
|
|
16519
16574
|
return tx;
|
|
16520
16575
|
}
|
|
16576
|
+
function finalizeVirtualTx(type, psbtBase64) {
|
|
16577
|
+
const tx = Transaction.fromPSBT(base64.decode(psbtBase64));
|
|
16578
|
+
if (type === "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */) {
|
|
16579
|
+
const input = tx.getInput(0);
|
|
16580
|
+
if (!input) {
|
|
16581
|
+
throw new Error("Input not found");
|
|
16582
|
+
}
|
|
16583
|
+
const tapKeySig = input.tapKeySig;
|
|
16584
|
+
if (!tapKeySig) {
|
|
16585
|
+
throw new Error("Tap key sig not found");
|
|
16586
|
+
}
|
|
16587
|
+
tx.updateInput(0, {
|
|
16588
|
+
finalScriptWitness: [tapKeySig]
|
|
16589
|
+
});
|
|
16590
|
+
return tx;
|
|
16591
|
+
}
|
|
16592
|
+
tx.finalize();
|
|
16593
|
+
return tx;
|
|
16594
|
+
}
|
|
16595
|
+
var ExitPathError = class extends Error {
|
|
16596
|
+
constructor(reason, message) {
|
|
16597
|
+
super(message);
|
|
16598
|
+
this.reason = reason;
|
|
16599
|
+
this.name = "ExitPathError";
|
|
16600
|
+
}
|
|
16601
|
+
};
|
|
16602
|
+
function delayWeight(sequence) {
|
|
16603
|
+
const t = sequenceToTimelock(sequence);
|
|
16604
|
+
return t.type === "blocks" ? Number(t.value) : Number(t.value) / 600;
|
|
16605
|
+
}
|
|
16606
|
+
async function resolveUnilateralPath(params) {
|
|
16607
|
+
const { vtxo, scriptHex, contractRepository } = params;
|
|
16608
|
+
const contract = contractRepository ? (await contractRepository.getContracts({ script: scriptHex }))[0] : void 0;
|
|
16609
|
+
if (contract) {
|
|
16610
|
+
const handler = contractHandlers.get(contract.type);
|
|
16611
|
+
if (!handler) {
|
|
16612
|
+
throw new ExitPathError(
|
|
16613
|
+
"no-handler",
|
|
16614
|
+
`no contract handler registered for type '${contract.type}'`
|
|
16615
|
+
);
|
|
16616
|
+
}
|
|
16617
|
+
const script = handler.createScript(contract.params);
|
|
16618
|
+
const paths = handler.getAllSpendingPaths(script, contract, {
|
|
16619
|
+
collaborative: false,
|
|
16620
|
+
currentTime: params.currentTime,
|
|
16621
|
+
walletDescriptor: params.walletDescriptor,
|
|
16622
|
+
walletPubKey: params.walletPubKeyHex
|
|
16623
|
+
}).filter((p) => p.sequence !== void 0);
|
|
16624
|
+
if (paths.length === 0) {
|
|
16625
|
+
throw new ExitPathError(
|
|
16626
|
+
"no-unilateral-path",
|
|
16627
|
+
`no unilateral path for vtxo ${vtxo.txid}:${vtxo.vout} (type '${contract.type}')`
|
|
16628
|
+
);
|
|
16629
|
+
}
|
|
16630
|
+
paths.sort((a, b) => delayWeight(a.sequence) - delayWeight(b.sequence));
|
|
16631
|
+
return { selection: paths[0], label: `${contract.type}:unilateral` };
|
|
16632
|
+
}
|
|
16633
|
+
const decoded = VtxoScript.decode(vtxo.tapTree);
|
|
16634
|
+
let best;
|
|
16635
|
+
for (const exit of decoded.exitPaths()) {
|
|
16636
|
+
const leaf = decoded.findLeaf(hex.encode(exit.script));
|
|
16637
|
+
if (!leaf) continue;
|
|
16638
|
+
const sequence = timelockToSequence(exit.params.timelock);
|
|
16639
|
+
const weight = delayWeight(sequence);
|
|
16640
|
+
if (!best || weight < best.weight) {
|
|
16641
|
+
best = { selection: { leaf, sequence }, weight };
|
|
16642
|
+
}
|
|
16643
|
+
}
|
|
16644
|
+
if (!best) {
|
|
16645
|
+
throw new ExitPathError(
|
|
16646
|
+
"no-unilateral-path",
|
|
16647
|
+
`no exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`
|
|
16648
|
+
);
|
|
16649
|
+
}
|
|
16650
|
+
return { selection: best.selection, label: "default:exit" };
|
|
16651
|
+
}
|
|
16652
|
+
|
|
16653
|
+
// src/wallet/unroll.ts
|
|
16521
16654
|
function chainTxTypeToChainedExit(t) {
|
|
16522
16655
|
switch (t) {
|
|
16523
16656
|
case "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */:
|
|
@@ -16627,22 +16760,7 @@ var Unroll;
|
|
|
16627
16760
|
if (!virtualTxBase64) {
|
|
16628
16761
|
throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
|
|
16629
16762
|
}
|
|
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
|
-
}
|
|
16763
|
+
const tx = finalizeVirtualTx(nextTxToBroadcast.type, virtualTxBase64);
|
|
16646
16764
|
const pkg = await this.bumper.bumpP2A(tx);
|
|
16647
16765
|
return {
|
|
16648
16766
|
type: 0 /* UNROLL */,
|
|
@@ -16696,20 +16814,21 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
|
|
|
16696
16814
|
if (!txStatus.confirmed) {
|
|
16697
16815
|
throw new Error(`tx ${vtxo.txid} is not confirmed`);
|
|
16698
16816
|
}
|
|
16699
|
-
const
|
|
16700
|
-
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
|
|
16704
|
-
|
|
16817
|
+
const resolved = await resolveUnilateralPath({
|
|
16818
|
+
vtxo,
|
|
16819
|
+
scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
|
|
16820
|
+
contractRepository: wallet.contractRepository,
|
|
16821
|
+
walletPubKeyHex: hex.encode(await wallet.identity.xOnlyPublicKey()),
|
|
16822
|
+
currentTime: Date.now()
|
|
16823
|
+
});
|
|
16824
|
+
const spendingLeaf = resolved.selection.leaf;
|
|
16825
|
+
const sequence = resolved.selection.sequence;
|
|
16826
|
+
const timelock = sequenceToTimelock(sequence);
|
|
16827
|
+
const elapsed = timelock.type === "blocks" ? chainTip.height >= txStatus.blockHeight + Number(timelock.value) : chainTip.time >= txStatus.blockTime + Number(timelock.value);
|
|
16828
|
+
if (!elapsed) {
|
|
16705
16829
|
throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
|
|
16706
16830
|
}
|
|
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
16831
|
totalAmount += BigInt(vtxo.value);
|
|
16712
|
-
const sequence = timelockToSequence(exit.params.timelock);
|
|
16713
16832
|
inputs.push({
|
|
16714
16833
|
txid: vtxo.txid,
|
|
16715
16834
|
index: vtxo.vout,
|
|
@@ -16773,21 +16892,809 @@ function doWait(onchainProvider, txid) {
|
|
|
16773
16892
|
});
|
|
16774
16893
|
};
|
|
16775
16894
|
}
|
|
16776
|
-
|
|
16777
|
-
|
|
16778
|
-
|
|
16779
|
-
|
|
16780
|
-
|
|
16781
|
-
|
|
16895
|
+
|
|
16896
|
+
// src/wallet/exit/chain.ts
|
|
16897
|
+
var isCommitment = (t) => t === "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */ || t === "INDEXER_CHAINED_TX_TYPE_UNSPECIFIED" /* UNSPECIFIED */;
|
|
16898
|
+
async function buildExitDag(params) {
|
|
16899
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
16900
|
+
for (const vtxo of params.vtxos) {
|
|
16901
|
+
const outpoint = `${vtxo.txid}:${vtxo.vout}`;
|
|
16902
|
+
let pageIndex = 0;
|
|
16903
|
+
let hasMore = true;
|
|
16904
|
+
while (hasMore) {
|
|
16905
|
+
const { chain, page } = await params.indexer.getVtxoChain(
|
|
16906
|
+
{ txid: vtxo.txid, vout: vtxo.vout },
|
|
16907
|
+
{ pageIndex, pageSize: DEFAULT_PAGE_SIZE }
|
|
16908
|
+
);
|
|
16909
|
+
for (const chainTx of chain) {
|
|
16910
|
+
const existing = byTxid.get(chainTx.txid);
|
|
16911
|
+
if (existing) {
|
|
16912
|
+
existing.forVtxos.add(outpoint);
|
|
16913
|
+
} else {
|
|
16914
|
+
byTxid.set(chainTx.txid, { ...chainTx, forVtxos: /* @__PURE__ */ new Set([outpoint]) });
|
|
16915
|
+
}
|
|
16782
16916
|
}
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16917
|
+
hasMore = page ? chain.length === DEFAULT_PAGE_SIZE : false;
|
|
16918
|
+
pageIndex++;
|
|
16919
|
+
}
|
|
16920
|
+
}
|
|
16921
|
+
const pending = new Map(
|
|
16922
|
+
[...byTxid.values()].filter((tx) => !isCommitment(tx.type)).map((tx) => [tx.txid, tx])
|
|
16923
|
+
);
|
|
16924
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
16925
|
+
for (const tx of pending.values()) {
|
|
16926
|
+
const parents = tx.spends.filter((p) => pending.has(p));
|
|
16927
|
+
inDegree.set(tx.txid, parents.length);
|
|
16928
|
+
}
|
|
16929
|
+
const order = [];
|
|
16930
|
+
const queue = [...pending.values()].filter((tx) => inDegree.get(tx.txid) === 0).sort((a, b) => a.txid.localeCompare(b.txid));
|
|
16931
|
+
while (queue.length > 0) {
|
|
16932
|
+
const tx = queue.shift();
|
|
16933
|
+
order.push(tx);
|
|
16934
|
+
const children = [...pending.values()].filter((c) => c.spends.includes(tx.txid)).sort((a, b) => a.txid.localeCompare(b.txid));
|
|
16935
|
+
for (const child of children) {
|
|
16936
|
+
const deg = inDegree.get(child.txid) - 1;
|
|
16937
|
+
inDegree.set(child.txid, deg);
|
|
16938
|
+
if (deg === 0) queue.push(child);
|
|
16939
|
+
}
|
|
16940
|
+
}
|
|
16941
|
+
if (order.length !== pending.size) {
|
|
16942
|
+
throw new Error("inconsistent vtxo chain from indexer (cycle detected)");
|
|
16943
|
+
}
|
|
16944
|
+
const nodes = [];
|
|
16945
|
+
for (const tx of order) {
|
|
16946
|
+
let confirmed = false;
|
|
16947
|
+
try {
|
|
16948
|
+
const status = await params.onchain.getTxStatus(tx.txid);
|
|
16949
|
+
confirmed = status.confirmed;
|
|
16950
|
+
} catch {
|
|
16951
|
+
confirmed = false;
|
|
16952
|
+
}
|
|
16953
|
+
const expires = Number(tx.expiresAt);
|
|
16954
|
+
nodes.push({
|
|
16955
|
+
txid: tx.txid,
|
|
16956
|
+
type: tx.type,
|
|
16957
|
+
expiresAt: Number.isFinite(expires) && expires > 0 ? expires : void 0,
|
|
16958
|
+
forVtxos: [...tx.forVtxos].sort(),
|
|
16959
|
+
confirmed
|
|
16960
|
+
});
|
|
16961
|
+
}
|
|
16962
|
+
return nodes;
|
|
16963
|
+
}
|
|
16964
|
+
function topoSortByDeps(items, idOf, depsOf) {
|
|
16965
|
+
const produced = new Set(items.map(idOf));
|
|
16966
|
+
const emitted = /* @__PURE__ */ new Set();
|
|
16967
|
+
const ordered = [];
|
|
16968
|
+
let remaining = items;
|
|
16969
|
+
while (remaining.length > 0) {
|
|
16970
|
+
const next = [];
|
|
16971
|
+
let progress = false;
|
|
16972
|
+
for (const item of remaining) {
|
|
16973
|
+
if (depsOf(item).every((d) => !produced.has(d) || emitted.has(d))) {
|
|
16974
|
+
ordered.push(item);
|
|
16975
|
+
emitted.add(idOf(item));
|
|
16976
|
+
progress = true;
|
|
16977
|
+
} else {
|
|
16978
|
+
next.push(item);
|
|
16786
16979
|
}
|
|
16787
16980
|
}
|
|
16981
|
+
if (!progress) {
|
|
16982
|
+
throw new Error("topological sort: cycle or unsatisfiable dependency");
|
|
16983
|
+
}
|
|
16984
|
+
remaining = next;
|
|
16788
16985
|
}
|
|
16789
|
-
return
|
|
16986
|
+
return ordered;
|
|
16987
|
+
}
|
|
16988
|
+
function extraWitnessSize(path) {
|
|
16989
|
+
return (path.extraWitness ?? []).reduce((sum, item) => sum + 1 + item.length, 0);
|
|
16790
16990
|
}
|
|
16991
|
+
function sweepFeeFor(path, outputAddress, network, feeRate) {
|
|
16992
|
+
const [controlBlock, scriptWithVersion] = path.leaf;
|
|
16993
|
+
const estimator = TxWeightEstimator.create().addTapscriptInput(
|
|
16994
|
+
64 + extraWitnessSize(path),
|
|
16995
|
+
scriptWithVersion.length,
|
|
16996
|
+
TaprootControlBlock.encode(controlBlock).length
|
|
16997
|
+
).addOutputAddress(outputAddress, network);
|
|
16998
|
+
return Number(estimator.vsize().fee(BigInt(Math.ceil(feeRate))));
|
|
16999
|
+
}
|
|
17000
|
+
async function buildSignedSweep(params) {
|
|
17001
|
+
const { vtxo, path, outputAddress, feeRate, network, identity } = params;
|
|
17002
|
+
const fee = sweepFeeFor(path, outputAddress, network, feeRate);
|
|
17003
|
+
const sendAmount = BigInt(vtxo.value) - BigInt(fee);
|
|
17004
|
+
if (sendAmount < BigInt(DUST_AMOUNT)) {
|
|
17005
|
+
throw new Error(
|
|
17006
|
+
`uneconomic vtxo ${vtxo.txid}:${vtxo.vout}: value ${vtxo.value} - fee ${fee} < dust`
|
|
17007
|
+
);
|
|
17008
|
+
}
|
|
17009
|
+
const tx = new Transaction({ version: 2 });
|
|
17010
|
+
tx.addInput({
|
|
17011
|
+
txid: vtxo.txid,
|
|
17012
|
+
index: vtxo.vout,
|
|
17013
|
+
tapLeafScript: [path.leaf],
|
|
17014
|
+
sequence: path.sequence,
|
|
17015
|
+
witnessUtxo: { amount: BigInt(vtxo.value), script: vtxo.pkScript },
|
|
17016
|
+
sighashType: SigHash.DEFAULT
|
|
17017
|
+
});
|
|
17018
|
+
tx.addOutputAddress(outputAddress, sendAmount, network);
|
|
17019
|
+
const signed = await identity.sign(tx);
|
|
17020
|
+
if (!path.extraWitness || path.extraWitness.length === 0) {
|
|
17021
|
+
signed.finalize();
|
|
17022
|
+
return { tx: signed, fee };
|
|
17023
|
+
}
|
|
17024
|
+
const input = signed.getInput(0);
|
|
17025
|
+
const tapScriptSig = input.tapScriptSig;
|
|
17026
|
+
if (!tapScriptSig || tapScriptSig.length === 0) {
|
|
17027
|
+
throw new Error("exit path requires additional signers");
|
|
17028
|
+
}
|
|
17029
|
+
const [controlBlock] = path.leaf;
|
|
17030
|
+
signed.updateInput(0, {
|
|
17031
|
+
finalScriptWitness: [
|
|
17032
|
+
tapScriptSig[0][1],
|
|
17033
|
+
...path.extraWitness,
|
|
17034
|
+
scriptFromTapLeafScript(path.leaf),
|
|
17035
|
+
TaprootControlBlock.encode(controlBlock)
|
|
17036
|
+
]
|
|
17037
|
+
});
|
|
17038
|
+
return { tx: signed, fee };
|
|
17039
|
+
}
|
|
17040
|
+
|
|
17041
|
+
// src/wallet/exit/estimate.ts
|
|
17042
|
+
var CHILD_OUTPUT_DUST = 546;
|
|
17043
|
+
function stepFundingAmount(stepFee) {
|
|
17044
|
+
return stepFee + CHILD_OUTPUT_DUST;
|
|
17045
|
+
}
|
|
17046
|
+
function resolveMode(opts) {
|
|
17047
|
+
return opts.mode ?? "funded";
|
|
17048
|
+
}
|
|
17049
|
+
function resolveNetworkName(opts) {
|
|
17050
|
+
if (opts.networkName) return opts.networkName;
|
|
17051
|
+
const { bech32 } = opts.wallet.network;
|
|
17052
|
+
if (bech32 === "bc") return "bitcoin";
|
|
17053
|
+
if (bech32 === "bcrt") return "regtest";
|
|
17054
|
+
return "testnet";
|
|
17055
|
+
}
|
|
17056
|
+
async function resolveFeeRate(opts) {
|
|
17057
|
+
if (opts.feeRate) return Math.ceil(opts.feeRate);
|
|
17058
|
+
const feeRate = await opts.wallet.onchainProvider.getFeeRate();
|
|
17059
|
+
if (!feeRate || feeRate < OnchainWallet.MIN_FEE_RATE) return OnchainWallet.MIN_FEE_RATE;
|
|
17060
|
+
return Math.ceil(feeRate);
|
|
17061
|
+
}
|
|
17062
|
+
async function selectExitVtxos(opts) {
|
|
17063
|
+
if (!opts.vtxos) return opts.wallet.getVtxos();
|
|
17064
|
+
const res = await opts.wallet.indexerProvider.getVtxos({ outpoints: opts.vtxos });
|
|
17065
|
+
const tapTrees = /* @__PURE__ */ new Map();
|
|
17066
|
+
const out = [];
|
|
17067
|
+
for (const vtxo of res.vtxos) {
|
|
17068
|
+
if (vtxo.isSpent) continue;
|
|
17069
|
+
let tapTree = tapTrees.get(vtxo.script);
|
|
17070
|
+
if (!tapTree) {
|
|
17071
|
+
const [contract] = await opts.wallet.contractRepository.getContracts({
|
|
17072
|
+
script: vtxo.script
|
|
17073
|
+
});
|
|
17074
|
+
if (!contract) {
|
|
17075
|
+
throw new Error(
|
|
17076
|
+
`no contract registered for vtxo script ${vtxo.script} \u2014 register the contract before exiting`
|
|
17077
|
+
);
|
|
17078
|
+
}
|
|
17079
|
+
const handler = contractHandlers.get(contract.type);
|
|
17080
|
+
if (!handler) {
|
|
17081
|
+
throw new Error(`no contract handler registered for type '${contract.type}'`);
|
|
17082
|
+
}
|
|
17083
|
+
tapTree = handler.createScript(contract.params).encode();
|
|
17084
|
+
tapTrees.set(vtxo.script, tapTree);
|
|
17085
|
+
}
|
|
17086
|
+
out.push({ txid: vtxo.txid, vout: vtxo.vout, value: vtxo.value, tapTree });
|
|
17087
|
+
}
|
|
17088
|
+
return out;
|
|
17089
|
+
}
|
|
17090
|
+
async function computeExitLayout(opts, feeRate) {
|
|
17091
|
+
const { wallet, onchainWallet } = opts;
|
|
17092
|
+
const vtxos = await selectExitVtxos(opts);
|
|
17093
|
+
if (vtxos.length === 0) throw new Error("no vtxos to exit");
|
|
17094
|
+
const dag = await buildExitDag({
|
|
17095
|
+
vtxos,
|
|
17096
|
+
indexer: wallet.indexerProvider,
|
|
17097
|
+
onchain: wallet.onchainProvider
|
|
17098
|
+
});
|
|
17099
|
+
const pendingNodes = dag.filter((n) => !n.confirmed);
|
|
17100
|
+
const psbts = /* @__PURE__ */ new Map();
|
|
17101
|
+
if (pendingNodes.length > 0) {
|
|
17102
|
+
const txids = pendingNodes.map((n) => n.txid);
|
|
17103
|
+
let pageIndex = 0;
|
|
17104
|
+
let hasMore = true;
|
|
17105
|
+
while (hasMore) {
|
|
17106
|
+
const { txs, page } = await wallet.indexerProvider.getVirtualTxs(txids, {
|
|
17107
|
+
pageIndex,
|
|
17108
|
+
pageSize: DEFAULT_PAGE_SIZE
|
|
17109
|
+
});
|
|
17110
|
+
for (const psbt of txs) {
|
|
17111
|
+
psbts.set(Transaction.fromPSBT(base64.decode(psbt)).id, psbt);
|
|
17112
|
+
}
|
|
17113
|
+
hasMore = page ? txs.length === DEFAULT_PAGE_SIZE : false;
|
|
17114
|
+
pageIndex++;
|
|
17115
|
+
}
|
|
17116
|
+
}
|
|
17117
|
+
const childVsize = Number(
|
|
17118
|
+
TxWeightEstimator.create().addP2AInput().addKeySpendInput(true).addOutputAddress(onchainWallet.address, wallet.network).vsize().value
|
|
17119
|
+
);
|
|
17120
|
+
const rawSteps = pendingNodes.map((node) => {
|
|
17121
|
+
const psbt = psbts.get(node.txid);
|
|
17122
|
+
if (!psbt) {
|
|
17123
|
+
throw new Error(`indexer did not return virtual tx ${node.txid}`);
|
|
17124
|
+
}
|
|
17125
|
+
const parent = finalizeVirtualTx(node.type, psbt);
|
|
17126
|
+
const stepFee = Math.ceil(feeRate * (parent.vsize + childVsize));
|
|
17127
|
+
return { node, parent, stepFee, funding: stepFundingAmount(stepFee) };
|
|
17128
|
+
});
|
|
17129
|
+
const parentInputTxids = (tx) => {
|
|
17130
|
+
const ids = [];
|
|
17131
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
17132
|
+
const txid = tx.getInput(i).txid;
|
|
17133
|
+
if (txid) ids.push(hex.encode(txid));
|
|
17134
|
+
}
|
|
17135
|
+
return ids;
|
|
17136
|
+
};
|
|
17137
|
+
const steps = topoSortByDeps(
|
|
17138
|
+
rawSteps,
|
|
17139
|
+
(s) => s.parent.id,
|
|
17140
|
+
(s) => parentInputTxids(s.parent)
|
|
17141
|
+
);
|
|
17142
|
+
const walletPubKeyHex = hex.encode(await wallet.identity.xOnlyPublicKey());
|
|
17143
|
+
const infos = [];
|
|
17144
|
+
const sweeps = [];
|
|
17145
|
+
for (const vtxo of vtxos) {
|
|
17146
|
+
const outpoint = `${vtxo.txid}:${vtxo.vout}`;
|
|
17147
|
+
try {
|
|
17148
|
+
const resolved = await resolveUnilateralPath({
|
|
17149
|
+
vtxo,
|
|
17150
|
+
scriptHex: hex.encode(VtxoScript.decode(vtxo.tapTree).pkScript),
|
|
17151
|
+
contractRepository: wallet.contractRepository,
|
|
17152
|
+
walletPubKeyHex,
|
|
17153
|
+
currentTime: Date.now()
|
|
17154
|
+
});
|
|
17155
|
+
const sweepFee = sweepFeeFor(
|
|
17156
|
+
resolved.selection,
|
|
17157
|
+
opts.sweepAddress,
|
|
17158
|
+
wallet.network,
|
|
17159
|
+
feeRate
|
|
17160
|
+
);
|
|
17161
|
+
if (vtxo.value - sweepFee < CHILD_OUTPUT_DUST) {
|
|
17162
|
+
infos.push({
|
|
17163
|
+
outpoint,
|
|
17164
|
+
value: vtxo.value,
|
|
17165
|
+
skipped: `uneconomic: value ${vtxo.value} <= sweep fee + dust`
|
|
17166
|
+
});
|
|
17167
|
+
continue;
|
|
17168
|
+
}
|
|
17169
|
+
const t = sequenceToTimelock(resolved.selection.sequence);
|
|
17170
|
+
const delay = { type: t.type, value: Number(t.value) };
|
|
17171
|
+
infos.push({ outpoint, value: vtxo.value, sweepFee, path: resolved.label, delay });
|
|
17172
|
+
sweeps.push({ vtxo, resolved, sweepFee, delay });
|
|
17173
|
+
} catch (e) {
|
|
17174
|
+
if (e instanceof ExitPathError) {
|
|
17175
|
+
infos.push({ outpoint, value: vtxo.value, skipped: e.message });
|
|
17176
|
+
continue;
|
|
17177
|
+
}
|
|
17178
|
+
throw e;
|
|
17179
|
+
}
|
|
17180
|
+
}
|
|
17181
|
+
const coins = (await onchainWallet.getCoins()).filter((c) => c.status.confirmed);
|
|
17182
|
+
const balance = coins.reduce((sum, c) => sum + c.value, 0);
|
|
17183
|
+
const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
|
|
17184
|
+
const splitterFeeFor = (inputCount) => {
|
|
17185
|
+
if (steps.length === 0) return 0;
|
|
17186
|
+
const est = TxWeightEstimator.create();
|
|
17187
|
+
for (let i = 0; i < inputCount; i++) est.addKeySpendInput(true);
|
|
17188
|
+
for (let i = 0; i < steps.length + 1; i++) {
|
|
17189
|
+
est.addOutputAddress(onchainWallet.address, wallet.network);
|
|
17190
|
+
}
|
|
17191
|
+
return Number(est.vsize().fee(BigInt(feeRate)));
|
|
17192
|
+
};
|
|
17193
|
+
const graph = resolveMode(opts) === "graph";
|
|
17194
|
+
let splitterFee = 0;
|
|
17195
|
+
if (!graph) {
|
|
17196
|
+
splitterFee = splitterFeeFor(Math.max(1, coins.length));
|
|
17197
|
+
if (steps.length > 0 && balance < fundingTotal + splitterFee) {
|
|
17198
|
+
splitterFee = splitterFeeFor(coins.length + 1);
|
|
17199
|
+
}
|
|
17200
|
+
}
|
|
17201
|
+
const stepFees = steps.reduce((s, x) => s + x.stepFee, 0);
|
|
17202
|
+
const sweepFees = sweeps.reduce((s, x) => s + x.sweepFee, 0);
|
|
17203
|
+
const totals = {
|
|
17204
|
+
// graph: each unroll step is parent + a live-built child (2 txs), no
|
|
17205
|
+
// splitter. funded: + 1 splitter tx and pre-signed children.
|
|
17206
|
+
txCount: (graph ? 0 : steps.length > 0 ? 1 : 0) + steps.length * 2 + sweeps.length,
|
|
17207
|
+
totalFeeSats: splitterFee + stepFees + sweepFees,
|
|
17208
|
+
// graph funding is what the executor sends to its own fee address:
|
|
17209
|
+
// just the CPFP fees (change recycles); funded also locks the
|
|
17210
|
+
// per-child dust into the splitter.
|
|
17211
|
+
fundingRequiredSats: graph ? stepFees : splitterFee + fundingTotal,
|
|
17212
|
+
recoveredSats: sweeps.reduce((s, x) => s + (x.vtxo.value - x.sweepFee), 0)
|
|
17213
|
+
};
|
|
17214
|
+
const expiries = pendingNodes.map((n) => n.expiresAt).filter((x) => x !== void 0);
|
|
17215
|
+
const validUntil = expiries.length > 0 ? Math.min(...expiries) : void 0;
|
|
17216
|
+
return {
|
|
17217
|
+
vtxos,
|
|
17218
|
+
dag,
|
|
17219
|
+
steps,
|
|
17220
|
+
sweeps,
|
|
17221
|
+
infos,
|
|
17222
|
+
totals,
|
|
17223
|
+
splitterFee,
|
|
17224
|
+
validUntil,
|
|
17225
|
+
balance,
|
|
17226
|
+
coins
|
|
17227
|
+
};
|
|
17228
|
+
}
|
|
17229
|
+
async function estimate(opts) {
|
|
17230
|
+
const feeRate = await resolveFeeRate(opts);
|
|
17231
|
+
const layout = await computeExitLayout(opts, feeRate);
|
|
17232
|
+
if (resolveMode(opts) === "graph") {
|
|
17233
|
+
return {
|
|
17234
|
+
feeRate,
|
|
17235
|
+
fundingAddress: "",
|
|
17236
|
+
currentBalanceSats: 0,
|
|
17237
|
+
shortfallSats: layout.totals.fundingRequiredSats,
|
|
17238
|
+
validUntil: layout.validUntil,
|
|
17239
|
+
totals: layout.totals,
|
|
17240
|
+
vtxos: layout.infos
|
|
17241
|
+
};
|
|
17242
|
+
}
|
|
17243
|
+
return {
|
|
17244
|
+
feeRate,
|
|
17245
|
+
fundingAddress: opts.onchainWallet.address,
|
|
17246
|
+
currentBalanceSats: layout.balance,
|
|
17247
|
+
shortfallSats: Math.max(0, layout.totals.fundingRequiredSats - layout.balance),
|
|
17248
|
+
validUntil: layout.validUntil,
|
|
17249
|
+
totals: layout.totals,
|
|
17250
|
+
vtxos: layout.infos
|
|
17251
|
+
};
|
|
17252
|
+
}
|
|
17253
|
+
|
|
17254
|
+
// src/wallet/exit/executor.ts
|
|
17255
|
+
var Executor = class {
|
|
17256
|
+
constructor(pkg, provider, opts) {
|
|
17257
|
+
this.pkg = pkg;
|
|
17258
|
+
this.provider = provider;
|
|
17259
|
+
this.pollIntervalMs = opts?.pollIntervalMs ?? 5e3;
|
|
17260
|
+
this.feeWallet = opts?.feeWallet;
|
|
17261
|
+
}
|
|
17262
|
+
pollIntervalMs;
|
|
17263
|
+
feeWallet;
|
|
17264
|
+
sleep() {
|
|
17265
|
+
return new Promise((r) => setTimeout(r, this.pollIntervalMs));
|
|
17266
|
+
}
|
|
17267
|
+
async status(txid) {
|
|
17268
|
+
try {
|
|
17269
|
+
return await this.provider.getTxStatus(txid);
|
|
17270
|
+
} catch {
|
|
17271
|
+
return void 0;
|
|
17272
|
+
}
|
|
17273
|
+
}
|
|
17274
|
+
async waitConfirmed(txid) {
|
|
17275
|
+
for (; ; ) {
|
|
17276
|
+
const s = await this.status(txid);
|
|
17277
|
+
if (s?.confirmed) return s;
|
|
17278
|
+
await this.sleep();
|
|
17279
|
+
}
|
|
17280
|
+
}
|
|
17281
|
+
async *[Symbol.asyncIterator]() {
|
|
17282
|
+
const dead = /* @__PURE__ */ new Set();
|
|
17283
|
+
if (this.pkg.validUntil && Date.now() / 1e3 > this.pkg.validUntil) {
|
|
17284
|
+
yield {
|
|
17285
|
+
stepIndex: -1,
|
|
17286
|
+
kind: "broadcast",
|
|
17287
|
+
status: "warning",
|
|
17288
|
+
reason: `package validUntil ${this.pkg.validUntil} has passed; the operator may have swept \u2014 attempting anyway`
|
|
17289
|
+
};
|
|
17290
|
+
}
|
|
17291
|
+
const sweeps = [];
|
|
17292
|
+
for (let i = 0; i < this.pkg.steps.length; i++) {
|
|
17293
|
+
const step = this.pkg.steps[i];
|
|
17294
|
+
if (step.kind === "sweep") {
|
|
17295
|
+
sweeps.push({ index: i, step });
|
|
17296
|
+
continue;
|
|
17297
|
+
}
|
|
17298
|
+
const forVtxos = step.kind === "package" || step.kind === "bump" ? step.forVtxos : void 0;
|
|
17299
|
+
if (forVtxos && forVtxos.every((v) => dead.has(v))) {
|
|
17300
|
+
yield {
|
|
17301
|
+
stepIndex: i,
|
|
17302
|
+
kind: step.kind,
|
|
17303
|
+
status: "skipped",
|
|
17304
|
+
forVtxos,
|
|
17305
|
+
reason: "branch failed earlier"
|
|
17306
|
+
};
|
|
17307
|
+
continue;
|
|
17308
|
+
}
|
|
17309
|
+
const anchorTxid = step.kind === "package" || step.kind === "bump" ? step.parentTxid : step.txid;
|
|
17310
|
+
const existing = await this.status(anchorTxid);
|
|
17311
|
+
if (existing?.confirmed) {
|
|
17312
|
+
yield {
|
|
17313
|
+
stepIndex: i,
|
|
17314
|
+
kind: step.kind,
|
|
17315
|
+
status: "skipped",
|
|
17316
|
+
txid: anchorTxid,
|
|
17317
|
+
forVtxos
|
|
17318
|
+
};
|
|
17319
|
+
continue;
|
|
17320
|
+
}
|
|
17321
|
+
if (!existing) {
|
|
17322
|
+
try {
|
|
17323
|
+
if (step.kind === "package") {
|
|
17324
|
+
await this.provider.broadcastTransaction(step.parentHex, step.childHex);
|
|
17325
|
+
} else if (step.kind === "bump") {
|
|
17326
|
+
if (!this.feeWallet) {
|
|
17327
|
+
throw new Error(
|
|
17328
|
+
"graph package requires a fee wallet (opts.feeWallet) to fund CPFP bumps"
|
|
17329
|
+
);
|
|
17330
|
+
}
|
|
17331
|
+
const [parentHex, childHex] = await this.feeWallet.bumpAnchor(
|
|
17332
|
+
step.parentHex,
|
|
17333
|
+
this.pkg.feeRate
|
|
17334
|
+
);
|
|
17335
|
+
await this.provider.broadcastTransaction(parentHex, childHex);
|
|
17336
|
+
} else {
|
|
17337
|
+
await this.provider.broadcastTransaction(step.hex);
|
|
17338
|
+
}
|
|
17339
|
+
yield {
|
|
17340
|
+
stepIndex: i,
|
|
17341
|
+
kind: step.kind,
|
|
17342
|
+
status: "broadcast",
|
|
17343
|
+
txid: anchorTxid,
|
|
17344
|
+
forVtxos
|
|
17345
|
+
};
|
|
17346
|
+
} catch (e) {
|
|
17347
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
17348
|
+
if (step.kind === "broadcast") {
|
|
17349
|
+
for (const s of this.pkg.steps) {
|
|
17350
|
+
if (s.kind === "package") s.forVtxos.forEach((v) => dead.add(v));
|
|
17351
|
+
}
|
|
17352
|
+
} else if (step.kind === "package" || step.kind === "bump") {
|
|
17353
|
+
step.forVtxos.forEach((v) => dead.add(v));
|
|
17354
|
+
}
|
|
17355
|
+
yield {
|
|
17356
|
+
stepIndex: i,
|
|
17357
|
+
kind: step.kind,
|
|
17358
|
+
status: "failed",
|
|
17359
|
+
txid: anchorTxid,
|
|
17360
|
+
forVtxos,
|
|
17361
|
+
reason
|
|
17362
|
+
};
|
|
17363
|
+
continue;
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
17366
|
+
await this.waitConfirmed(anchorTxid);
|
|
17367
|
+
yield {
|
|
17368
|
+
stepIndex: i,
|
|
17369
|
+
kind: step.kind,
|
|
17370
|
+
status: "confirmed",
|
|
17371
|
+
txid: anchorTxid,
|
|
17372
|
+
forVtxos
|
|
17373
|
+
};
|
|
17374
|
+
}
|
|
17375
|
+
for (const { index, step } of sweeps) {
|
|
17376
|
+
if (dead.has(step.vtxo)) {
|
|
17377
|
+
yield {
|
|
17378
|
+
stepIndex: index,
|
|
17379
|
+
kind: "sweep",
|
|
17380
|
+
status: "failed",
|
|
17381
|
+
txid: step.txid,
|
|
17382
|
+
forVtxos: [step.vtxo],
|
|
17383
|
+
reason: "branch failed earlier"
|
|
17384
|
+
};
|
|
17385
|
+
}
|
|
17386
|
+
}
|
|
17387
|
+
const pending = sweeps.filter(({ step }) => !dead.has(step.vtxo));
|
|
17388
|
+
const waitingAnnounced = /* @__PURE__ */ new Set();
|
|
17389
|
+
const done = /* @__PURE__ */ new Set();
|
|
17390
|
+
while (done.size < pending.length) {
|
|
17391
|
+
for (const { index, step } of pending) {
|
|
17392
|
+
if (done.has(index)) continue;
|
|
17393
|
+
const swept = await this.status(step.txid);
|
|
17394
|
+
if (swept?.confirmed) {
|
|
17395
|
+
done.add(index);
|
|
17396
|
+
yield {
|
|
17397
|
+
stepIndex: index,
|
|
17398
|
+
kind: "sweep",
|
|
17399
|
+
status: "confirmed",
|
|
17400
|
+
txid: step.txid,
|
|
17401
|
+
forVtxos: [step.vtxo]
|
|
17402
|
+
};
|
|
17403
|
+
continue;
|
|
17404
|
+
}
|
|
17405
|
+
if (swept) continue;
|
|
17406
|
+
const dep = await this.status(step.dependsOnTxid);
|
|
17407
|
+
if (!dep?.confirmed) continue;
|
|
17408
|
+
const tip = await this.provider.getChainTip();
|
|
17409
|
+
const mature = step.delay.type === "blocks" ? tip.height >= (dep.blockHeight ?? 0) + step.delay.value : tip.time >= (dep.blockTime ?? 0) + step.delay.value;
|
|
17410
|
+
if (!mature) {
|
|
17411
|
+
if (!waitingAnnounced.has(index)) {
|
|
17412
|
+
waitingAnnounced.add(index);
|
|
17413
|
+
yield {
|
|
17414
|
+
stepIndex: index,
|
|
17415
|
+
kind: "sweep",
|
|
17416
|
+
status: "waiting_csv",
|
|
17417
|
+
txid: step.txid,
|
|
17418
|
+
forVtxos: [step.vtxo],
|
|
17419
|
+
...step.delay.type === "blocks" ? { maturesAtHeight: (dep.blockHeight ?? 0) + step.delay.value } : { maturesAtTime: (dep.blockTime ?? 0) + step.delay.value }
|
|
17420
|
+
};
|
|
17421
|
+
}
|
|
17422
|
+
continue;
|
|
17423
|
+
}
|
|
17424
|
+
try {
|
|
17425
|
+
await this.provider.broadcastTransaction(step.hex);
|
|
17426
|
+
yield {
|
|
17427
|
+
stepIndex: index,
|
|
17428
|
+
kind: "sweep",
|
|
17429
|
+
status: "broadcast",
|
|
17430
|
+
txid: step.txid,
|
|
17431
|
+
forVtxos: [step.vtxo]
|
|
17432
|
+
};
|
|
17433
|
+
} catch (e) {
|
|
17434
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
17435
|
+
if (/non-?bip68|non-?final|premature/i.test(reason)) {
|
|
17436
|
+
continue;
|
|
17437
|
+
}
|
|
17438
|
+
done.add(index);
|
|
17439
|
+
yield {
|
|
17440
|
+
stepIndex: index,
|
|
17441
|
+
kind: "sweep",
|
|
17442
|
+
status: "failed",
|
|
17443
|
+
txid: step.txid,
|
|
17444
|
+
forVtxos: [step.vtxo],
|
|
17445
|
+
reason
|
|
17446
|
+
};
|
|
17447
|
+
}
|
|
17448
|
+
}
|
|
17449
|
+
if (done.size < pending.length) await this.sleep();
|
|
17450
|
+
}
|
|
17451
|
+
}
|
|
17452
|
+
};
|
|
17453
|
+
async function prepare(opts) {
|
|
17454
|
+
const { wallet, onchainWallet } = opts;
|
|
17455
|
+
const feeRate = await resolveFeeRate(opts);
|
|
17456
|
+
const mode = resolveMode(opts);
|
|
17457
|
+
if (mode === "funded") {
|
|
17458
|
+
const walletKey = await wallet.identity.xOnlyPublicKey();
|
|
17459
|
+
if (hex.encode(onchainWallet.onchainP2TR.tapInternalKey) !== hex.encode(walletKey)) {
|
|
17460
|
+
throw new Error("onchainWallet must share the wallet identity");
|
|
17461
|
+
}
|
|
17462
|
+
}
|
|
17463
|
+
const layout = await computeExitLayout(opts, feeRate);
|
|
17464
|
+
const sweepSteps = [];
|
|
17465
|
+
const activeOutpoints = /* @__PURE__ */ new Set();
|
|
17466
|
+
for (const sweep of layout.sweeps) {
|
|
17467
|
+
const outpoint = `${sweep.vtxo.txid}:${sweep.vtxo.vout}`;
|
|
17468
|
+
try {
|
|
17469
|
+
const { tx } = await buildSignedSweep({
|
|
17470
|
+
vtxo: {
|
|
17471
|
+
txid: sweep.vtxo.txid,
|
|
17472
|
+
vout: sweep.vtxo.vout,
|
|
17473
|
+
value: sweep.vtxo.value,
|
|
17474
|
+
pkScript: VtxoScript.decode(sweep.vtxo.tapTree).pkScript
|
|
17475
|
+
},
|
|
17476
|
+
path: sweep.resolved.selection,
|
|
17477
|
+
outputAddress: opts.sweepAddress,
|
|
17478
|
+
feeRate,
|
|
17479
|
+
network: wallet.network,
|
|
17480
|
+
identity: wallet.identity
|
|
17481
|
+
});
|
|
17482
|
+
activeOutpoints.add(outpoint);
|
|
17483
|
+
sweepSteps.push({
|
|
17484
|
+
kind: "sweep",
|
|
17485
|
+
vtxo: outpoint,
|
|
17486
|
+
txid: tx.id,
|
|
17487
|
+
hex: tx.hex,
|
|
17488
|
+
dependsOnTxid: sweep.vtxo.txid,
|
|
17489
|
+
delay: sweep.delay
|
|
17490
|
+
});
|
|
17491
|
+
} catch (e) {
|
|
17492
|
+
const info = layout.infos.find((i) => i.outpoint === outpoint);
|
|
17493
|
+
if (info) {
|
|
17494
|
+
info.skipped = e instanceof Error ? e.message : String(e);
|
|
17495
|
+
delete info.sweepFee;
|
|
17496
|
+
delete info.path;
|
|
17497
|
+
delete info.delay;
|
|
17498
|
+
}
|
|
17499
|
+
}
|
|
17500
|
+
}
|
|
17501
|
+
const steps = layout.steps.filter((s) => s.node.forVtxos.some((v) => activeOutpoints.has(v)));
|
|
17502
|
+
if (steps.length === 0 && sweepSteps.length === 0) {
|
|
17503
|
+
throw new Error("no exitable vtxos (all skipped)");
|
|
17504
|
+
}
|
|
17505
|
+
if (mode === "graph") {
|
|
17506
|
+
const bumpSteps = steps.map((step) => ({
|
|
17507
|
+
kind: "bump",
|
|
17508
|
+
parentTxid: step.parent.id,
|
|
17509
|
+
parentHex: step.parent.hex,
|
|
17510
|
+
forVtxos: step.node.forVtxos.filter((v) => activeOutpoints.has(v))
|
|
17511
|
+
}));
|
|
17512
|
+
const stepFees2 = steps.reduce((s, x) => s + x.stepFee, 0);
|
|
17513
|
+
const activeInfos2 = layout.infos.filter((i) => !i.skipped);
|
|
17514
|
+
const sweepFees2 = activeInfos2.reduce((s, i) => s + (i.sweepFee ?? 0), 0);
|
|
17515
|
+
const recovered2 = activeInfos2.reduce((s, i) => s + (i.value ?? 0) - (i.sweepFee ?? 0), 0);
|
|
17516
|
+
return {
|
|
17517
|
+
version: 1,
|
|
17518
|
+
mode: "graph",
|
|
17519
|
+
network: resolveNetworkName(opts),
|
|
17520
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
17521
|
+
validUntil: layout.validUntil,
|
|
17522
|
+
feeRate,
|
|
17523
|
+
sweepAddress: opts.sweepAddress,
|
|
17524
|
+
totals: {
|
|
17525
|
+
txCount: bumpSteps.length * 2 + sweepSteps.length,
|
|
17526
|
+
totalFeeSats: stepFees2 + sweepFees2,
|
|
17527
|
+
fundingRequiredSats: stepFees2,
|
|
17528
|
+
recoveredSats: recovered2
|
|
17529
|
+
},
|
|
17530
|
+
vtxos: layout.infos,
|
|
17531
|
+
steps: [...bumpSteps, ...sweepSteps]
|
|
17532
|
+
};
|
|
17533
|
+
}
|
|
17534
|
+
const packageSteps = [];
|
|
17535
|
+
let broadcastStep;
|
|
17536
|
+
let splitterFee = 0;
|
|
17537
|
+
if (steps.length > 0) {
|
|
17538
|
+
const fundingTotal = steps.reduce((sum, s) => sum + s.funding, 0);
|
|
17539
|
+
let selected = { inputs: [], changeAmount: 0n };
|
|
17540
|
+
for (let i = 0; i < 10; i++) {
|
|
17541
|
+
const target = fundingTotal + splitterFee;
|
|
17542
|
+
try {
|
|
17543
|
+
selected = selectCoins(layout.coins, target);
|
|
17544
|
+
} catch {
|
|
17545
|
+
throw new Error(
|
|
17546
|
+
`insufficient confirmed onchain funds: need ${target} sats, have ${layout.balance} (deposit the shortfall to ${onchainWallet.address})`
|
|
17547
|
+
);
|
|
17548
|
+
}
|
|
17549
|
+
const est = TxWeightEstimator.create();
|
|
17550
|
+
for (const _ of selected.inputs) est.addKeySpendInput(true);
|
|
17551
|
+
for (const _ of steps) est.addOutputAddress(onchainWallet.address, wallet.network);
|
|
17552
|
+
if (selected.changeAmount >= BigInt(DUST_AMOUNT)) {
|
|
17553
|
+
est.addOutputAddress(onchainWallet.address, wallet.network);
|
|
17554
|
+
}
|
|
17555
|
+
const newFee = Number(est.vsize().fee(BigInt(feeRate)));
|
|
17556
|
+
if (newFee <= splitterFee) break;
|
|
17557
|
+
splitterFee = newFee;
|
|
17558
|
+
}
|
|
17559
|
+
const splitter = new Transaction({ version: 2 });
|
|
17560
|
+
for (const coin of selected.inputs) {
|
|
17561
|
+
splitter.addInput({
|
|
17562
|
+
txid: coin.txid,
|
|
17563
|
+
index: coin.vout,
|
|
17564
|
+
witnessUtxo: {
|
|
17565
|
+
script: onchainWallet.onchainP2TR.script,
|
|
17566
|
+
amount: BigInt(coin.value)
|
|
17567
|
+
},
|
|
17568
|
+
tapInternalKey: onchainWallet.onchainP2TR.tapInternalKey
|
|
17569
|
+
});
|
|
17570
|
+
}
|
|
17571
|
+
for (const step of steps) {
|
|
17572
|
+
splitter.addOutputAddress(onchainWallet.address, BigInt(step.funding), wallet.network);
|
|
17573
|
+
}
|
|
17574
|
+
const inputSum = selected.inputs.reduce((s, c) => s + c.value, 0);
|
|
17575
|
+
const change = BigInt(inputSum - fundingTotal - splitterFee);
|
|
17576
|
+
if (change < 0n) {
|
|
17577
|
+
throw new Error(
|
|
17578
|
+
`insufficient confirmed onchain funds for the splitter fee: need ${fundingTotal + splitterFee} sats, have ${inputSum} (deposit the shortfall to ${onchainWallet.address})`
|
|
17579
|
+
);
|
|
17580
|
+
}
|
|
17581
|
+
if (change >= BigInt(DUST_AMOUNT)) {
|
|
17582
|
+
splitter.addOutputAddress(onchainWallet.address, change, wallet.network);
|
|
17583
|
+
} else if (change > 0n) {
|
|
17584
|
+
splitterFee += Number(change);
|
|
17585
|
+
}
|
|
17586
|
+
const signedSplitter = await wallet.identity.sign(splitter);
|
|
17587
|
+
signedSplitter.finalize();
|
|
17588
|
+
for (let k = 0; k < steps.length; k++) {
|
|
17589
|
+
const step = steps[k];
|
|
17590
|
+
const { child } = buildAnchorChild({
|
|
17591
|
+
parent: step.parent,
|
|
17592
|
+
feeRate,
|
|
17593
|
+
fundingCoins: [{ txid: signedSplitter.id, vout: k, value: step.funding }],
|
|
17594
|
+
changeAddress: onchainWallet.address,
|
|
17595
|
+
changeScript: onchainWallet.onchainP2TR.script,
|
|
17596
|
+
tapInternalKey: onchainWallet.onchainP2TR.tapInternalKey,
|
|
17597
|
+
network: wallet.network
|
|
17598
|
+
});
|
|
17599
|
+
const signedChild = await wallet.identity.sign(child);
|
|
17600
|
+
for (let i = 1; i < signedChild.inputsLength; i++) {
|
|
17601
|
+
signedChild.finalizeIdx(i);
|
|
17602
|
+
}
|
|
17603
|
+
packageSteps.push({
|
|
17604
|
+
kind: "package",
|
|
17605
|
+
parentTxid: step.parent.id,
|
|
17606
|
+
parentHex: step.parent.hex,
|
|
17607
|
+
childTxid: signedChild.id,
|
|
17608
|
+
childHex: signedChild.hex,
|
|
17609
|
+
forVtxos: step.node.forVtxos.filter((v) => activeOutpoints.has(v))
|
|
17610
|
+
});
|
|
17611
|
+
}
|
|
17612
|
+
broadcastStep = { kind: "broadcast", txid: signedSplitter.id, hex: signedSplitter.hex };
|
|
17613
|
+
await onchainWallet.provider.broadcastTransaction(signedSplitter.hex);
|
|
17614
|
+
}
|
|
17615
|
+
const stepFees = steps.reduce((s, x) => s + x.stepFee, 0);
|
|
17616
|
+
const activeInfos = layout.infos.filter((i) => !i.skipped);
|
|
17617
|
+
const sweepFees = activeInfos.reduce((s, i) => s + (i.sweepFee ?? 0), 0);
|
|
17618
|
+
const recovered = activeInfos.reduce((s, i) => s + (i.value ?? 0) - (i.sweepFee ?? 0), 0);
|
|
17619
|
+
const fundingRequired = splitterFee + steps.reduce((s, x) => s + x.funding, 0);
|
|
17620
|
+
return {
|
|
17621
|
+
version: 1,
|
|
17622
|
+
mode: "funded",
|
|
17623
|
+
network: resolveNetworkName(opts),
|
|
17624
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
17625
|
+
validUntil: layout.validUntil,
|
|
17626
|
+
feeRate,
|
|
17627
|
+
sweepAddress: opts.sweepAddress,
|
|
17628
|
+
totals: {
|
|
17629
|
+
txCount: (broadcastStep ? 1 : 0) + packageSteps.length * 2 + sweepSteps.length,
|
|
17630
|
+
totalFeeSats: splitterFee + stepFees + sweepFees,
|
|
17631
|
+
fundingRequiredSats: fundingRequired,
|
|
17632
|
+
recoveredSats: recovered
|
|
17633
|
+
},
|
|
17634
|
+
vtxos: layout.infos,
|
|
17635
|
+
steps: [...broadcastStep ? [broadcastStep] : [], ...packageSteps, ...sweepSteps]
|
|
17636
|
+
};
|
|
17637
|
+
}
|
|
17638
|
+
|
|
17639
|
+
// src/wallet/exit/types.ts
|
|
17640
|
+
function serializeExitPackage(pkg) {
|
|
17641
|
+
return JSON.stringify(pkg);
|
|
17642
|
+
}
|
|
17643
|
+
function deserializeExitPackage(json) {
|
|
17644
|
+
const data = JSON.parse(json);
|
|
17645
|
+
if (data?.version !== 1) {
|
|
17646
|
+
throw new Error(`unsupported exit package version: ${data?.version}`);
|
|
17647
|
+
}
|
|
17648
|
+
if (typeof data.network !== "string" || typeof data.sweepAddress !== "string") {
|
|
17649
|
+
throw new Error("invalid exit package: missing network or sweepAddress");
|
|
17650
|
+
}
|
|
17651
|
+
if (typeof data.feeRate !== "number" || typeof data.createdAt !== "number") {
|
|
17652
|
+
throw new Error("invalid exit package: missing feeRate or createdAt");
|
|
17653
|
+
}
|
|
17654
|
+
if (!Array.isArray(data.steps) || !Array.isArray(data.vtxos)) {
|
|
17655
|
+
throw new Error("invalid exit package: steps and vtxos must be arrays");
|
|
17656
|
+
}
|
|
17657
|
+
if (!isValidTotals(data.totals)) {
|
|
17658
|
+
throw new Error("invalid exit package: malformed totals");
|
|
17659
|
+
}
|
|
17660
|
+
for (const step of data.steps) {
|
|
17661
|
+
if (!isValidStep(step)) {
|
|
17662
|
+
throw new Error(`invalid step: ${JSON.stringify(step)}`);
|
|
17663
|
+
}
|
|
17664
|
+
}
|
|
17665
|
+
return data;
|
|
17666
|
+
}
|
|
17667
|
+
function isValidDelay(d) {
|
|
17668
|
+
const delay = d;
|
|
17669
|
+
return !!delay && (delay.type === "blocks" || delay.type === "seconds") && typeof delay.value === "number";
|
|
17670
|
+
}
|
|
17671
|
+
function isValidTotals(t) {
|
|
17672
|
+
const totals = t;
|
|
17673
|
+
return !!totals && typeof totals === "object" && typeof totals.txCount === "number" && typeof totals.totalFeeSats === "number" && typeof totals.fundingRequiredSats === "number" && typeof totals.recoveredSats === "number";
|
|
17674
|
+
}
|
|
17675
|
+
function isValidStep(step) {
|
|
17676
|
+
const s = step;
|
|
17677
|
+
if (!s || typeof s !== "object") return false;
|
|
17678
|
+
switch (s.kind) {
|
|
17679
|
+
case "broadcast":
|
|
17680
|
+
return typeof s.txid === "string" && typeof s.hex === "string";
|
|
17681
|
+
case "package":
|
|
17682
|
+
return typeof s.parentTxid === "string" && typeof s.parentHex === "string" && typeof s.childTxid === "string" && typeof s.childHex === "string" && Array.isArray(s.forVtxos);
|
|
17683
|
+
case "bump":
|
|
17684
|
+
return typeof s.parentTxid === "string" && typeof s.parentHex === "string" && Array.isArray(s.forVtxos);
|
|
17685
|
+
case "sweep":
|
|
17686
|
+
return typeof s.vtxo === "string" && typeof s.txid === "string" && typeof s.hex === "string" && typeof s.dependsOnTxid === "string" && isValidDelay(s.delay);
|
|
17687
|
+
default:
|
|
17688
|
+
return false;
|
|
17689
|
+
}
|
|
17690
|
+
}
|
|
17691
|
+
|
|
17692
|
+
// src/wallet/exit/index.ts
|
|
17693
|
+
var UnilateralExit = {
|
|
17694
|
+
estimate,
|
|
17695
|
+
prepare,
|
|
17696
|
+
Executor
|
|
17697
|
+
};
|
|
16791
17698
|
var ARKCONTRACT_PREFIX = "arkcontract";
|
|
16792
17699
|
function encodeArkContract(contract) {
|
|
16793
17700
|
const params = new URLSearchParams();
|
|
@@ -16847,6 +17754,6 @@ function isArkContract(str) {
|
|
|
16847
17754
|
return str.startsWith(ARKCONTRACT_PREFIX + "=");
|
|
16848
17755
|
}
|
|
16849
17756
|
|
|
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-
|
|
17757
|
+
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, decodeArkContract, deserializeAssets, deserializeExitPackage, 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, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
|
|
17758
|
+
//# sourceMappingURL=chunk-FMI2GKYO.js.map
|
|
17759
|
+
//# sourceMappingURL=chunk-FMI2GKYO.js.map
|