@arkade-os/sdk 0.4.42 → 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.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-CKDWpuGL.d.cts → ark-I8jfe5VE.d.cts} +157 -2
- package/dist/{ark-CKDWpuGL.d.ts → ark-I8jfe5VE.d.ts} +157 -2
- package/dist/{asyncStorageTaskQueue-BB2MffwG.d.ts → asyncStorageTaskQueue-BtSOVZFn.d.ts} +2 -2
- package/dist/{asyncStorageTaskQueue-BpN8RRHR.d.cts → asyncStorageTaskQueue-Dap7qUlF.d.cts} +2 -2
- package/dist/{chunk-V3FCNVJE.js → chunk-FMI2GKYO.js} +2158 -502
- package/dist/chunk-FMI2GKYO.js.map +1 -0
- package/dist/{chunk-FLPPSRYH.js → chunk-LCQQPOIT.js} +3 -3
- package/dist/{chunk-FLPPSRYH.js.map → chunk-LCQQPOIT.js.map} +1 -1
- package/dist/{chunk-5X5YMZHB.js → chunk-LJ35HPJW.js} +39 -5
- package/dist/chunk-LJ35HPJW.js.map +1 -0
- package/dist/{chunk-HHPUCCB3.cjs → chunk-RJ6XVCX4.cjs} +7 -7
- package/dist/{chunk-HHPUCCB3.cjs.map → chunk-RJ6XVCX4.cjs.map} +1 -1
- package/dist/{chunk-YEF5FL6E.cjs → chunk-UBYY2TPK.cjs} +39 -4
- package/dist/chunk-UBYY2TPK.cjs.map +1 -0
- package/dist/{chunk-2V3BU2X6.js → chunk-V7WIZR2B.js} +3 -3
- package/dist/{chunk-2V3BU2X6.js.map → chunk-V7WIZR2B.js.map} +1 -1
- package/dist/{chunk-T2ACJD22.cjs → chunk-X37EKDIH.cjs} +11 -11
- package/dist/{chunk-T2ACJD22.cjs.map → chunk-X37EKDIH.cjs.map} +1 -1
- package/dist/{chunk-ED7JDFX4.cjs → chunk-XIVCG6EP.cjs} +2248 -578
- package/dist/chunk-XIVCG6EP.cjs.map +1 -0
- package/dist/contracts/handlers/index.d.cts +3 -3
- package/dist/contracts/handlers/index.d.ts +3 -3
- package/dist/{delegate-yjhcR9n4.d.cts → delegate-BRqbh2fW.d.cts} +1 -1
- package/dist/{delegate-DllAQ3Eu.d.ts → delegate-CgjVjYOP.d.ts} +1 -1
- package/dist/{index-D1te-ZR6.d.cts → index-DHuXlDEg.d.cts} +2 -2
- package/dist/{index-DVdCGyyH.d.ts → index-DvWNB_rl.d.ts} +2 -2
- package/dist/index.cjs +162 -118
- package/dist/index.d.cts +344 -9
- package/dist/index.d.ts +344 -9
- package/dist/index.js +2 -2
- package/dist/repositories/realm/index.cjs +268 -12
- package/dist/repositories/realm/index.cjs.map +1 -1
- package/dist/repositories/realm/index.d.cts +101 -3
- package/dist/repositories/realm/index.d.ts +101 -3
- package/dist/repositories/realm/index.js +254 -4
- package/dist/repositories/realm/index.js.map +1 -1
- package/dist/repositories/sqlite/index.cjs +366 -40
- package/dist/repositories/sqlite/index.cjs.map +1 -1
- package/dist/repositories/sqlite/index.d.cts +52 -6
- package/dist/repositories/sqlite/index.d.ts +52 -6
- package/dist/repositories/sqlite/index.js +353 -32
- package/dist/repositories/sqlite/index.js.map +1 -1
- package/dist/{taskRunner-BdiRy4iI.d.ts → taskRunner-Bptw9UMa.d.ts} +2 -2
- package/dist/{taskRunner-CuTk8RYg.d.cts → taskRunner-CeFxKhSH.d.cts} +2 -2
- package/dist/wallet/expo/background.cjs +12 -12
- package/dist/wallet/expo/background.d.cts +3 -3
- package/dist/wallet/expo/background.d.ts +3 -3
- package/dist/wallet/expo/background.js +4 -4
- package/dist/wallet/expo/index.cjs +12 -12
- package/dist/wallet/expo/index.d.cts +4 -4
- package/dist/wallet/expo/index.d.ts +4 -4
- package/dist/wallet/expo/index.js +3 -3
- package/dist/{wallet-CyPOQ0oM.d.cts → wallet-BsqRU8KM.d.cts} +29 -5
- package/dist/{wallet-CrIVUQMb.d.ts → wallet-D9z7wssl.d.ts} +29 -5
- package/dist/worker/expo/index.cjs +7 -7
- package/dist/worker/expo/index.d.cts +4 -4
- package/dist/worker/expo/index.d.ts +4 -4
- package/dist/worker/expo/index.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-5X5YMZHB.js.map +0 -1
- package/dist/chunk-ED7JDFX4.cjs.map +0 -1
- package/dist/chunk-V3FCNVJE.js.map +0 -1
- package/dist/chunk-YEF5FL6E.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";
|
|
@@ -2644,7 +2677,7 @@ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
|
|
|
2644
2677
|
);
|
|
2645
2678
|
}
|
|
2646
2679
|
function isMigrationCapable(wallet) {
|
|
2647
|
-
return "arkProvider" in wallet && "arkServerPublicKey" in wallet && "onchainProvider" in wallet && typeof wallet.rotateServerSigner === "function" && typeof wallet.sendSelectedVtxosToSelf === "function" && typeof wallet.getBoardingUtxosForSigners === "function";
|
|
2680
|
+
return "arkProvider" in wallet && "arkServerPublicKey" in wallet && "onchainProvider" in wallet && typeof wallet.rotateServerSigner === "function" && typeof wallet.refreshDeprecatedSigners === "function" && typeof wallet.sendSelectedVtxosToSelf === "function" && typeof wallet.getBoardingUtxosForSigners === "function";
|
|
2648
2681
|
}
|
|
2649
2682
|
function classifiedToRef(c) {
|
|
2650
2683
|
return {
|
|
@@ -3168,6 +3201,7 @@ var VtxoManager = class _VtxoManager {
|
|
|
3168
3201
|
async migrateCore(options) {
|
|
3169
3202
|
const wallet = this.requireMigrationCapableWallet();
|
|
3170
3203
|
const info = await wallet.arkProvider.getInfo();
|
|
3204
|
+
wallet.refreshDeprecatedSigners(info);
|
|
3171
3205
|
const signerSet = signerSetFromInfo(info);
|
|
3172
3206
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
3173
3207
|
const walletSignerHex = hex.encode(wallet.arkServerPublicKey);
|
|
@@ -4136,6 +4170,123 @@ function readUInt32BE(array, offset) {
|
|
|
4136
4170
|
function noteTapscript(preimageHash) {
|
|
4137
4171
|
return Script.encode(["SHA256", preimageHash, "EQUAL"]);
|
|
4138
4172
|
}
|
|
4173
|
+
|
|
4174
|
+
// src/repositories/intentRepository.ts
|
|
4175
|
+
var ALL_INTENT_STATES = [
|
|
4176
|
+
"waiting_to_submit",
|
|
4177
|
+
"waiting_for_batch",
|
|
4178
|
+
"batch_in_progress",
|
|
4179
|
+
"batch_failed",
|
|
4180
|
+
"batch_succeeded",
|
|
4181
|
+
"cancelled"
|
|
4182
|
+
];
|
|
4183
|
+
var INTENT_TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
4184
|
+
"batch_failed",
|
|
4185
|
+
"batch_succeeded",
|
|
4186
|
+
"cancelled"
|
|
4187
|
+
]);
|
|
4188
|
+
function isTerminalIntentState(s) {
|
|
4189
|
+
return INTENT_TERMINAL_STATES.has(s);
|
|
4190
|
+
}
|
|
4191
|
+
function assertIntentIdUnique(intent, existing) {
|
|
4192
|
+
if (intent.intentId === void 0) return;
|
|
4193
|
+
for (const e of existing) {
|
|
4194
|
+
if (e.intentTxId !== intent.intentTxId && e.intentId === intent.intentId) {
|
|
4195
|
+
throw new Error(
|
|
4196
|
+
`intentId "${intent.intentId}" is already used by intent ${e.intentTxId}`
|
|
4197
|
+
);
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
function intentMatchesFilter(i, f) {
|
|
4202
|
+
if (f.intentTxIds && !f.intentTxIds.includes(i.intentTxId)) return false;
|
|
4203
|
+
if (f.intentIds && (!i.intentId || !f.intentIds.includes(i.intentId))) return false;
|
|
4204
|
+
if (f.states && !f.states.includes(i.state)) return false;
|
|
4205
|
+
if (f.containingInputs) {
|
|
4206
|
+
const keys = new Set(i.intentVtxos.map((o) => `${o.txid}:${o.vout}`));
|
|
4207
|
+
if (!f.containingInputs.some((o) => keys.has(`${o.txid}:${o.vout}`))) return false;
|
|
4208
|
+
}
|
|
4209
|
+
if (f.validAt !== void 0) {
|
|
4210
|
+
if (i.validFrom !== void 0 && f.validAt < i.validFrom) return false;
|
|
4211
|
+
if (i.validUntil !== void 0 && f.validAt > i.validUntil) return false;
|
|
4212
|
+
}
|
|
4213
|
+
if (f.searchText) {
|
|
4214
|
+
const hay = [i.intentId, i.batchId, i.commitmentTransactionId].filter(Boolean).join(" ");
|
|
4215
|
+
if (!hay.includes(f.searchText)) return false;
|
|
4216
|
+
}
|
|
4217
|
+
return true;
|
|
4218
|
+
}
|
|
4219
|
+
function intentPageBounds(filter, total) {
|
|
4220
|
+
const skip = Math.max(0, filter?.skip ?? 0);
|
|
4221
|
+
const take = filter?.take === void 0 ? total : Math.max(0, filter.take);
|
|
4222
|
+
return { skip, end: skip + take };
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
// src/wallet/intentStateReducer.ts
|
|
4226
|
+
function reduceIntentState(current, event) {
|
|
4227
|
+
if (isTerminalIntentState(current)) return current;
|
|
4228
|
+
switch (event.type) {
|
|
4229
|
+
case "batch_started" /* BatchStarted */:
|
|
4230
|
+
return "batch_in_progress";
|
|
4231
|
+
case "batch_finalized" /* BatchFinalized */:
|
|
4232
|
+
return "batch_succeeded";
|
|
4233
|
+
case "batch_failed" /* BatchFailed */:
|
|
4234
|
+
return "batch_failed";
|
|
4235
|
+
default:
|
|
4236
|
+
return current;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
function applySettlementEventToIntent(intent, event) {
|
|
4240
|
+
const next = reduceIntentState(intent.state, event);
|
|
4241
|
+
if (next === intent.state) return void 0;
|
|
4242
|
+
const updated = { ...intent, state: next };
|
|
4243
|
+
if ("id" in event && typeof event.id === "string") {
|
|
4244
|
+
updated.batchId = event.id;
|
|
4245
|
+
}
|
|
4246
|
+
if (event.type === "batch_finalized" /* BatchFinalized */) {
|
|
4247
|
+
updated.commitmentTransactionId = event.commitmentTxid;
|
|
4248
|
+
}
|
|
4249
|
+
if (event.type === "batch_failed" /* BatchFailed */) {
|
|
4250
|
+
updated.cancellationReason = event.reason;
|
|
4251
|
+
}
|
|
4252
|
+
return updated;
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
// src/wallet/intentPersistenceHandler.ts
|
|
4256
|
+
async function persistTransition(deps, event) {
|
|
4257
|
+
const repo = deps.intentRepository;
|
|
4258
|
+
if (!repo) return;
|
|
4259
|
+
try {
|
|
4260
|
+
const cur = (await repo.getIntents({ intentTxIds: [deps.intentTxId] }))[0];
|
|
4261
|
+
if (!cur) return;
|
|
4262
|
+
const next = applySettlementEventToIntent(cur, event);
|
|
4263
|
+
if (next) await repo.saveIntent(next);
|
|
4264
|
+
} catch (e) {
|
|
4265
|
+
console.error(`Failed to apply settlement event to intent ${deps.intentTxId}`, e);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
function wrapHandlerWithIntentPersistence(base, deps) {
|
|
4269
|
+
return {
|
|
4270
|
+
...base,
|
|
4271
|
+
onBatchStarted: async (event) => {
|
|
4272
|
+
const result = await base.onBatchStarted(event);
|
|
4273
|
+
if (!result.skip) await persistTransition(deps, event);
|
|
4274
|
+
return result;
|
|
4275
|
+
},
|
|
4276
|
+
onBatchFinalized: async (event) => {
|
|
4277
|
+
if (base.onBatchFinalized) await base.onBatchFinalized(event);
|
|
4278
|
+
await persistTransition(deps, event);
|
|
4279
|
+
},
|
|
4280
|
+
onBatchFailed: async (event) => {
|
|
4281
|
+
await persistTransition(deps, event);
|
|
4282
|
+
if (base.onBatchFailed) {
|
|
4283
|
+
await base.onBatchFailed(event);
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
throw new Error(event.reason);
|
|
4287
|
+
}
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4139
4290
|
var TxTree = class {
|
|
4140
4291
|
constructor(root, children = /* @__PURE__ */ new Map()) {
|
|
4141
4292
|
this.root = root;
|
|
@@ -5245,201 +5396,199 @@ function isAnnotated(v) {
|
|
|
5245
5396
|
return v.tapTree !== void 0 && v.forfeitTapLeafScript !== void 0 && v.intentTapLeafScript !== void 0;
|
|
5246
5397
|
}
|
|
5247
5398
|
|
|
5248
|
-
// src/
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
const rejected = [];
|
|
5261
|
-
for (const v of vtxos) {
|
|
5262
|
-
if (isVtxoForScript(v, script)) {
|
|
5263
|
-
matches.push(v);
|
|
5264
|
-
} else {
|
|
5265
|
-
rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
if (rejected.length > 0) {
|
|
5269
|
-
console.warn(
|
|
5270
|
-
`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`
|
|
5271
|
-
);
|
|
5272
|
-
}
|
|
5273
|
-
return matches;
|
|
5274
|
-
}
|
|
5275
|
-
function validateVtxosForScript(vtxos, script, context) {
|
|
5276
|
-
const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
|
|
5277
|
-
if (mismatches.length === 0) return;
|
|
5278
|
-
const detail = mismatches.map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`).join(", ");
|
|
5279
|
-
throw new Error(
|
|
5280
|
-
`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`
|
|
5281
|
-
);
|
|
5282
|
-
}
|
|
5283
|
-
async function getVtxosForContract(repo, contract) {
|
|
5284
|
-
return repo.getVtxosForScript ? repo.getVtxosForScript(contract.script) : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
|
|
5285
|
-
}
|
|
5286
|
-
async function saveVtxosForContract(repo, contract, vtxos) {
|
|
5287
|
-
if (repo.saveVtxosForScript) {
|
|
5288
|
-
return repo.saveVtxosForScript(
|
|
5289
|
-
{ script: contract.script, address: contract.address },
|
|
5290
|
-
vtxos
|
|
5291
|
-
);
|
|
5292
|
-
}
|
|
5293
|
-
validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
|
|
5294
|
-
return repo.saveVtxos(contract.address, vtxos);
|
|
5399
|
+
// src/repositories/virtualTxRepository.ts
|
|
5400
|
+
var ChainedTxType = /* @__PURE__ */ ((ChainedTxType2) => {
|
|
5401
|
+
ChainedTxType2[ChainedTxType2["Unspecified"] = 0] = "Unspecified";
|
|
5402
|
+
ChainedTxType2[ChainedTxType2["Commitment"] = 1] = "Commitment";
|
|
5403
|
+
ChainedTxType2[ChainedTxType2["Ark"] = 2] = "Ark";
|
|
5404
|
+
ChainedTxType2[ChainedTxType2["Tree"] = 3] = "Tree";
|
|
5405
|
+
ChainedTxType2[ChainedTxType2["Checkpoint"] = 4] = "Checkpoint";
|
|
5406
|
+
return ChainedTxType2;
|
|
5407
|
+
})(ChainedTxType || {});
|
|
5408
|
+
function mergeChainedTxType(incoming, prev) {
|
|
5409
|
+
if (incoming !== 0 /* Unspecified */) return incoming;
|
|
5410
|
+
return prev ?? 0 /* Unspecified */;
|
|
5295
5411
|
}
|
|
5296
5412
|
|
|
5297
|
-
// src/repositories/inMemory/
|
|
5298
|
-
var
|
|
5413
|
+
// src/repositories/inMemory/intentRepository.ts
|
|
5414
|
+
var InMemoryIntentRepository = class {
|
|
5299
5415
|
version = 1;
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
async
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
|
|
5416
|
+
byId = /* @__PURE__ */ new Map();
|
|
5417
|
+
async clear() {
|
|
5418
|
+
this.byId.clear();
|
|
5419
|
+
}
|
|
5420
|
+
async saveIntent(intent) {
|
|
5421
|
+
assertIntentIdUnique(intent, this.byId.values());
|
|
5422
|
+
this.byId.set(intent.intentTxId, {
|
|
5423
|
+
...intent,
|
|
5424
|
+
intentVtxos: [...intent.intentVtxos],
|
|
5425
|
+
partialForfeits: [...intent.partialForfeits],
|
|
5426
|
+
updatedAt: Date.now()
|
|
5427
|
+
});
|
|
5311
5428
|
}
|
|
5312
|
-
async
|
|
5313
|
-
this.
|
|
5429
|
+
async getIntents(filter) {
|
|
5430
|
+
let out = [...this.byId.values()];
|
|
5431
|
+
if (filter) out = out.filter((i) => intentMatchesFilter(i, filter));
|
|
5432
|
+
out.sort((a, b) => a.createdAt - b.createdAt || a.intentTxId.localeCompare(b.intentTxId));
|
|
5433
|
+
const { skip, end } = intentPageBounds(filter, out.length);
|
|
5434
|
+
return out.slice(skip, end).map(clone);
|
|
5314
5435
|
}
|
|
5315
|
-
async
|
|
5316
|
-
const
|
|
5317
|
-
for (const
|
|
5318
|
-
for (const
|
|
5319
|
-
|
|
5320
|
-
allMatches.push(vtxo);
|
|
5321
|
-
}
|
|
5322
|
-
}
|
|
5323
|
-
}
|
|
5324
|
-
return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
|
|
5436
|
+
async getLockedVtxoOutpoints() {
|
|
5437
|
+
const out = [];
|
|
5438
|
+
for (const i of this.byId.values())
|
|
5439
|
+
if (!isTerminalIntentState(i.state)) for (const o of i.intentVtxos) out.push({ ...o });
|
|
5440
|
+
return out;
|
|
5325
5441
|
}
|
|
5326
|
-
async
|
|
5327
|
-
if (!key.address) {
|
|
5328
|
-
throw new Error("InMemoryWalletRepository requires an address");
|
|
5329
|
-
}
|
|
5330
|
-
for (const vtxo of vtxos) {
|
|
5331
|
-
if (!isVtxoForScript(vtxo, key.script)) {
|
|
5332
|
-
throw new Error(
|
|
5333
|
-
`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`
|
|
5334
|
-
);
|
|
5335
|
-
}
|
|
5336
|
-
}
|
|
5337
|
-
return this.saveVtxos(key.address, vtxos);
|
|
5442
|
+
async [Symbol.asyncDispose]() {
|
|
5338
5443
|
}
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5444
|
+
};
|
|
5445
|
+
var clone = (i) => ({
|
|
5446
|
+
...i,
|
|
5447
|
+
intentVtxos: i.intentVtxos.map((o) => ({ ...o })),
|
|
5448
|
+
partialForfeits: [...i.partialForfeits]
|
|
5449
|
+
});
|
|
5450
|
+
|
|
5451
|
+
// src/repositories/inMemory/virtualTxRepository.ts
|
|
5452
|
+
var opKey = (o) => `${o.txid}:${o.vout}`;
|
|
5453
|
+
var InMemoryVirtualTxRepository = class {
|
|
5454
|
+
version = 1;
|
|
5455
|
+
txs = /* @__PURE__ */ new Map();
|
|
5456
|
+
branches = /* @__PURE__ */ new Map();
|
|
5457
|
+
async clear() {
|
|
5458
|
+
this.txs.clear();
|
|
5459
|
+
this.branches.clear();
|
|
5460
|
+
}
|
|
5461
|
+
async upsertVirtualTxs(txs) {
|
|
5462
|
+
for (const t of txs) {
|
|
5463
|
+
const prev = this.txs.get(t.txid);
|
|
5464
|
+
if (!prev) {
|
|
5465
|
+
this.txs.set(t.txid, { ...t });
|
|
5466
|
+
continue;
|
|
5346
5467
|
}
|
|
5468
|
+
this.txs.set(t.txid, {
|
|
5469
|
+
txid: t.txid,
|
|
5470
|
+
psbt: t.psbt ?? prev.psbt,
|
|
5471
|
+
expiresAt: t.expiresAt ?? prev.expiresAt,
|
|
5472
|
+
type: mergeChainedTxType(t.type, prev.type)
|
|
5473
|
+
});
|
|
5347
5474
|
}
|
|
5348
5475
|
}
|
|
5349
|
-
async
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
async saveUtxos(address, utxos) {
|
|
5353
|
-
const existing = this.utxosByAddress.get(address) ?? [];
|
|
5354
|
-
const next = mergeByKey(existing, utxos, (item) => `${item.txid}:${item.vout}`);
|
|
5355
|
-
this.utxosByAddress.set(address, next);
|
|
5356
|
-
}
|
|
5357
|
-
async deleteUtxos(address) {
|
|
5358
|
-
this.utxosByAddress.delete(address);
|
|
5359
|
-
}
|
|
5360
|
-
async getTransactionHistory(address) {
|
|
5361
|
-
return this.txsByAddress.get(address) ?? [];
|
|
5362
|
-
}
|
|
5363
|
-
async saveTransactions(address, txs) {
|
|
5364
|
-
const existing = this.txsByAddress.get(address) ?? [];
|
|
5365
|
-
const next = mergeByKey(existing, txs, serializeTxKey);
|
|
5366
|
-
this.txsByAddress.set(address, next);
|
|
5476
|
+
async getVirtualTx(txid) {
|
|
5477
|
+
const t = this.txs.get(txid);
|
|
5478
|
+
return t ? { ...t } : null;
|
|
5367
5479
|
}
|
|
5368
|
-
async
|
|
5369
|
-
this.
|
|
5480
|
+
async setBranch(vtxo, branch) {
|
|
5481
|
+
this.branches.set(
|
|
5482
|
+
opKey(vtxo),
|
|
5483
|
+
[...branch].sort((a, b) => a.position - b.position)
|
|
5484
|
+
);
|
|
5370
5485
|
}
|
|
5371
|
-
async
|
|
5372
|
-
|
|
5486
|
+
async getBranch(vtxo) {
|
|
5487
|
+
const b = this.branches.get(opKey(vtxo)) ?? [];
|
|
5488
|
+
return b.map((x) => this.txs.get(x.virtualTxid)).filter((t) => !!t).map((t) => ({ ...t }));
|
|
5373
5489
|
}
|
|
5374
|
-
async
|
|
5375
|
-
this.
|
|
5490
|
+
async hasBranch(vtxo) {
|
|
5491
|
+
return (this.branches.get(opKey(vtxo)) ?? []).length > 0;
|
|
5376
5492
|
}
|
|
5377
|
-
async
|
|
5378
|
-
this.
|
|
5379
|
-
this.
|
|
5380
|
-
|
|
5381
|
-
this.
|
|
5493
|
+
async pruneForSpentVtxo(vtxo) {
|
|
5494
|
+
const removed = this.branches.get(opKey(vtxo)) ?? [];
|
|
5495
|
+
this.branches.delete(opKey(vtxo));
|
|
5496
|
+
const stillReferenced = /* @__PURE__ */ new Set();
|
|
5497
|
+
for (const b of this.branches.values())
|
|
5498
|
+
for (const e of b) stillReferenced.add(e.virtualTxid);
|
|
5499
|
+
for (const e of removed)
|
|
5500
|
+
if (!stillReferenced.has(e.virtualTxid)) this.txs.delete(e.virtualTxid);
|
|
5382
5501
|
}
|
|
5383
5502
|
async [Symbol.asyncDispose]() {
|
|
5384
|
-
return;
|
|
5385
5503
|
}
|
|
5386
5504
|
};
|
|
5387
|
-
function serializeTxKey(tx) {
|
|
5388
|
-
const key = tx.key;
|
|
5389
|
-
return `${key.boardingTxid}:${key.commitmentTxid}:${key.arkTxid}`;
|
|
5390
|
-
}
|
|
5391
|
-
function mergeByKey(existing, incoming, toKey) {
|
|
5392
|
-
const next = /* @__PURE__ */ new Map();
|
|
5393
|
-
existing.forEach((item) => {
|
|
5394
|
-
next.set(toKey(item), item);
|
|
5395
|
-
});
|
|
5396
|
-
incoming.forEach((item) => {
|
|
5397
|
-
next.set(toKey(item), item);
|
|
5398
|
-
});
|
|
5399
|
-
return Array.from(next.values());
|
|
5400
|
-
}
|
|
5401
5505
|
|
|
5402
|
-
// src/repositories/
|
|
5403
|
-
var
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
if (
|
|
5417
|
-
return
|
|
5506
|
+
// src/repositories/indexedDB/idbUtils.ts
|
|
5507
|
+
var promisifyRequest = (request) => new Promise((resolve, reject) => {
|
|
5508
|
+
request.onsuccess = () => resolve(request.result);
|
|
5509
|
+
request.onerror = () => reject(request.error);
|
|
5510
|
+
});
|
|
5511
|
+
var awaitTransaction = (transaction) => new Promise((resolve, reject) => {
|
|
5512
|
+
transaction.oncomplete = () => resolve();
|
|
5513
|
+
transaction.onerror = () => reject(transaction.error);
|
|
5514
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("transaction aborted"));
|
|
5515
|
+
});
|
|
5516
|
+
|
|
5517
|
+
// src/repositories/indexedDB/manager.ts
|
|
5518
|
+
function getGlobalObject() {
|
|
5519
|
+
if (typeof globalThis !== "undefined") {
|
|
5520
|
+
if (typeof globalThis.self === "object" && globalThis.self !== null) {
|
|
5521
|
+
return { globalObject: globalThis.self };
|
|
5418
5522
|
}
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
return true;
|
|
5422
|
-
}
|
|
5423
|
-
return Array.isArray(criterion) ? criterion.includes(value) : value === criterion;
|
|
5424
|
-
};
|
|
5425
|
-
const results = [];
|
|
5426
|
-
for (const contract of contracts) {
|
|
5427
|
-
if (matches(contract.script, filter.script) && matches(contract.state, filter.state) && matches(contract.type, filter.type)) {
|
|
5428
|
-
results.push(contract);
|
|
5429
|
-
}
|
|
5523
|
+
if (typeof globalThis.window === "object" && globalThis.window !== null) {
|
|
5524
|
+
return { globalObject: globalThis.window };
|
|
5430
5525
|
}
|
|
5431
|
-
return
|
|
5526
|
+
return { globalObject: globalThis };
|
|
5432
5527
|
}
|
|
5433
|
-
|
|
5434
|
-
|
|
5528
|
+
throw new Error("Global object not found");
|
|
5529
|
+
}
|
|
5530
|
+
var dbCache = /* @__PURE__ */ new Map();
|
|
5531
|
+
var refCounts = /* @__PURE__ */ new Map();
|
|
5532
|
+
async function openDatabase(dbName, dbVersion, initDatabase2) {
|
|
5533
|
+
const { globalObject } = getGlobalObject();
|
|
5534
|
+
if (!globalObject.indexedDB) {
|
|
5535
|
+
throw new Error("IndexedDB is not available in this environment");
|
|
5435
5536
|
}
|
|
5436
|
-
|
|
5437
|
-
|
|
5537
|
+
const cached = dbCache.get(dbName);
|
|
5538
|
+
if (cached) {
|
|
5539
|
+
if (cached.version !== dbVersion) {
|
|
5540
|
+
throw new Error(
|
|
5541
|
+
`Database "${dbName}" already opened with version ${cached.version}; requested ${dbVersion}`
|
|
5542
|
+
);
|
|
5543
|
+
}
|
|
5544
|
+
refCounts.set(dbName, (refCounts.get(dbName) ?? 0) + 1);
|
|
5545
|
+
return cached.promise;
|
|
5438
5546
|
}
|
|
5439
|
-
|
|
5440
|
-
|
|
5547
|
+
const dbPromise = new Promise((resolve, reject) => {
|
|
5548
|
+
const request = globalObject.indexedDB.open(dbName, dbVersion);
|
|
5549
|
+
request.onerror = () => {
|
|
5550
|
+
dbCache.delete(dbName);
|
|
5551
|
+
refCounts.delete(dbName);
|
|
5552
|
+
reject(request.error);
|
|
5553
|
+
};
|
|
5554
|
+
request.onsuccess = () => {
|
|
5555
|
+
const db = request.result;
|
|
5556
|
+
db.onversionchange = () => {
|
|
5557
|
+
db.close();
|
|
5558
|
+
dbCache.delete(dbName);
|
|
5559
|
+
refCounts.delete(dbName);
|
|
5560
|
+
};
|
|
5561
|
+
resolve(db);
|
|
5562
|
+
};
|
|
5563
|
+
request.onupgradeneeded = (event) => {
|
|
5564
|
+
const db = request.result;
|
|
5565
|
+
initDatabase2(db, event.oldVersion, request.transaction);
|
|
5566
|
+
};
|
|
5567
|
+
request.onblocked = () => {
|
|
5568
|
+
console.warn("Database upgrade blocked - close other tabs/connections");
|
|
5569
|
+
};
|
|
5570
|
+
});
|
|
5571
|
+
dbCache.set(dbName, { version: dbVersion, promise: dbPromise });
|
|
5572
|
+
refCounts.set(dbName, 1);
|
|
5573
|
+
return dbPromise;
|
|
5574
|
+
}
|
|
5575
|
+
async function closeDatabase(dbName) {
|
|
5576
|
+
const cachedEntry = dbCache.get(dbName);
|
|
5577
|
+
if (!cachedEntry) return false;
|
|
5578
|
+
const count = (refCounts.get(dbName) ?? 1) - 1;
|
|
5579
|
+
if (count > 0) {
|
|
5580
|
+
refCounts.set(dbName, count);
|
|
5581
|
+
return false;
|
|
5441
5582
|
}
|
|
5442
|
-
|
|
5583
|
+
refCounts.delete(dbName);
|
|
5584
|
+
dbCache.delete(dbName);
|
|
5585
|
+
try {
|
|
5586
|
+
const db = await cachedEntry.promise;
|
|
5587
|
+
db.close();
|
|
5588
|
+
} catch {
|
|
5589
|
+
}
|
|
5590
|
+
return true;
|
|
5591
|
+
}
|
|
5443
5592
|
function scriptFromArkAddress(address) {
|
|
5444
5593
|
return hex.encode(ArkAddress.decode(address).pkScript);
|
|
5445
5594
|
}
|
|
@@ -5450,8 +5599,12 @@ var STORE_UTXOS = "utxos";
|
|
|
5450
5599
|
var STORE_TRANSACTIONS = "transactions";
|
|
5451
5600
|
var STORE_WALLET_STATE = "walletState";
|
|
5452
5601
|
var STORE_CONTRACTS = "contracts";
|
|
5602
|
+
var STORE_INTENTS = "intents";
|
|
5603
|
+
var STORE_VIRTUAL_TXS = "virtualTxs";
|
|
5604
|
+
var STORE_VTXO_BRANCHES = "vtxoBranches";
|
|
5453
5605
|
var LEGACY_STORE_CONTRACT_COLLECTIONS = "contractsCollections";
|
|
5454
5606
|
var DB_VERSION = 3;
|
|
5607
|
+
var INTENT_DB_VERSION = 5;
|
|
5455
5608
|
function initDatabase(db, oldVersion, transaction) {
|
|
5456
5609
|
if (!db.objectStoreNames.contains(STORE_VTXOS)) {
|
|
5457
5610
|
const vtxosStore = db.createObjectStore(STORE_VTXOS, {
|
|
@@ -5603,20 +5756,493 @@ function initDatabase(db, oldVersion, transaction) {
|
|
|
5603
5756
|
backfillVtxoScripts(transaction);
|
|
5604
5757
|
}
|
|
5605
5758
|
}
|
|
5606
|
-
function
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5759
|
+
function initDatabaseWithIntents(db, oldVersion, transaction) {
|
|
5760
|
+
initDatabase(db, oldVersion, transaction);
|
|
5761
|
+
if (!db.objectStoreNames.contains(STORE_INTENTS)) {
|
|
5762
|
+
const intentsStore = db.createObjectStore(STORE_INTENTS, {
|
|
5763
|
+
keyPath: "intentTxId"
|
|
5764
|
+
});
|
|
5765
|
+
intentsStore.createIndex("intentId", "intentId", { unique: true });
|
|
5766
|
+
intentsStore.createIndex("state", "state", { unique: false });
|
|
5767
|
+
}
|
|
5768
|
+
if (!db.objectStoreNames.contains(STORE_VIRTUAL_TXS)) {
|
|
5769
|
+
db.createObjectStore(STORE_VIRTUAL_TXS, { keyPath: "txid" });
|
|
5770
|
+
}
|
|
5771
|
+
if (!db.objectStoreNames.contains(STORE_VTXO_BRANCHES)) {
|
|
5772
|
+
const branchesStore = db.createObjectStore(STORE_VTXO_BRANCHES, {
|
|
5773
|
+
keyPath: ["vtxoTxid", "vtxoVout", "position"]
|
|
5774
|
+
});
|
|
5775
|
+
branchesStore.createIndex("vtxo", ["vtxoTxid", "vtxoVout"], { unique: false });
|
|
5776
|
+
branchesStore.createIndex("virtualTxid", "virtualTxid", { unique: false });
|
|
5777
|
+
}
|
|
5778
|
+
if (oldVersion === 4 && transaction) {
|
|
5779
|
+
const intentsStore = transaction.objectStore(STORE_INTENTS);
|
|
5780
|
+
if (intentsStore.indexNames.contains("intentId")) {
|
|
5781
|
+
intentsStore.deleteIndex("intentId");
|
|
5782
|
+
}
|
|
5783
|
+
dedupeIntentIds(intentsStore, () => {
|
|
5784
|
+
intentsStore.createIndex("intentId", "intentId", { unique: true });
|
|
5785
|
+
});
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
function dedupeIntentIds(store, onComplete) {
|
|
5789
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5790
|
+
const cursorRequest = store.openCursor();
|
|
5791
|
+
cursorRequest.onsuccess = () => {
|
|
5792
|
+
const cursor = cursorRequest.result;
|
|
5793
|
+
if (!cursor) {
|
|
5794
|
+
onComplete();
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
const id = cursor.value.intentId;
|
|
5798
|
+
if (id != null) {
|
|
5799
|
+
if (seen.has(id)) {
|
|
5800
|
+
cursor.delete();
|
|
5801
|
+
} else {
|
|
5802
|
+
seen.add(id);
|
|
5803
|
+
}
|
|
5804
|
+
}
|
|
5805
|
+
cursor.continue();
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
5808
|
+
function backfillVtxoScripts(transaction) {
|
|
5809
|
+
const store = transaction.objectStore(STORE_VTXOS);
|
|
5810
|
+
const cursorRequest = store.openCursor();
|
|
5811
|
+
cursorRequest.onsuccess = () => {
|
|
5812
|
+
const cursor = cursorRequest.result;
|
|
5813
|
+
if (!cursor) return;
|
|
5612
5814
|
const value = cursor.value;
|
|
5613
5815
|
if (!value.script) {
|
|
5614
5816
|
value.script = scriptFromArkAddress(value.address);
|
|
5615
5817
|
cursor.update(value);
|
|
5616
5818
|
}
|
|
5617
|
-
cursor.continue();
|
|
5618
|
-
};
|
|
5619
|
-
}
|
|
5819
|
+
cursor.continue();
|
|
5820
|
+
};
|
|
5821
|
+
}
|
|
5822
|
+
|
|
5823
|
+
// src/worker/browser/utils.ts
|
|
5824
|
+
var DEFAULT_DB_NAME = "arkade-service-worker";
|
|
5825
|
+
var DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = 1e4;
|
|
5826
|
+
function normalizeOptions2(pathOrOptions) {
|
|
5827
|
+
if (typeof pathOrOptions === "string") {
|
|
5828
|
+
return {
|
|
5829
|
+
path: pathOrOptions,
|
|
5830
|
+
activationTimeoutMs: DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS
|
|
5831
|
+
};
|
|
5832
|
+
}
|
|
5833
|
+
return {
|
|
5834
|
+
path: pathOrOptions.path,
|
|
5835
|
+
activationTimeoutMs: pathOrOptions.activationTimeoutMs ?? DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS
|
|
5836
|
+
};
|
|
5837
|
+
}
|
|
5838
|
+
function waitForServiceWorkerReady(timeoutMs) {
|
|
5839
|
+
return new Promise((resolve, reject) => {
|
|
5840
|
+
const timeoutId = setTimeout(() => {
|
|
5841
|
+
reject(new Error(`Service worker activation timed out after ${timeoutMs}ms`));
|
|
5842
|
+
}, timeoutMs);
|
|
5843
|
+
navigator.serviceWorker.ready.then((registration) => {
|
|
5844
|
+
clearTimeout(timeoutId);
|
|
5845
|
+
resolve(registration);
|
|
5846
|
+
}).catch((error) => {
|
|
5847
|
+
clearTimeout(timeoutId);
|
|
5848
|
+
reject(error);
|
|
5849
|
+
});
|
|
5850
|
+
});
|
|
5851
|
+
}
|
|
5852
|
+
async function setupServiceWorker(pathOrOptions) {
|
|
5853
|
+
if (!("serviceWorker" in navigator)) {
|
|
5854
|
+
throw new Error("Service workers are not supported in this browser");
|
|
5855
|
+
}
|
|
5856
|
+
const { path, activationTimeoutMs } = normalizeOptions2(pathOrOptions);
|
|
5857
|
+
const registration = await navigator.serviceWorker.register(path);
|
|
5858
|
+
await registration.update();
|
|
5859
|
+
const serviceWorker = registration.active || registration.waiting || registration.installing;
|
|
5860
|
+
if (!serviceWorker) {
|
|
5861
|
+
throw new Error("Failed to get service worker instance");
|
|
5862
|
+
}
|
|
5863
|
+
if (serviceWorker.state === "activated") {
|
|
5864
|
+
return serviceWorker;
|
|
5865
|
+
}
|
|
5866
|
+
const readyRegistration = await waitForServiceWorkerReady(activationTimeoutMs);
|
|
5867
|
+
if (!readyRegistration.active) {
|
|
5868
|
+
throw new Error("Service worker registration is ready but has no active worker");
|
|
5869
|
+
}
|
|
5870
|
+
return readyRegistration.active;
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
// src/repositories/indexedDB/virtualTxRepository.ts
|
|
5874
|
+
var IndexedDBVirtualTxRepository = class {
|
|
5875
|
+
constructor(dbName = DEFAULT_DB_NAME) {
|
|
5876
|
+
this.dbName = dbName;
|
|
5877
|
+
}
|
|
5878
|
+
version = 1;
|
|
5879
|
+
db = null;
|
|
5880
|
+
async getDB() {
|
|
5881
|
+
if (!this.db)
|
|
5882
|
+
this.db = await openDatabase(this.dbName, INTENT_DB_VERSION, initDatabaseWithIntents);
|
|
5883
|
+
return this.db;
|
|
5884
|
+
}
|
|
5885
|
+
async clear() {
|
|
5886
|
+
const db = await this.getDB();
|
|
5887
|
+
const transaction = db.transaction([STORE_VIRTUAL_TXS, STORE_VTXO_BRANCHES], "readwrite");
|
|
5888
|
+
transaction.objectStore(STORE_VIRTUAL_TXS).clear();
|
|
5889
|
+
transaction.objectStore(STORE_VTXO_BRANCHES).clear();
|
|
5890
|
+
await awaitTransaction(transaction);
|
|
5891
|
+
}
|
|
5892
|
+
async upsertVirtualTxs(txs) {
|
|
5893
|
+
if (txs.length === 0) return;
|
|
5894
|
+
const db = await this.getDB();
|
|
5895
|
+
const merged = /* @__PURE__ */ new Map();
|
|
5896
|
+
for (const tx of txs) {
|
|
5897
|
+
const acc = merged.get(tx.txid);
|
|
5898
|
+
merged.set(
|
|
5899
|
+
tx.txid,
|
|
5900
|
+
acc ? {
|
|
5901
|
+
txid: tx.txid,
|
|
5902
|
+
psbt: tx.psbt ?? acc.psbt,
|
|
5903
|
+
expiresAt: tx.expiresAt ?? acc.expiresAt,
|
|
5904
|
+
type: mergeChainedTxType(tx.type, acc.type)
|
|
5905
|
+
} : tx
|
|
5906
|
+
);
|
|
5907
|
+
}
|
|
5908
|
+
const transaction = db.transaction([STORE_VIRTUAL_TXS], "readwrite");
|
|
5909
|
+
const store = transaction.objectStore(STORE_VIRTUAL_TXS);
|
|
5910
|
+
for (const tx of merged.values()) {
|
|
5911
|
+
const getReq = store.get(tx.txid);
|
|
5912
|
+
getReq.onsuccess = () => {
|
|
5913
|
+
const prev = getReq.result;
|
|
5914
|
+
store.put({
|
|
5915
|
+
txid: tx.txid,
|
|
5916
|
+
psbt: tx.psbt ?? prev?.psbt ?? null,
|
|
5917
|
+
expiresAt: tx.expiresAt ?? prev?.expiresAt ?? null,
|
|
5918
|
+
type: mergeChainedTxType(tx.type, prev?.type)
|
|
5919
|
+
});
|
|
5920
|
+
};
|
|
5921
|
+
}
|
|
5922
|
+
await awaitTransaction(transaction);
|
|
5923
|
+
}
|
|
5924
|
+
async getVirtualTx(txid) {
|
|
5925
|
+
const db = await this.getDB();
|
|
5926
|
+
const store = db.transaction([STORE_VIRTUAL_TXS], "readonly").objectStore(STORE_VIRTUAL_TXS);
|
|
5927
|
+
const r = await promisifyRequest(store.get(txid));
|
|
5928
|
+
return r ?? null;
|
|
5929
|
+
}
|
|
5930
|
+
async setBranch(vtxo, branch) {
|
|
5931
|
+
const db = await this.getDB();
|
|
5932
|
+
const transaction = db.transaction([STORE_VTXO_BRANCHES], "readwrite");
|
|
5933
|
+
const store = transaction.objectStore(STORE_VTXO_BRANCHES);
|
|
5934
|
+
const getAllReq = store.index("vtxo").getAll(IDBKeyRange.only([vtxo.txid, vtxo.vout]));
|
|
5935
|
+
getAllReq.onsuccess = () => {
|
|
5936
|
+
const existing = getAllReq.result;
|
|
5937
|
+
for (const e of existing) store.delete([e.vtxoTxid, e.vtxoVout, e.position]);
|
|
5938
|
+
for (const b of branch) store.put(b);
|
|
5939
|
+
};
|
|
5940
|
+
await awaitTransaction(transaction);
|
|
5941
|
+
}
|
|
5942
|
+
async getBranch(vtxo) {
|
|
5943
|
+
const db = await this.getDB();
|
|
5944
|
+
const transaction = db.transaction([STORE_VTXO_BRANCHES, STORE_VIRTUAL_TXS], "readonly");
|
|
5945
|
+
const branchStore = transaction.objectStore(STORE_VTXO_BRANCHES);
|
|
5946
|
+
const txStore = transaction.objectStore(STORE_VIRTUAL_TXS);
|
|
5947
|
+
const getAllReq = branchStore.index("vtxo").getAll(IDBKeyRange.only([vtxo.txid, vtxo.vout]));
|
|
5948
|
+
const byTxid = /* @__PURE__ */ new Map();
|
|
5949
|
+
let ordered = [];
|
|
5950
|
+
getAllReq.onsuccess = () => {
|
|
5951
|
+
ordered = getAllReq.result.slice().sort((a, b) => a.position - b.position);
|
|
5952
|
+
for (const b of ordered) {
|
|
5953
|
+
const txReq = txStore.get(b.virtualTxid);
|
|
5954
|
+
txReq.onsuccess = () => {
|
|
5955
|
+
const r = txReq.result;
|
|
5956
|
+
if (r) byTxid.set(b.virtualTxid, r);
|
|
5957
|
+
};
|
|
5958
|
+
}
|
|
5959
|
+
};
|
|
5960
|
+
await awaitTransaction(transaction);
|
|
5961
|
+
const out = [];
|
|
5962
|
+
for (const b of ordered) {
|
|
5963
|
+
const tx = byTxid.get(b.virtualTxid);
|
|
5964
|
+
if (tx) out.push(tx);
|
|
5965
|
+
}
|
|
5966
|
+
return out;
|
|
5967
|
+
}
|
|
5968
|
+
async hasBranch(vtxo) {
|
|
5969
|
+
const db = await this.getDB();
|
|
5970
|
+
const vtxoIndex = db.transaction([STORE_VTXO_BRANCHES], "readonly").objectStore(STORE_VTXO_BRANCHES).index("vtxo");
|
|
5971
|
+
const count = await promisifyRequest(
|
|
5972
|
+
vtxoIndex.count(IDBKeyRange.only([vtxo.txid, vtxo.vout]))
|
|
5973
|
+
);
|
|
5974
|
+
return count > 0;
|
|
5975
|
+
}
|
|
5976
|
+
async pruneForSpentVtxo(vtxo) {
|
|
5977
|
+
const db = await this.getDB();
|
|
5978
|
+
const transaction = db.transaction([STORE_VTXO_BRANCHES, STORE_VIRTUAL_TXS], "readwrite");
|
|
5979
|
+
const branches = transaction.objectStore(STORE_VTXO_BRANCHES);
|
|
5980
|
+
const txStore = transaction.objectStore(STORE_VIRTUAL_TXS);
|
|
5981
|
+
const getAllReq = branches.index("vtxo").getAll(IDBKeyRange.only([vtxo.txid, vtxo.vout]));
|
|
5982
|
+
getAllReq.onsuccess = () => {
|
|
5983
|
+
const removed = getAllReq.result;
|
|
5984
|
+
if (removed.length === 0) return;
|
|
5985
|
+
for (const e of removed) branches.delete([e.vtxoTxid, e.vtxoVout, e.position]);
|
|
5986
|
+
for (const e of removed) {
|
|
5987
|
+
const countReq = branches.index("virtualTxid").count(IDBKeyRange.only(e.virtualTxid));
|
|
5988
|
+
countReq.onsuccess = () => {
|
|
5989
|
+
if (countReq.result === 0) txStore.delete(e.virtualTxid);
|
|
5990
|
+
};
|
|
5991
|
+
}
|
|
5992
|
+
};
|
|
5993
|
+
await awaitTransaction(transaction);
|
|
5994
|
+
}
|
|
5995
|
+
async [Symbol.asyncDispose]() {
|
|
5996
|
+
if (!this.db) return;
|
|
5997
|
+
await closeDatabase(this.dbName);
|
|
5998
|
+
this.db = null;
|
|
5999
|
+
}
|
|
6000
|
+
};
|
|
6001
|
+
|
|
6002
|
+
// src/repositories/indexedDB/intentRepository.ts
|
|
6003
|
+
var IndexedDBIntentRepository = class {
|
|
6004
|
+
constructor(dbName = DEFAULT_DB_NAME) {
|
|
6005
|
+
this.dbName = dbName;
|
|
6006
|
+
}
|
|
6007
|
+
version = 1;
|
|
6008
|
+
db = null;
|
|
6009
|
+
async getDB() {
|
|
6010
|
+
if (!this.db)
|
|
6011
|
+
this.db = await openDatabase(this.dbName, INTENT_DB_VERSION, initDatabaseWithIntents);
|
|
6012
|
+
return this.db;
|
|
6013
|
+
}
|
|
6014
|
+
async clear() {
|
|
6015
|
+
const db = await this.getDB();
|
|
6016
|
+
const transaction = db.transaction([STORE_INTENTS], "readwrite");
|
|
6017
|
+
transaction.objectStore(STORE_INTENTS).clear();
|
|
6018
|
+
await awaitTransaction(transaction);
|
|
6019
|
+
}
|
|
6020
|
+
async saveIntent(intent) {
|
|
6021
|
+
const db = await this.getDB();
|
|
6022
|
+
const transaction = db.transaction([STORE_INTENTS], "readwrite");
|
|
6023
|
+
transaction.objectStore(STORE_INTENTS).put({ ...intent, updatedAt: Date.now() });
|
|
6024
|
+
await awaitTransaction(transaction);
|
|
6025
|
+
}
|
|
6026
|
+
async getIntents(filter) {
|
|
6027
|
+
const db = await this.getDB();
|
|
6028
|
+
const store = db.transaction([STORE_INTENTS], "readonly").objectStore(STORE_INTENTS);
|
|
6029
|
+
const all = await promisifyRequest(store.getAll());
|
|
6030
|
+
all.sort((a, b) => a.createdAt - b.createdAt || a.intentTxId.localeCompare(b.intentTxId));
|
|
6031
|
+
const out = filter ? all.filter((i) => intentMatchesFilter(i, filter)) : all;
|
|
6032
|
+
const { skip, end } = intentPageBounds(filter, out.length);
|
|
6033
|
+
return out.slice(skip, end);
|
|
6034
|
+
}
|
|
6035
|
+
async getLockedVtxoOutpoints() {
|
|
6036
|
+
const db = await this.getDB();
|
|
6037
|
+
const store = db.transaction([STORE_INTENTS], "readonly").objectStore(STORE_INTENTS);
|
|
6038
|
+
const all = await promisifyRequest(store.getAll());
|
|
6039
|
+
const out = [];
|
|
6040
|
+
for (const i of all)
|
|
6041
|
+
if (!isTerminalIntentState(i.state)) for (const o of i.intentVtxos) out.push(o);
|
|
6042
|
+
return out;
|
|
6043
|
+
}
|
|
6044
|
+
async [Symbol.asyncDispose]() {
|
|
6045
|
+
if (!this.db) return;
|
|
6046
|
+
await closeDatabase(this.dbName);
|
|
6047
|
+
this.db = null;
|
|
6048
|
+
}
|
|
6049
|
+
};
|
|
6050
|
+
|
|
6051
|
+
// src/contracts/vtxoOwnership.ts
|
|
6052
|
+
function vtxoOutpoint(vtxo) {
|
|
6053
|
+
return `${vtxo.txid}:${vtxo.vout}`;
|
|
6054
|
+
}
|
|
6055
|
+
function isVtxoForScript(vtxo, script) {
|
|
6056
|
+
return !!vtxo.script && vtxo.script === script;
|
|
6057
|
+
}
|
|
6058
|
+
function filterVtxosForScript(vtxos, script) {
|
|
6059
|
+
return vtxos.filter((v) => isVtxoForScript(v, script));
|
|
6060
|
+
}
|
|
6061
|
+
function warnAndFilterVtxosForScript(vtxos, script, context) {
|
|
6062
|
+
const matches = [];
|
|
6063
|
+
const rejected = [];
|
|
6064
|
+
for (const v of vtxos) {
|
|
6065
|
+
if (isVtxoForScript(v, script)) {
|
|
6066
|
+
matches.push(v);
|
|
6067
|
+
} else {
|
|
6068
|
+
rejected.push(`${vtxoOutpoint(v)}(script=${v.script ?? ""})`);
|
|
6069
|
+
}
|
|
6070
|
+
}
|
|
6071
|
+
if (rejected.length > 0) {
|
|
6072
|
+
console.warn(
|
|
6073
|
+
`${context}: dropped ${rejected.length} wrong-script VTXO(s) for script ${script}: ${rejected.join(", ")}`
|
|
6074
|
+
);
|
|
6075
|
+
}
|
|
6076
|
+
return matches;
|
|
6077
|
+
}
|
|
6078
|
+
function validateVtxosForScript(vtxos, script, context) {
|
|
6079
|
+
const mismatches = vtxos.filter((v) => !isVtxoForScript(v, script));
|
|
6080
|
+
if (mismatches.length === 0) return;
|
|
6081
|
+
const detail = mismatches.map((v) => `${vtxoOutpoint(v)}(script=${v.script ?? ""})`).join(", ");
|
|
6082
|
+
throw new Error(
|
|
6083
|
+
`${context}: refusing to persist ${mismatches.length} VTXO(s) whose script does not match ${script}: ${detail}`
|
|
6084
|
+
);
|
|
6085
|
+
}
|
|
6086
|
+
async function getVtxosForContract(repo, contract) {
|
|
6087
|
+
return repo.getVtxosForScript ? repo.getVtxosForScript(contract.script) : filterVtxosForScript(await repo.getVtxos(contract.address), contract.script);
|
|
6088
|
+
}
|
|
6089
|
+
async function saveVtxosForContract(repo, contract, vtxos) {
|
|
6090
|
+
if (repo.saveVtxosForScript) {
|
|
6091
|
+
return repo.saveVtxosForScript(
|
|
6092
|
+
{ script: contract.script, address: contract.address },
|
|
6093
|
+
vtxos
|
|
6094
|
+
);
|
|
6095
|
+
}
|
|
6096
|
+
validateVtxosForScript(vtxos, contract.script, "saveVtxosForContract");
|
|
6097
|
+
return repo.saveVtxos(contract.address, vtxos);
|
|
6098
|
+
}
|
|
6099
|
+
|
|
6100
|
+
// src/repositories/inMemory/walletRepository.ts
|
|
6101
|
+
var InMemoryWalletRepository = class {
|
|
6102
|
+
version = 1;
|
|
6103
|
+
vtxosByAddress = /* @__PURE__ */ new Map();
|
|
6104
|
+
utxosByAddress = /* @__PURE__ */ new Map();
|
|
6105
|
+
txsByAddress = /* @__PURE__ */ new Map();
|
|
6106
|
+
walletState = null;
|
|
6107
|
+
async getVtxos(address) {
|
|
6108
|
+
return this.vtxosByAddress.get(address) ?? [];
|
|
6109
|
+
}
|
|
6110
|
+
async saveVtxos(address, vtxos) {
|
|
6111
|
+
const existing = this.vtxosByAddress.get(address) ?? [];
|
|
6112
|
+
const next = mergeByKey(existing, vtxos, (item) => `${item.txid}:${item.vout}`);
|
|
6113
|
+
this.vtxosByAddress.set(address, next);
|
|
6114
|
+
}
|
|
6115
|
+
async deleteVtxos(address) {
|
|
6116
|
+
this.vtxosByAddress.delete(address);
|
|
6117
|
+
}
|
|
6118
|
+
async getVtxosForScript(script) {
|
|
6119
|
+
const allMatches = [];
|
|
6120
|
+
for (const bucket of this.vtxosByAddress.values()) {
|
|
6121
|
+
for (const vtxo of bucket) {
|
|
6122
|
+
if (isVtxoForScript(vtxo, script)) {
|
|
6123
|
+
allMatches.push(vtxo);
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
}
|
|
6127
|
+
return mergeByKey([], allMatches, (item) => `${item.txid}:${item.vout}`);
|
|
6128
|
+
}
|
|
6129
|
+
async saveVtxosForScript(key, vtxos) {
|
|
6130
|
+
if (!key.address) {
|
|
6131
|
+
throw new Error("InMemoryWalletRepository requires an address");
|
|
6132
|
+
}
|
|
6133
|
+
for (const vtxo of vtxos) {
|
|
6134
|
+
if (!isVtxoForScript(vtxo, key.script)) {
|
|
6135
|
+
throw new Error(
|
|
6136
|
+
`VTXO ${vtxo.txid}:${vtxo.vout} script mismatch: expected ${key.script}, got ${vtxo.script}`
|
|
6137
|
+
);
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
return this.saveVtxos(key.address, vtxos);
|
|
6141
|
+
}
|
|
6142
|
+
async deleteVtxosForScript(script) {
|
|
6143
|
+
for (const [address, bucket] of this.vtxosByAddress.entries()) {
|
|
6144
|
+
const next = bucket.filter((v) => !isVtxoForScript(v, script));
|
|
6145
|
+
if (next.length === 0) {
|
|
6146
|
+
this.vtxosByAddress.delete(address);
|
|
6147
|
+
} else {
|
|
6148
|
+
this.vtxosByAddress.set(address, next);
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
async getUtxos(address) {
|
|
6153
|
+
return this.utxosByAddress.get(address) ?? [];
|
|
6154
|
+
}
|
|
6155
|
+
async saveUtxos(address, utxos) {
|
|
6156
|
+
const existing = this.utxosByAddress.get(address) ?? [];
|
|
6157
|
+
const next = mergeByKey(existing, utxos, (item) => `${item.txid}:${item.vout}`);
|
|
6158
|
+
this.utxosByAddress.set(address, next);
|
|
6159
|
+
}
|
|
6160
|
+
async deleteUtxos(address) {
|
|
6161
|
+
this.utxosByAddress.delete(address);
|
|
6162
|
+
}
|
|
6163
|
+
async getTransactionHistory(address) {
|
|
6164
|
+
return this.txsByAddress.get(address) ?? [];
|
|
6165
|
+
}
|
|
6166
|
+
async saveTransactions(address, txs) {
|
|
6167
|
+
const existing = this.txsByAddress.get(address) ?? [];
|
|
6168
|
+
const next = mergeByKey(existing, txs, serializeTxKey);
|
|
6169
|
+
this.txsByAddress.set(address, next);
|
|
6170
|
+
}
|
|
6171
|
+
async deleteTransactions(address) {
|
|
6172
|
+
this.txsByAddress.delete(address);
|
|
6173
|
+
}
|
|
6174
|
+
async getWalletState() {
|
|
6175
|
+
return this.walletState;
|
|
6176
|
+
}
|
|
6177
|
+
async saveWalletState(state) {
|
|
6178
|
+
this.walletState = state;
|
|
6179
|
+
}
|
|
6180
|
+
async clear() {
|
|
6181
|
+
this.vtxosByAddress.clear();
|
|
6182
|
+
this.utxosByAddress.clear();
|
|
6183
|
+
this.txsByAddress.clear();
|
|
6184
|
+
this.walletState = null;
|
|
6185
|
+
}
|
|
6186
|
+
async [Symbol.asyncDispose]() {
|
|
6187
|
+
return;
|
|
6188
|
+
}
|
|
6189
|
+
};
|
|
6190
|
+
function serializeTxKey(tx) {
|
|
6191
|
+
const key = tx.key;
|
|
6192
|
+
return `${key.boardingTxid}:${key.commitmentTxid}:${key.arkTxid}`;
|
|
6193
|
+
}
|
|
6194
|
+
function mergeByKey(existing, incoming, toKey) {
|
|
6195
|
+
const next = /* @__PURE__ */ new Map();
|
|
6196
|
+
existing.forEach((item) => {
|
|
6197
|
+
next.set(toKey(item), item);
|
|
6198
|
+
});
|
|
6199
|
+
incoming.forEach((item) => {
|
|
6200
|
+
next.set(toKey(item), item);
|
|
6201
|
+
});
|
|
6202
|
+
return Array.from(next.values());
|
|
6203
|
+
}
|
|
6204
|
+
|
|
6205
|
+
// src/repositories/inMemory/contractRepository.ts
|
|
6206
|
+
var InMemoryContractRepository = class {
|
|
6207
|
+
version = 1;
|
|
6208
|
+
contractData = /* @__PURE__ */ new Map();
|
|
6209
|
+
collections = /* @__PURE__ */ new Map();
|
|
6210
|
+
contractsByScript = /* @__PURE__ */ new Map();
|
|
6211
|
+
async clear() {
|
|
6212
|
+
this.contractData.clear();
|
|
6213
|
+
this.collections.clear();
|
|
6214
|
+
this.contractsByScript.clear();
|
|
6215
|
+
}
|
|
6216
|
+
// Contract entity management methods
|
|
6217
|
+
async getContracts(filter) {
|
|
6218
|
+
const contracts = this.contractsByScript.values();
|
|
6219
|
+
if (!filter) {
|
|
6220
|
+
return [...contracts];
|
|
6221
|
+
}
|
|
6222
|
+
const matches = (value, criterion) => {
|
|
6223
|
+
if (criterion === void 0) {
|
|
6224
|
+
return true;
|
|
6225
|
+
}
|
|
6226
|
+
return Array.isArray(criterion) ? criterion.includes(value) : value === criterion;
|
|
6227
|
+
};
|
|
6228
|
+
const results = [];
|
|
6229
|
+
for (const contract of contracts) {
|
|
6230
|
+
if (matches(contract.script, filter.script) && matches(contract.state, filter.state) && matches(contract.type, filter.type)) {
|
|
6231
|
+
results.push(contract);
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
return results;
|
|
6235
|
+
}
|
|
6236
|
+
async saveContract(contract) {
|
|
6237
|
+
this.contractsByScript.set(contract.script, contract);
|
|
6238
|
+
}
|
|
6239
|
+
async deleteContract(script) {
|
|
6240
|
+
this.contractsByScript.delete(script);
|
|
6241
|
+
}
|
|
6242
|
+
async [Symbol.asyncDispose]() {
|
|
6243
|
+
return;
|
|
6244
|
+
}
|
|
6245
|
+
};
|
|
5620
6246
|
var serializeTapLeaf = ([cb, s]) => ({
|
|
5621
6247
|
cb: hex.encode(TaprootControlBlock.encode(cb)),
|
|
5622
6248
|
s: hex.encode(s)
|
|
@@ -5683,132 +6309,6 @@ var deserializeTransaction = (o) => ({
|
|
|
5683
6309
|
assets: deserializeAssets(o.assets)
|
|
5684
6310
|
});
|
|
5685
6311
|
|
|
5686
|
-
// src/repositories/indexedDB/manager.ts
|
|
5687
|
-
function getGlobalObject() {
|
|
5688
|
-
if (typeof globalThis !== "undefined") {
|
|
5689
|
-
if (typeof globalThis.self === "object" && globalThis.self !== null) {
|
|
5690
|
-
return { globalObject: globalThis.self };
|
|
5691
|
-
}
|
|
5692
|
-
if (typeof globalThis.window === "object" && globalThis.window !== null) {
|
|
5693
|
-
return { globalObject: globalThis.window };
|
|
5694
|
-
}
|
|
5695
|
-
return { globalObject: globalThis };
|
|
5696
|
-
}
|
|
5697
|
-
throw new Error("Global object not found");
|
|
5698
|
-
}
|
|
5699
|
-
var dbCache = /* @__PURE__ */ new Map();
|
|
5700
|
-
var refCounts = /* @__PURE__ */ new Map();
|
|
5701
|
-
async function openDatabase(dbName, dbVersion, initDatabase2) {
|
|
5702
|
-
const { globalObject } = getGlobalObject();
|
|
5703
|
-
if (!globalObject.indexedDB) {
|
|
5704
|
-
throw new Error("IndexedDB is not available in this environment");
|
|
5705
|
-
}
|
|
5706
|
-
const cached = dbCache.get(dbName);
|
|
5707
|
-
if (cached) {
|
|
5708
|
-
if (cached.version !== dbVersion) {
|
|
5709
|
-
throw new Error(
|
|
5710
|
-
`Database "${dbName}" already opened with version ${cached.version}; requested ${dbVersion}`
|
|
5711
|
-
);
|
|
5712
|
-
}
|
|
5713
|
-
refCounts.set(dbName, (refCounts.get(dbName) ?? 0) + 1);
|
|
5714
|
-
return cached.promise;
|
|
5715
|
-
}
|
|
5716
|
-
const dbPromise = new Promise((resolve, reject) => {
|
|
5717
|
-
const request = globalObject.indexedDB.open(dbName, dbVersion);
|
|
5718
|
-
request.onerror = () => {
|
|
5719
|
-
dbCache.delete(dbName);
|
|
5720
|
-
refCounts.delete(dbName);
|
|
5721
|
-
reject(request.error);
|
|
5722
|
-
};
|
|
5723
|
-
request.onsuccess = () => {
|
|
5724
|
-
const db = request.result;
|
|
5725
|
-
db.onversionchange = () => {
|
|
5726
|
-
db.close();
|
|
5727
|
-
dbCache.delete(dbName);
|
|
5728
|
-
refCounts.delete(dbName);
|
|
5729
|
-
};
|
|
5730
|
-
resolve(db);
|
|
5731
|
-
};
|
|
5732
|
-
request.onupgradeneeded = (event) => {
|
|
5733
|
-
const db = request.result;
|
|
5734
|
-
initDatabase2(db, event.oldVersion, request.transaction);
|
|
5735
|
-
};
|
|
5736
|
-
request.onblocked = () => {
|
|
5737
|
-
console.warn("Database upgrade blocked - close other tabs/connections");
|
|
5738
|
-
};
|
|
5739
|
-
});
|
|
5740
|
-
dbCache.set(dbName, { version: dbVersion, promise: dbPromise });
|
|
5741
|
-
refCounts.set(dbName, 1);
|
|
5742
|
-
return dbPromise;
|
|
5743
|
-
}
|
|
5744
|
-
async function closeDatabase(dbName) {
|
|
5745
|
-
const cachedEntry = dbCache.get(dbName);
|
|
5746
|
-
if (!cachedEntry) return false;
|
|
5747
|
-
const count = (refCounts.get(dbName) ?? 1) - 1;
|
|
5748
|
-
if (count > 0) {
|
|
5749
|
-
refCounts.set(dbName, count);
|
|
5750
|
-
return false;
|
|
5751
|
-
}
|
|
5752
|
-
refCounts.delete(dbName);
|
|
5753
|
-
dbCache.delete(dbName);
|
|
5754
|
-
try {
|
|
5755
|
-
const db = await cachedEntry.promise;
|
|
5756
|
-
db.close();
|
|
5757
|
-
} catch {
|
|
5758
|
-
}
|
|
5759
|
-
return true;
|
|
5760
|
-
}
|
|
5761
|
-
|
|
5762
|
-
// src/worker/browser/utils.ts
|
|
5763
|
-
var DEFAULT_DB_NAME = "arkade-service-worker";
|
|
5764
|
-
var DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS = 1e4;
|
|
5765
|
-
function normalizeOptions2(pathOrOptions) {
|
|
5766
|
-
if (typeof pathOrOptions === "string") {
|
|
5767
|
-
return {
|
|
5768
|
-
path: pathOrOptions,
|
|
5769
|
-
activationTimeoutMs: DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS
|
|
5770
|
-
};
|
|
5771
|
-
}
|
|
5772
|
-
return {
|
|
5773
|
-
path: pathOrOptions.path,
|
|
5774
|
-
activationTimeoutMs: pathOrOptions.activationTimeoutMs ?? DEFAULT_SERVICE_WORKER_ACTIVATION_TIMEOUT_MS
|
|
5775
|
-
};
|
|
5776
|
-
}
|
|
5777
|
-
function waitForServiceWorkerReady(timeoutMs) {
|
|
5778
|
-
return new Promise((resolve, reject) => {
|
|
5779
|
-
const timeoutId = setTimeout(() => {
|
|
5780
|
-
reject(new Error(`Service worker activation timed out after ${timeoutMs}ms`));
|
|
5781
|
-
}, timeoutMs);
|
|
5782
|
-
navigator.serviceWorker.ready.then((registration) => {
|
|
5783
|
-
clearTimeout(timeoutId);
|
|
5784
|
-
resolve(registration);
|
|
5785
|
-
}).catch((error) => {
|
|
5786
|
-
clearTimeout(timeoutId);
|
|
5787
|
-
reject(error);
|
|
5788
|
-
});
|
|
5789
|
-
});
|
|
5790
|
-
}
|
|
5791
|
-
async function setupServiceWorker(pathOrOptions) {
|
|
5792
|
-
if (!("serviceWorker" in navigator)) {
|
|
5793
|
-
throw new Error("Service workers are not supported in this browser");
|
|
5794
|
-
}
|
|
5795
|
-
const { path, activationTimeoutMs } = normalizeOptions2(pathOrOptions);
|
|
5796
|
-
const registration = await navigator.serviceWorker.register(path);
|
|
5797
|
-
await registration.update();
|
|
5798
|
-
const serviceWorker = registration.active || registration.waiting || registration.installing;
|
|
5799
|
-
if (!serviceWorker) {
|
|
5800
|
-
throw new Error("Failed to get service worker instance");
|
|
5801
|
-
}
|
|
5802
|
-
if (serviceWorker.state === "activated") {
|
|
5803
|
-
return serviceWorker;
|
|
5804
|
-
}
|
|
5805
|
-
const readyRegistration = await waitForServiceWorkerReady(activationTimeoutMs);
|
|
5806
|
-
if (!readyRegistration.active) {
|
|
5807
|
-
throw new Error("Service worker registration is ready but has no active worker");
|
|
5808
|
-
}
|
|
5809
|
-
return readyRegistration.active;
|
|
5810
|
-
}
|
|
5811
|
-
|
|
5812
6312
|
// src/repositories/indexedDB/contractRepository.ts
|
|
5813
6313
|
var IndexedDBContractRepository = class {
|
|
5814
6314
|
constructor(dbName = DEFAULT_DB_NAME) {
|
|
@@ -7246,6 +7746,89 @@ var ContractWatcher = class {
|
|
|
7246
7746
|
}
|
|
7247
7747
|
};
|
|
7248
7748
|
|
|
7749
|
+
// src/wallet/intentReconciliation.ts
|
|
7750
|
+
var NON_TERMINAL_INTENT_STATES = ALL_INTENT_STATES.filter(
|
|
7751
|
+
(s) => !isTerminalIntentState(s)
|
|
7752
|
+
);
|
|
7753
|
+
function isConsumed(vtxo) {
|
|
7754
|
+
return vtxo !== void 0 && (vtxo.isSpent === true || vtxo.settledBy !== void 0);
|
|
7755
|
+
}
|
|
7756
|
+
async function classifyIntent(intent, vtxosByOutpoint, now) {
|
|
7757
|
+
if (isTerminalIntentState(intent.state)) return void 0;
|
|
7758
|
+
const inputs = intent.intentVtxos;
|
|
7759
|
+
const consumed = inputs.map((o) => vtxosByOutpoint.get(outpointKey(o)));
|
|
7760
|
+
if (inputs.length > 0 && consumed.every(isConsumed)) {
|
|
7761
|
+
const commitmentTransactionId = intent.commitmentTransactionId ?? consumed.map((v) => v?.settledBy).find((s) => !!s);
|
|
7762
|
+
return terminal(intent, "batch_succeeded", now, { commitmentTransactionId });
|
|
7763
|
+
}
|
|
7764
|
+
if (intent.state === "waiting_to_submit") {
|
|
7765
|
+
return terminal(intent, "cancelled", now, {
|
|
7766
|
+
cancellationReason: "reconciliation: intent was never submitted before restart"
|
|
7767
|
+
});
|
|
7768
|
+
}
|
|
7769
|
+
if (intent.validUntil !== void 0 && intent.validUntil < now) {
|
|
7770
|
+
return terminal(intent, "cancelled", now, {
|
|
7771
|
+
cancellationReason: "reconciliation: intent expired"
|
|
7772
|
+
});
|
|
7773
|
+
}
|
|
7774
|
+
return void 0;
|
|
7775
|
+
}
|
|
7776
|
+
async function reconcileIntents(deps) {
|
|
7777
|
+
const now = deps.now ?? Date.now;
|
|
7778
|
+
let intents;
|
|
7779
|
+
try {
|
|
7780
|
+
intents = await deps.intentRepository.getIntents({
|
|
7781
|
+
states: [...NON_TERMINAL_INTENT_STATES]
|
|
7782
|
+
});
|
|
7783
|
+
} catch (e) {
|
|
7784
|
+
console.error("reconcileIntents: failed to read non-terminal intents", e);
|
|
7785
|
+
return;
|
|
7786
|
+
}
|
|
7787
|
+
if (intents.length === 0) return;
|
|
7788
|
+
const outpoints = dedupeOutpoints(intents.flatMap((i) => i.intentVtxos));
|
|
7789
|
+
let vtxosByOutpoint = /* @__PURE__ */ new Map();
|
|
7790
|
+
if (outpoints.length > 0) {
|
|
7791
|
+
try {
|
|
7792
|
+
const { vtxos } = await deps.indexerProvider.getVtxos({ outpoints });
|
|
7793
|
+
vtxosByOutpoint = new Map(vtxos.map((v) => [outpointKey(v), v]));
|
|
7794
|
+
} catch (e) {
|
|
7795
|
+
console.error("reconcileIntents: failed to fetch input VTXO state", e);
|
|
7796
|
+
return;
|
|
7797
|
+
}
|
|
7798
|
+
}
|
|
7799
|
+
const nowMs = now();
|
|
7800
|
+
for (const intent of intents) {
|
|
7801
|
+
try {
|
|
7802
|
+
const next = await classifyIntent(intent, vtxosByOutpoint, nowMs);
|
|
7803
|
+
if (!next) continue;
|
|
7804
|
+
const [fresh] = await deps.intentRepository.getIntents({
|
|
7805
|
+
intentTxIds: [intent.intentTxId]
|
|
7806
|
+
});
|
|
7807
|
+
if (!fresh || fresh.state !== intent.state) continue;
|
|
7808
|
+
await deps.intentRepository.saveIntent(next);
|
|
7809
|
+
} catch (e) {
|
|
7810
|
+
console.error(`reconcileIntents: failed to reconcile intent ${intent.intentTxId}`, e);
|
|
7811
|
+
}
|
|
7812
|
+
}
|
|
7813
|
+
}
|
|
7814
|
+
function terminal(intent, state, now, patch) {
|
|
7815
|
+
return { ...intent, ...patch, state, updatedAt: now };
|
|
7816
|
+
}
|
|
7817
|
+
function outpointKey(o) {
|
|
7818
|
+
return `${o.txid}:${o.vout}`;
|
|
7819
|
+
}
|
|
7820
|
+
function dedupeOutpoints(outpoints) {
|
|
7821
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7822
|
+
const out = [];
|
|
7823
|
+
for (const o of outpoints) {
|
|
7824
|
+
const k = outpointKey(o);
|
|
7825
|
+
if (seen.has(k)) continue;
|
|
7826
|
+
seen.add(k);
|
|
7827
|
+
out.push(o);
|
|
7828
|
+
}
|
|
7829
|
+
return out;
|
|
7830
|
+
}
|
|
7831
|
+
|
|
7249
7832
|
// src/utils/syncCursors.ts
|
|
7250
7833
|
var SAFETY_LAG_MS = 3e4;
|
|
7251
7834
|
var OVERLAP_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -7369,6 +7952,26 @@ var ContractManager = class _ContractManager {
|
|
|
7369
7952
|
if (watched.length > 0) {
|
|
7370
7953
|
await this.reconcilePendingFrontier(watched);
|
|
7371
7954
|
}
|
|
7955
|
+
await this.reconcileStaleIntents();
|
|
7956
|
+
}
|
|
7957
|
+
/**
|
|
7958
|
+
* Crash-recovery for persisted settlement intents: reconcile any
|
|
7959
|
+
* non-terminal intent left behind by a mid-settle crash against
|
|
7960
|
+
* authoritative indexer state (see {@link reconcileIntents}). Runs on the
|
|
7961
|
+
* online sync path only — boot and subscription reconnect — never from a
|
|
7962
|
+
* wallet read API. Best-effort: intent recovery is not a sync invariant, so
|
|
7963
|
+
* a failure is logged and sync continues. No-op without an intent store.
|
|
7964
|
+
*/
|
|
7965
|
+
async reconcileStaleIntents() {
|
|
7966
|
+
if (!this.config.intentRepository) return;
|
|
7967
|
+
try {
|
|
7968
|
+
await reconcileIntents({
|
|
7969
|
+
intentRepository: this.config.intentRepository,
|
|
7970
|
+
indexerProvider: this.config.indexerProvider
|
|
7971
|
+
});
|
|
7972
|
+
} catch (e) {
|
|
7973
|
+
console.error("ContractManager: intent reconciliation failed", e);
|
|
7974
|
+
}
|
|
7372
7975
|
}
|
|
7373
7976
|
/**
|
|
7374
7977
|
* Create and register a new contract.
|
|
@@ -8739,6 +9342,22 @@ async function resolveBoardingBootTapscript(contractRepository, serverPubKey, ba
|
|
|
8739
9342
|
function hasToReadonly(identity) {
|
|
8740
9343
|
return typeof identity === "object" && identity !== null && "toReadonly" in identity && typeof identity.toReadonly === "function";
|
|
8741
9344
|
}
|
|
9345
|
+
function excludeLockedOutpoints(vtxos, locked) {
|
|
9346
|
+
if (locked.length === 0) return vtxos;
|
|
9347
|
+
const lockedKeys = new Set(locked.map((o) => `${o.txid}:${o.vout}`));
|
|
9348
|
+
return vtxos.filter((v) => !lockedKeys.has(`${v.txid}:${v.vout}`));
|
|
9349
|
+
}
|
|
9350
|
+
async function spendableVtxosExcludingLocked(vtxos, intentRepository) {
|
|
9351
|
+
if (!intentRepository) return vtxos;
|
|
9352
|
+
let locked;
|
|
9353
|
+
try {
|
|
9354
|
+
locked = await intentRepository.getLockedVtxoOutpoints();
|
|
9355
|
+
} catch (e) {
|
|
9356
|
+
console.error("getLockedVtxoOutpoints failed; reporting unfiltered balance", e);
|
|
9357
|
+
return vtxos;
|
|
9358
|
+
}
|
|
9359
|
+
return excludeLockedOutpoints(vtxos, locked);
|
|
9360
|
+
}
|
|
8742
9361
|
var ReadonlyWallet = class _ReadonlyWallet {
|
|
8743
9362
|
constructor(identity, network, onchainProvider, indexerProvider, arkServerPublicKey, offchainTapscript, boardingTapscript, dustAmount, walletRepository, contractRepository, delegateProvider, watcherConfig, walletContractTimelocks) {
|
|
8744
9363
|
this.identity = identity;
|
|
@@ -8769,6 +9388,20 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8769
9388
|
_contractManager;
|
|
8770
9389
|
_contractManagerInitializing;
|
|
8771
9390
|
watcherConfig;
|
|
9391
|
+
/**
|
|
9392
|
+
* Opt-in intent-lifecycle repository. Assigned by the `create()`
|
|
9393
|
+
* factories from `config.storage.intentRepository`; `undefined` ⇒ all
|
|
9394
|
+
* intent-persistence code paths are no-ops (default behaviour unchanged).
|
|
9395
|
+
*/
|
|
9396
|
+
intentRepository;
|
|
9397
|
+
/**
|
|
9398
|
+
* **Experimental / inert.** Opt-in virtual-tx / exit-branch repository,
|
|
9399
|
+
* exposed so callers can pass it to {@link Unroll.Session.create} as a
|
|
9400
|
+
* best-effort raw-tx cache. Assigned by `create()` from
|
|
9401
|
+
* `config.storage.virtualTxRepository`. Normal sync never writes it
|
|
9402
|
+
* (ContractManager isn't given it); `undefined` ⇒ no-op.
|
|
9403
|
+
*/
|
|
9404
|
+
virtualTxRepository;
|
|
8772
9405
|
_assetManager;
|
|
8773
9406
|
walletContractTimelocks;
|
|
8774
9407
|
// Outpoints ("txid:vout") committed to an in-flight settle/send. Filtered
|
|
@@ -8826,9 +9459,12 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
8826
9459
|
_deprecatedSigners = /* @__PURE__ */ new Map();
|
|
8827
9460
|
/**
|
|
8828
9461
|
* Refresh the cached deprecated-signer set from a fresh server-info
|
|
8829
|
-
* snapshot. Called by the create() factories at construction
|
|
8830
|
-
* server-info-change handler mid-session
|
|
8831
|
-
*
|
|
9462
|
+
* snapshot. Called by the create() factories at construction, by the
|
|
9463
|
+
* server-info-change handler mid-session, and by the deprecated-signer
|
|
9464
|
+
* migration pass (`VtxoManager.migrateDeprecatedSignerVtxos`). This set feeds
|
|
9465
|
+
* {@link pendingRecoveryOutpoints}, which drops EXPIRED (past-cutoff)
|
|
9466
|
+
* deprecated-signer VTXOs from the wallet's own coin selection. Lenient: a
|
|
9467
|
+
* malformed deprecated entry is skipped, never fatal to wallet creation.
|
|
8832
9468
|
*/
|
|
8833
9469
|
refreshDeprecatedSigners(info) {
|
|
8834
9470
|
const next = /* @__PURE__ */ new Map();
|
|
@@ -9044,6 +9680,8 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
9044
9680
|
config.watcherConfig,
|
|
9045
9681
|
setup.walletContractTimelocks
|
|
9046
9682
|
);
|
|
9683
|
+
wallet.intentRepository = config.storage?.intentRepository;
|
|
9684
|
+
wallet.virtualTxRepository = config.storage?.virtualTxRepository;
|
|
9047
9685
|
wallet.refreshDeprecatedSigners(setup.info);
|
|
9048
9686
|
return wallet;
|
|
9049
9687
|
}
|
|
@@ -9075,6 +9713,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
9075
9713
|
this.pendingRecoveryOutpoints()
|
|
9076
9714
|
]);
|
|
9077
9715
|
const isPendingRecovery = (coin) => pendingOutpoints.has(`${coin.txid}:${coin.vout}`);
|
|
9716
|
+
const unlockedVtxos = await spendableVtxosExcludingLocked(vtxos, this.intentRepository);
|
|
9078
9717
|
let confirmed = 0;
|
|
9079
9718
|
let unconfirmed = 0;
|
|
9080
9719
|
for (const utxo of boardingUtxos) {
|
|
@@ -9084,14 +9723,15 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
9084
9723
|
unconfirmed += utxo.value;
|
|
9085
9724
|
}
|
|
9086
9725
|
}
|
|
9087
|
-
let settled = 0;
|
|
9088
|
-
let preconfirmed = 0;
|
|
9089
9726
|
let recoverable = 0;
|
|
9090
9727
|
let pendingRecovery = 0;
|
|
9091
|
-
settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled" && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
|
|
9092
|
-
preconfirmed = vtxos.filter(
|
|
9728
|
+
const settled = vtxos.filter((coin) => coin.virtualStatus.state === "settled" && !isPendingRecovery(coin)).reduce((sum, coin) => sum + coin.value, 0);
|
|
9729
|
+
const preconfirmed = vtxos.filter(
|
|
9093
9730
|
(coin) => coin.virtualStatus.state === "preconfirmed" && !isPendingRecovery(coin)
|
|
9094
9731
|
).reduce((sum, coin) => sum + coin.value, 0);
|
|
9732
|
+
const available = unlockedVtxos.filter(
|
|
9733
|
+
(coin) => (coin.virtualStatus.state === "settled" || coin.virtualStatus.state === "preconfirmed") && !isPendingRecovery(coin)
|
|
9734
|
+
).reduce((sum, coin) => sum + coin.value, 0);
|
|
9095
9735
|
recoverable = vtxos.filter((coin) => isSpendable(coin) && coin.virtualStatus.state === "swept").reduce((sum, coin) => sum + coin.value, 0);
|
|
9096
9736
|
pendingRecovery = vtxos.filter(isPendingRecovery).reduce((sum, coin) => sum + coin.value, 0);
|
|
9097
9737
|
const totalBoarding = confirmed + unconfirmed;
|
|
@@ -9118,7 +9758,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
9118
9758
|
},
|
|
9119
9759
|
settled,
|
|
9120
9760
|
preconfirmed,
|
|
9121
|
-
available
|
|
9761
|
+
available,
|
|
9122
9762
|
recoverable,
|
|
9123
9763
|
pendingRecovery,
|
|
9124
9764
|
total: totalBoarding + totalOffchain,
|
|
@@ -9595,6 +10235,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
9595
10235
|
indexerProvider: this.indexerProvider,
|
|
9596
10236
|
contractRepository: this.contractRepository,
|
|
9597
10237
|
walletRepository: this.walletRepository,
|
|
10238
|
+
intentRepository: this.intentRepository,
|
|
9598
10239
|
watcherConfig: this.watcherConfig
|
|
9599
10240
|
});
|
|
9600
10241
|
const baselinePubkey = await this.identity.xOnlyPublicKey();
|
|
@@ -10288,6 +10929,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10288
10929
|
wallet.setBoardingTapscriptForRotation(resolvedBoarding);
|
|
10289
10930
|
}
|
|
10290
10931
|
}
|
|
10932
|
+
wallet.intentRepository = config.storage?.intentRepository;
|
|
10933
|
+
wallet.virtualTxRepository = config.storage?.virtualTxRepository;
|
|
10291
10934
|
await wallet.getVtxoManager();
|
|
10292
10935
|
return wallet;
|
|
10293
10936
|
}
|
|
@@ -10567,12 +11210,26 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10567
11210
|
),
|
|
10568
11211
|
this.makeDeleteIntentSignature(params.inputs)
|
|
10569
11212
|
]);
|
|
11213
|
+
let intentTxId;
|
|
11214
|
+
try {
|
|
11215
|
+
intentTxId = Transaction$2.fromPSBT(base64.decode(intent.proof)).id;
|
|
11216
|
+
} catch {
|
|
11217
|
+
intentTxId = params.inputs.map((i) => `${i.txid}:${i.vout}`).sort().join("|");
|
|
11218
|
+
}
|
|
11219
|
+
await this.persistIntentSnapshot(
|
|
11220
|
+
intentTxId,
|
|
11221
|
+
"waiting_to_submit",
|
|
11222
|
+
intent,
|
|
11223
|
+
deleteIntent,
|
|
11224
|
+
params.inputs
|
|
11225
|
+
);
|
|
10570
11226
|
const topics = [
|
|
10571
11227
|
...signingPublicKeys,
|
|
10572
11228
|
...params.inputs.map((input) => `${input.txid}:${input.vout}`)
|
|
10573
11229
|
];
|
|
10574
11230
|
const abortController = new AbortController();
|
|
10575
11231
|
let stream;
|
|
11232
|
+
let committedTxid;
|
|
10576
11233
|
this._addPendingSpends(params.inputs);
|
|
10577
11234
|
try {
|
|
10578
11235
|
stream = this.arkProvider.getEventStream(abortController.signal, topics);
|
|
@@ -10587,16 +11244,43 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10587
11244
|
yield* stream;
|
|
10588
11245
|
})();
|
|
10589
11246
|
const intentId = await this.safeRegisterIntent(intent, params.inputs);
|
|
10590
|
-
|
|
11247
|
+
await this.persistIntentSnapshot(
|
|
11248
|
+
intentTxId,
|
|
11249
|
+
"waiting_for_batch",
|
|
11250
|
+
intent,
|
|
11251
|
+
deleteIntent,
|
|
11252
|
+
params.inputs,
|
|
11253
|
+
{ intentId }
|
|
11254
|
+
);
|
|
11255
|
+
const handler = wrapHandlerWithIntentPersistence(
|
|
11256
|
+
this.createBatchHandler(intentId, params.inputs, recipients, session),
|
|
11257
|
+
{ intentRepository: this.intentRepository, intentTxId }
|
|
11258
|
+
);
|
|
10591
11259
|
const commitmentTxid = await Batch.join(primedStream, handler, {
|
|
10592
11260
|
abortController,
|
|
10593
11261
|
skipVtxoTreeSigning: !hasOffchainOutputs,
|
|
10594
|
-
|
|
11262
|
+
// async so a synchronous throw becomes a rejection Batch.join
|
|
11263
|
+
// swallows, keeping the observational callback off the stream path.
|
|
11264
|
+
eventCallback: eventCallback && (async (event) => {
|
|
11265
|
+
await eventCallback(event);
|
|
11266
|
+
})
|
|
10595
11267
|
});
|
|
11268
|
+
committedTxid = commitmentTxid;
|
|
10596
11269
|
await this.updateDbAfterSettle(params.inputs, commitmentTxid);
|
|
10597
11270
|
await this.maybeRotateBoardingAfterBoard(params.inputs);
|
|
10598
11271
|
return commitmentTxid;
|
|
10599
11272
|
} catch (error) {
|
|
11273
|
+
if (committedTxid !== void 0) {
|
|
11274
|
+
await this.persistIntentSnapshot(
|
|
11275
|
+
intentTxId,
|
|
11276
|
+
"batch_succeeded",
|
|
11277
|
+
intent,
|
|
11278
|
+
deleteIntent,
|
|
11279
|
+
params.inputs,
|
|
11280
|
+
{ commitmentTransactionId: committedTxid }
|
|
11281
|
+
);
|
|
11282
|
+
throw error;
|
|
11283
|
+
}
|
|
10600
11284
|
const inputIds = params.inputs.map((i) => `${i.txid}:${i.vout}`).join(",");
|
|
10601
11285
|
await this.arkProvider.deleteIntent(deleteIntent).catch((e) => {
|
|
10602
11286
|
console.warn(
|
|
@@ -10604,6 +11288,16 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10604
11288
|
e
|
|
10605
11289
|
);
|
|
10606
11290
|
});
|
|
11291
|
+
await this.persistIntentSnapshot(
|
|
11292
|
+
intentTxId,
|
|
11293
|
+
"cancelled",
|
|
11294
|
+
intent,
|
|
11295
|
+
deleteIntent,
|
|
11296
|
+
params.inputs,
|
|
11297
|
+
{
|
|
11298
|
+
cancellationReason: error instanceof Error ? error.message : String(error)
|
|
11299
|
+
}
|
|
11300
|
+
);
|
|
10607
11301
|
throw error;
|
|
10608
11302
|
} finally {
|
|
10609
11303
|
this._removePendingSpends(params.inputs);
|
|
@@ -10878,6 +11572,42 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
10878
11572
|
}
|
|
10879
11573
|
return jobs;
|
|
10880
11574
|
}
|
|
11575
|
+
/**
|
|
11576
|
+
* Best-effort upsert of the current settlement intent into the optional
|
|
11577
|
+
* {@link intentRepository}. Never throws into the settle path — intent
|
|
11578
|
+
* persistence is observational and must not break money flow. Preserves
|
|
11579
|
+
* fields already written by the event reducer (state-event ordering).
|
|
11580
|
+
*/
|
|
11581
|
+
async persistIntentSnapshot(intentTxId, state, intent, deleteIntent, inputs, patch) {
|
|
11582
|
+
const repo = this.intentRepository;
|
|
11583
|
+
if (!repo) return;
|
|
11584
|
+
try {
|
|
11585
|
+
const now = Date.now();
|
|
11586
|
+
const existing = (await repo.getIntents({ intentTxIds: [intentTxId] }))[0];
|
|
11587
|
+
if (existing && isTerminalIntentState(existing.state)) {
|
|
11588
|
+
return;
|
|
11589
|
+
}
|
|
11590
|
+
await repo.saveIntent({
|
|
11591
|
+
...existing ?? {},
|
|
11592
|
+
intentTxId,
|
|
11593
|
+
registerProof: intent.proof,
|
|
11594
|
+
registerProofMessage: Intent.encodeMessage(intent.message),
|
|
11595
|
+
deleteProof: deleteIntent.proof,
|
|
11596
|
+
deleteProofMessage: Intent.encodeMessage(deleteIntent.message),
|
|
11597
|
+
intentVtxos: inputs.map((i) => ({
|
|
11598
|
+
txid: i.txid,
|
|
11599
|
+
vout: i.vout
|
|
11600
|
+
})),
|
|
11601
|
+
partialForfeits: existing?.partialForfeits ?? [],
|
|
11602
|
+
createdAt: existing?.createdAt ?? now,
|
|
11603
|
+
updatedAt: now,
|
|
11604
|
+
state,
|
|
11605
|
+
...patch
|
|
11606
|
+
});
|
|
11607
|
+
} catch (e) {
|
|
11608
|
+
console.error(`Failed to persist intent ${intentTxId} (state=${state})`, e);
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
10881
11611
|
/**
|
|
10882
11612
|
* @internal Sign an on-chain boarding exit / sweep transaction, routing
|
|
10883
11613
|
* each input to the correct key by its `witnessUtxo.script`: the identity
|
|
@@ -14887,42 +15617,11 @@ var OnchainWallet = class _OnchainWallet {
|
|
|
14887
15617
|
* @see send
|
|
14888
15618
|
*/
|
|
14889
15619
|
async bumpP2A(parent) {
|
|
14890
|
-
const parentVsize = parent.vsize;
|
|
14891
|
-
let child = new Transaction({
|
|
14892
|
-
version: 3,
|
|
14893
|
-
allowLegacyWitnessUtxo: true
|
|
14894
|
-
});
|
|
14895
|
-
child.addInput(findP2AOutput(parent));
|
|
14896
|
-
const childVsize = TxWeightEstimator.create().addKeySpendInput(true).addP2AInput().addOutputAddress(this.address, this.network).vsize().value;
|
|
14897
|
-
const packageVSize = parentVsize + Number(childVsize);
|
|
14898
15620
|
let feeRate = await this.provider.getFeeRate();
|
|
14899
15621
|
if (!feeRate || feeRate < _OnchainWallet.MIN_FEE_RATE) {
|
|
14900
15622
|
feeRate = _OnchainWallet.MIN_FEE_RATE;
|
|
14901
15623
|
}
|
|
14902
|
-
const
|
|
14903
|
-
if (!fee) {
|
|
14904
|
-
throw new Error(
|
|
14905
|
-
`invalid fee, got ${fee} with vsize ${packageVSize}, feeRate ${feeRate}`
|
|
14906
|
-
);
|
|
14907
|
-
}
|
|
14908
|
-
const coins = await this.getCoins();
|
|
14909
|
-
const selected = selectCoins(coins, fee, true);
|
|
14910
|
-
for (const input of selected.inputs) {
|
|
14911
|
-
child.addInput({
|
|
14912
|
-
txid: input.txid,
|
|
14913
|
-
index: input.vout,
|
|
14914
|
-
witnessUtxo: {
|
|
14915
|
-
script: this.onchainP2TR.script,
|
|
14916
|
-
amount: BigInt(input.value)
|
|
14917
|
-
},
|
|
14918
|
-
tapInternalKey: this.onchainP2TR.tapInternalKey
|
|
14919
|
-
});
|
|
14920
|
-
}
|
|
14921
|
-
child.addOutputAddress(this.address, P2A.amount + selected.changeAmount, this.network);
|
|
14922
|
-
child = await this.identity.sign(child);
|
|
14923
|
-
for (let i = 1; i < child.inputsLength; i++) {
|
|
14924
|
-
child.finalizeIdx(i);
|
|
14925
|
-
}
|
|
15624
|
+
const child = await this.buildBumpPackage(parent, feeRate, await this.getCoins());
|
|
14926
15625
|
try {
|
|
14927
15626
|
await this.provider.broadcastTransaction(parent.hex, child.hex);
|
|
14928
15627
|
} catch (error) {
|
|
@@ -14931,6 +15630,59 @@ var OnchainWallet = class _OnchainWallet {
|
|
|
14931
15630
|
return [parent.hex, child.hex];
|
|
14932
15631
|
}
|
|
14933
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
|
+
}
|
|
14934
15686
|
};
|
|
14935
15687
|
function selectCoins(coins, targetAmount, forceChange = false) {
|
|
14936
15688
|
if (isNaN(targetAmount)) {
|
|
@@ -15821,6 +16573,98 @@ function craftBIP322ToSignSimple(toSpend, pkScript) {
|
|
|
15821
16573
|
});
|
|
15822
16574
|
return tx;
|
|
15823
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
|
|
16654
|
+
function chainTxTypeToChainedExit(t) {
|
|
16655
|
+
switch (t) {
|
|
16656
|
+
case "INDEXER_CHAINED_TX_TYPE_COMMITMENT" /* COMMITMENT */:
|
|
16657
|
+
return 1 /* Commitment */;
|
|
16658
|
+
case "INDEXER_CHAINED_TX_TYPE_ARK" /* ARK */:
|
|
16659
|
+
return 2 /* Ark */;
|
|
16660
|
+
case "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */:
|
|
16661
|
+
return 3 /* Tree */;
|
|
16662
|
+
case "INDEXER_CHAINED_TX_TYPE_CHECKPOINT" /* CHECKPOINT */:
|
|
16663
|
+
return 4 /* Checkpoint */;
|
|
16664
|
+
default:
|
|
16665
|
+
return 0 /* Unspecified */;
|
|
16666
|
+
}
|
|
16667
|
+
}
|
|
15824
16668
|
var Unroll;
|
|
15825
16669
|
((Unroll2) => {
|
|
15826
16670
|
((StepType2) => {
|
|
@@ -15830,16 +16674,54 @@ var Unroll;
|
|
|
15830
16674
|
})(Unroll2.StepType || (Unroll2.StepType = {}));
|
|
15831
16675
|
class Session2 {
|
|
15832
16676
|
/** Create an unroll session from a virtual output outpoint and its dependency chain. */
|
|
15833
|
-
constructor(toUnroll, bumper, explorer, indexer) {
|
|
16677
|
+
constructor(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
|
|
15834
16678
|
this.toUnroll = toUnroll;
|
|
15835
16679
|
this.bumper = bumper;
|
|
15836
16680
|
this.explorer = explorer;
|
|
15837
16681
|
this.indexer = indexer;
|
|
16682
|
+
this.virtualTxRepository = virtualTxRepository;
|
|
15838
16683
|
}
|
|
15839
16684
|
/** Create an unroll session by loading the virtual output chain from the indexer. */
|
|
15840
|
-
static async create(toUnroll, bumper, explorer, indexer) {
|
|
16685
|
+
static async create(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
|
|
15841
16686
|
const { chain } = await indexer.getVtxoChain(toUnroll);
|
|
15842
|
-
return new Session2(
|
|
16687
|
+
return new Session2(
|
|
16688
|
+
{ ...toUnroll, chain },
|
|
16689
|
+
bumper,
|
|
16690
|
+
explorer,
|
|
16691
|
+
indexer,
|
|
16692
|
+
virtualTxRepository
|
|
16693
|
+
);
|
|
16694
|
+
}
|
|
16695
|
+
/**
|
|
16696
|
+
* Resolve a chain tx's raw PSBT (base64). Repo first when configured;
|
|
16697
|
+
* indexer on a miss, then best-effort cached so a later exit doesn't
|
|
16698
|
+
* re-fetch. Never throws from the cache
|
|
16699
|
+
* write — exit correctness must not depend on persistence.
|
|
16700
|
+
*/
|
|
16701
|
+
async resolveVirtualTxBase64(next) {
|
|
16702
|
+
const repo = this.virtualTxRepository;
|
|
16703
|
+
if (repo) {
|
|
16704
|
+
try {
|
|
16705
|
+
const stored = await repo.getVirtualTx(next.txid);
|
|
16706
|
+
if (stored?.psbt) return stored.psbt;
|
|
16707
|
+
} catch {
|
|
16708
|
+
}
|
|
16709
|
+
}
|
|
16710
|
+
const fetched = await this.indexer.getVirtualTxs([next.txid]);
|
|
16711
|
+
const psbt = fetched.txs[0];
|
|
16712
|
+
if (psbt && repo) {
|
|
16713
|
+
const cached = {
|
|
16714
|
+
txid: next.txid,
|
|
16715
|
+
psbt,
|
|
16716
|
+
expiresAt: null,
|
|
16717
|
+
type: chainTxTypeToChainedExit(next.type)
|
|
16718
|
+
};
|
|
16719
|
+
try {
|
|
16720
|
+
await repo.upsertVirtualTxs([cached]);
|
|
16721
|
+
} catch {
|
|
16722
|
+
}
|
|
16723
|
+
}
|
|
16724
|
+
return psbt;
|
|
15843
16725
|
}
|
|
15844
16726
|
/**
|
|
15845
16727
|
* Get the next step to be executed
|
|
@@ -15874,26 +16756,11 @@ var Unroll;
|
|
|
15874
16756
|
do: () => Promise.resolve()
|
|
15875
16757
|
};
|
|
15876
16758
|
}
|
|
15877
|
-
const
|
|
15878
|
-
if (
|
|
16759
|
+
const virtualTxBase64 = await this.resolveVirtualTxBase64(nextTxToBroadcast);
|
|
16760
|
+
if (!virtualTxBase64) {
|
|
15879
16761
|
throw new Error(`Tx ${nextTxToBroadcast.txid} not found`);
|
|
15880
16762
|
}
|
|
15881
|
-
const tx =
|
|
15882
|
-
if (nextTxToBroadcast.type === "INDEXER_CHAINED_TX_TYPE_TREE" /* TREE */) {
|
|
15883
|
-
const input = tx.getInput(0);
|
|
15884
|
-
if (!input) {
|
|
15885
|
-
throw new Error("Input not found");
|
|
15886
|
-
}
|
|
15887
|
-
const tapKeySig = input.tapKeySig;
|
|
15888
|
-
if (!tapKeySig) {
|
|
15889
|
-
throw new Error("Tap key sig not found");
|
|
15890
|
-
}
|
|
15891
|
-
tx.updateInput(0, {
|
|
15892
|
-
finalScriptWitness: [tapKeySig]
|
|
15893
|
-
});
|
|
15894
|
-
} else {
|
|
15895
|
-
tx.finalize();
|
|
15896
|
-
}
|
|
16763
|
+
const tx = finalizeVirtualTx(nextTxToBroadcast.type, virtualTxBase64);
|
|
15897
16764
|
const pkg = await this.bumper.bumpP2A(tx);
|
|
15898
16765
|
return {
|
|
15899
16766
|
type: 0 /* UNROLL */,
|
|
@@ -15947,20 +16814,21 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
|
|
|
15947
16814
|
if (!txStatus.confirmed) {
|
|
15948
16815
|
throw new Error(`tx ${vtxo.txid} is not confirmed`);
|
|
15949
16816
|
}
|
|
15950
|
-
const
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
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) {
|
|
15956
16829
|
throw new Error(`no available exit path found for vtxo ${vtxo.txid}:${vtxo.vout}`);
|
|
15957
16830
|
}
|
|
15958
|
-
const spendingLeaf = VtxoScript.decode(vtxo.tapTree).findLeaf(hex.encode(exit.script));
|
|
15959
|
-
if (!spendingLeaf) {
|
|
15960
|
-
throw new Error(`spending leaf not found for vtxo ${vtxo.txid}:${vtxo.vout}`);
|
|
15961
|
-
}
|
|
15962
16831
|
totalAmount += BigInt(vtxo.value);
|
|
15963
|
-
const sequence = timelockToSequence(exit.params.timelock);
|
|
15964
16832
|
inputs.push({
|
|
15965
16833
|
txid: vtxo.txid,
|
|
15966
16834
|
index: vtxo.vout,
|
|
@@ -16024,21 +16892,809 @@ function doWait(onchainProvider, txid) {
|
|
|
16024
16892
|
});
|
|
16025
16893
|
};
|
|
16026
16894
|
}
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
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
|
+
}
|
|
16033
16916
|
}
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
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);
|
|
16037
16979
|
}
|
|
16038
16980
|
}
|
|
16981
|
+
if (!progress) {
|
|
16982
|
+
throw new Error("topological sort: cycle or unsatisfiable dependency");
|
|
16983
|
+
}
|
|
16984
|
+
remaining = next;
|
|
16039
16985
|
}
|
|
16040
|
-
return
|
|
16986
|
+
return ordered;
|
|
16987
|
+
}
|
|
16988
|
+
function extraWitnessSize(path) {
|
|
16989
|
+
return (path.extraWitness ?? []).reduce((sum, item) => sum + 1 + item.length, 0);
|
|
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);
|
|
16041
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
|
+
};
|
|
16042
17698
|
var ARKCONTRACT_PREFIX = "arkcontract";
|
|
16043
17699
|
function encodeArkContract(contract) {
|
|
16044
17700
|
const params = new URLSearchParams();
|
|
@@ -16098,6 +17754,6 @@ function isArkContract(str) {
|
|
|
16098
17754
|
return str.startsWith(ARKCONTRACT_PREFIX + "=");
|
|
16099
17755
|
}
|
|
16100
17756
|
|
|
16101
|
-
export { ActivityRegistry, ArkNote, AssetManager, BIP322, Batch, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EsploraProvider, Estimator, HDDescriptorProvider, InMemoryContractRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBWalletRepository, MESSAGE_BUS_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, boardingResolver, buildForfeitTx, buildOffchainTx, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, createDefaultActivityRegistry, decodeArkContract, deserializeAssets, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, getMigrationStatus, getRandomId, hasBoardingTxExpired, isArkContract, isBatchSignable, isCooperativelyMigratable, isDiscoverable, isExpired, isRecoverable, isSpendable, isSubdust, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, migrateWalletRepository, openDatabase, requiresMigration, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, serializeAssets, serializeUtxo, serializeVtxo, setupServiceWorker, signerSetFromInfo, toXOnlySignerHex, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
|
|
16102
|
-
//# sourceMappingURL=chunk-
|
|
16103
|
-
//# 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
|