@arkade-os/sdk 0.4.66 → 0.4.68
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 +71 -15
- package/dist/adapters/expo.cjs +5 -5
- package/dist/adapters/expo.d.cts +2 -2
- package/dist/adapters/expo.d.ts +2 -2
- package/dist/adapters/expo.js +3 -3
- package/dist/adapters/indexedDB.cjs +5 -5
- package/dist/adapters/indexedDB.js +4 -4
- package/dist/{asyncStorageTaskQueue-BZcIN7sA.d.cts → asyncStorageTaskQueue-B2CcXIHU.d.cts} +1 -1
- package/dist/{asyncStorageTaskQueue-B6SijMi3.d.ts → asyncStorageTaskQueue-DaZlVTbM.d.ts} +1 -1
- package/dist/{chunk-2JZGPXU4.js → chunk-33Q2RFET.js} +3 -3
- package/dist/{chunk-2JZGPXU4.js.map → chunk-33Q2RFET.js.map} +1 -1
- package/dist/{chunk-CYOI4PEA.js → chunk-45FKMOJH.js} +160 -114
- package/dist/chunk-45FKMOJH.js.map +1 -0
- package/dist/{chunk-J33ZEM3A.js → chunk-4CMXV5SF.js} +45 -14
- package/dist/chunk-4CMXV5SF.js.map +1 -0
- package/dist/{chunk-LL47QIHD.cjs → chunk-4KCVS4ZN.cjs} +10 -10
- package/dist/{chunk-LL47QIHD.cjs.map → chunk-4KCVS4ZN.cjs.map} +1 -1
- package/dist/{chunk-PXPPZF5K.cjs → chunk-6YUACVX4.cjs} +205 -159
- package/dist/chunk-6YUACVX4.cjs.map +1 -0
- package/dist/{chunk-ZIRFXBKL.cjs → chunk-EJOFL5WH.cjs} +47 -13
- package/dist/chunk-EJOFL5WH.cjs.map +1 -0
- package/dist/{chunk-R5POS3RS.js → chunk-G4I6PU5H.js} +4 -4
- package/dist/{chunk-R5POS3RS.js.map → chunk-G4I6PU5H.js.map} +1 -1
- package/dist/{chunk-55ZZ6RBL.js → chunk-GQ4NMQGR.js} +4 -4
- package/dist/{chunk-55ZZ6RBL.js.map → chunk-GQ4NMQGR.js.map} +1 -1
- package/dist/{chunk-IM6IKNSL.cjs → chunk-JGMSZIE4.cjs} +17 -17
- package/dist/{chunk-IM6IKNSL.cjs.map → chunk-JGMSZIE4.cjs.map} +1 -1
- package/dist/{chunk-3S3JZUEN.cjs → chunk-PJR63FTK.cjs} +17 -17
- package/dist/{chunk-3S3JZUEN.cjs.map → chunk-PJR63FTK.cjs.map} +1 -1
- package/dist/{chunk-DSC3G6QZ.js → chunk-VBHXBI7F.js} +1060 -185
- package/dist/chunk-VBHXBI7F.js.map +1 -0
- package/dist/{chunk-5E6IZNRG.cjs → chunk-WCJVKFAP.cjs} +1467 -581
- package/dist/chunk-WCJVKFAP.cjs.map +1 -0
- package/dist/contracts/handlers/index.cjs +9 -9
- package/dist/contracts/handlers/index.d.cts +3 -3
- package/dist/contracts/handlers/index.d.ts +3 -3
- package/dist/contracts/handlers/index.js +2 -2
- package/dist/{delegate-T-vgxMYZ.d.ts → delegate-AomVWxO_.d.ts} +1 -1
- package/dist/{delegate-YXupcWWJ.d.cts → delegate-Bffc5f54.d.cts} +1 -1
- package/dist/{wallet-CNibc3yJ.d.ts → hdWalletCapable-B5nRs3RH.d.cts} +318 -127
- package/dist/{wallet-DJN1QgFt.d.cts → hdWalletCapable-DZdIVa23.d.ts} +318 -127
- package/dist/{index-uUE_uQPn.d.ts → index-C7RZCfoY.d.cts} +222 -19
- package/dist/{index-uUE_uQPn.d.cts → index-C7RZCfoY.d.ts} +222 -19
- package/dist/{index-e7LYXk1d.d.cts → index-jouvy8FD.d.ts} +172 -99
- package/dist/{index-D_FgwLH9.d.ts → index-nfyQM33h.d.cts} +172 -99
- package/dist/index.cjs +326 -278
- package/dist/index.d.cts +295 -41
- package/dist/index.d.ts +295 -41
- package/dist/index.js +4 -4
- package/dist/repositories/realm/index.cjs +19 -19
- package/dist/repositories/realm/index.d.cts +2 -2
- package/dist/repositories/realm/index.d.ts +2 -2
- package/dist/repositories/realm/index.js +5 -5
- package/dist/repositories/sqlite/index.cjs +18 -18
- package/dist/repositories/sqlite/index.d.cts +2 -2
- package/dist/repositories/sqlite/index.d.ts +2 -2
- package/dist/repositories/sqlite/index.js +5 -5
- package/dist/{taskRunner-BOVcIH1N.d.cts → taskRunner-D2vnpPBT.d.cts} +1 -1
- package/dist/{taskRunner-DNyD1kXG.d.ts → taskRunner-DuwK9t8O.d.ts} +1 -1
- package/dist/wallet/expo/background.cjs +14 -14
- package/dist/wallet/expo/background.d.cts +3 -3
- package/dist/wallet/expo/background.d.ts +3 -3
- package/dist/wallet/expo/background.js +6 -6
- package/dist/wallet/expo/index.cjs +18 -14
- package/dist/wallet/expo/index.cjs.map +1 -1
- package/dist/wallet/expo/index.d.cts +7 -5
- package/dist/wallet/expo/index.d.ts +7 -5
- package/dist/wallet/expo/index.js +10 -6
- package/dist/wallet/expo/index.js.map +1 -1
- package/dist/worker/expo/index.cjs +9 -9
- package/dist/worker/expo/index.d.cts +4 -4
- package/dist/worker/expo/index.d.ts +4 -4
- package/dist/worker/expo/index.js +5 -5
- package/package.json +3 -3
- package/dist/chunk-5E6IZNRG.cjs.map +0 -1
- package/dist/chunk-CYOI4PEA.js.map +0 -1
- package/dist/chunk-DSC3G6QZ.js.map +0 -1
- package/dist/chunk-J33ZEM3A.js.map +0 -1
- package/dist/chunk-PXPPZF5K.cjs.map +0 -1
- package/dist/chunk-ZIRFXBKL.cjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, BufferReader, BufferWriter, AssetRef, RestIndexerProvider } from './chunk-
|
|
2
|
-
import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, parseHDDescriptor, DefaultVtxo, speculativeReceiveMetadata, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler
|
|
3
|
-
import { VtxoScript, Transaction, isRecoverable, CSVMultisigTapscript, DEFAULT_NETWORK, resolveEmulatorPubkey, hasTerminalSpend, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, craftToSpendTx, ArkAddress, isVirtualCoin, getArkPsbtFields, CosignerPublicKey, ServerResponseMismatchError, OP_RETURN_EMPTY_PKSCRIPT, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, timelockToSequence, baseFetch, Intent, rateGate, DEFAULT_NETWORK_NAME, normalizeVtxo, getSequence, toBatchExpiry, maybeArkError, isArkError, ArkErrorName, toOffchainInputFeeParams, canRecoverOnchain, getNetwork, MultisigTapscript, isEventSourceError, isEventSourceUnavailableError, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, RestArkProvider, toVirtualStatus, ArkError, TapTreeCoder, resolveTimeHeight, canSpendOffchain, parseLegacyExpiry, isExpired, DEFAULT_ARKADE_SERVER_URL, ProviderUnavailableError, networks as networks$1, isPastExpiry, fetchVtxoCreatedAtByTxid } from './chunk-
|
|
1
|
+
import { AssetId, AssetGroup, AssetInput, AssetOutput, Metadata, Packet, BufferReader, BufferWriter, AssetRef, RestIndexerProvider } from './chunk-33Q2RFET.js';
|
|
2
|
+
import { witnessRefToBytes, validateProgram, toASM, stringifyArtifact, serializeArkadeContractParams, resolveAsm, parseArtifact, getOpcodeValue, getOpcodeName, fromASM, deserializeArkadeContractParams, computeArkadeScriptPublicKey, bytesToASM, asmToBytes, arkadeWitnessHash, arkadeScriptHash, SUPPORTED_PROGRAM_VERSION, OPCODE_VALUES, OPCODE_NAMES, bignum_exports, ArkadeScript, ArkadeProgramScript, ARKADE_OPS, ARKADE_OPCODE_VALUES, ARKADE_OPCODE_NAMES, ARKADE_OPCODES, ARKADE_OP, toXOnly, inputName, contractHandlers, isMainnetDescriptor, descriptorIsOurs, deriveDescriptorLeafPubKey, deriveDescriptorLeafCompressedPubKey, parseHDDescriptor, identityDescriptor, DefaultVtxo, speculativeReceiveMetadata, DelegateVtxo, WALLET_RECEIVE_SOURCE, BoardingContractHandler } from './chunk-45FKMOJH.js';
|
|
3
|
+
import { VtxoScript, Transaction, isRecoverable, CSVMultisigTapscript, DEFAULT_NETWORK, resolveEmulatorPubkey, hasTerminalSpend, getNormalizedVtxos, setArkPsbtField, PrevArkTxField, ConditionWitness, craftToSpendTx, ArkAddress, isVirtualCoin, getArkPsbtFields, CosignerPublicKey, ServerResponseMismatchError, OP_RETURN_EMPTY_PKSCRIPT, canSweepOnchain, sequenceToTimelock, decodeTapscript, scriptFromTapLeafScript, CLTVMultisigTapscript, VtxoTaprootTree, timelockToSequence, assertAllowedSighashTypes, baseFetch, Intent, rateGate, DEFAULT_NETWORK_NAME, formatSighash, normalizeVtxo, getSequence, toBatchExpiry, maybeArkError, isArkError, ArkErrorName, toOffchainInputFeeParams, canRecoverOnchain, getNetwork, MultisigTapscript, isEventSourceError, isEventSourceUnavailableError, isRetryableProviderError, getAllNormalizedVtxos, DEFAULT_PAGE_SIZE, RestArkProvider, toVirtualStatus, ArkError, TapTreeCoder, resolveTimeHeight, canSpendOffchain, parseLegacyExpiry, isExpired, PrevoutTxField, DEFAULT_ARKADE_SERVER_URL, ProviderUnavailableError, networks as networks$1, isPastExpiry, fetchVtxoCreatedAtByTxid } from './chunk-4CMXV5SF.js';
|
|
4
4
|
import { __export } from './chunk-NSBPE2FW.js';
|
|
5
5
|
import { sha256 as sha256$1, equalBytes, hash160, sha256x2, concatBytes, randomPrivateKeyBytes, pubECDSA, pubSchnorr } from '@scure/btc-signer/utils.js';
|
|
6
6
|
import { SigHash, Script, OP, OutScript, Address, RawWitness, p2tr, p2wpkh, TaprootControlBlock, DEFAULT_SEQUENCE, Transaction as Transaction$2 } from '@scure/btc-signer';
|
|
@@ -272,7 +272,7 @@ function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
|
272
272
|
script: pkScript
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
-
var
|
|
275
|
+
var ALLOWED_SIGHASH = [SigHash.DEFAULT, SigHash.ALL, SigHash.ALL_ANYONECANPAY];
|
|
276
276
|
var SingleKey = class _SingleKey {
|
|
277
277
|
key;
|
|
278
278
|
constructor(key) {
|
|
@@ -301,8 +301,9 @@ var SingleKey = class _SingleKey {
|
|
|
301
301
|
async sign(tx, inputIndexes) {
|
|
302
302
|
const txCpy = tx.clone();
|
|
303
303
|
if (!inputIndexes) {
|
|
304
|
+
assertAllowedSighashTypes(txCpy, ALLOWED_SIGHASH);
|
|
304
305
|
try {
|
|
305
|
-
if (!txCpy.sign(this.key,
|
|
306
|
+
if (!txCpy.sign(this.key, ALLOWED_SIGHASH)) {
|
|
306
307
|
throw new Error("Failed to sign transaction");
|
|
307
308
|
}
|
|
308
309
|
} catch (e) {
|
|
@@ -313,7 +314,7 @@ var SingleKey = class _SingleKey {
|
|
|
313
314
|
return txCpy;
|
|
314
315
|
}
|
|
315
316
|
for (const inputIndex of inputIndexes) {
|
|
316
|
-
if (!txCpy.signIdx(this.key, inputIndex,
|
|
317
|
+
if (!txCpy.signIdx(this.key, inputIndex, ALLOWED_SIGHASH)) {
|
|
317
318
|
throw new Error(`Failed to sign input #${inputIndex}`);
|
|
318
319
|
}
|
|
319
320
|
}
|
|
@@ -378,7 +379,7 @@ var ReadonlySingleKey = class _ReadonlySingleKey {
|
|
|
378
379
|
return Promise.resolve(this.publicKey);
|
|
379
380
|
}
|
|
380
381
|
};
|
|
381
|
-
var
|
|
382
|
+
var ALLOWED_SIGHASH2 = [SigHash.DEFAULT, SigHash.ALL, SigHash.ALL_ANYONECANPAY];
|
|
382
383
|
var seedBytes = /* @__PURE__ */ new WeakMap();
|
|
383
384
|
var mnemonicMeta = /* @__PURE__ */ new WeakMap();
|
|
384
385
|
var SeedIdentity = class _SeedIdentity {
|
|
@@ -581,8 +582,9 @@ var SeedIdentity = class _SeedIdentity {
|
|
|
581
582
|
signTxWithKey(tx, key, inputIndexes) {
|
|
582
583
|
const txCpy = tx.clone();
|
|
583
584
|
if (!inputIndexes) {
|
|
585
|
+
assertAllowedSighashTypes(txCpy, ALLOWED_SIGHASH2);
|
|
584
586
|
try {
|
|
585
|
-
if (!txCpy.sign(key,
|
|
587
|
+
if (!txCpy.sign(key, ALLOWED_SIGHASH2)) {
|
|
586
588
|
throw new Error("Failed to sign transaction");
|
|
587
589
|
}
|
|
588
590
|
} catch (e) {
|
|
@@ -592,7 +594,7 @@ var SeedIdentity = class _SeedIdentity {
|
|
|
592
594
|
}
|
|
593
595
|
} else {
|
|
594
596
|
for (const idx of inputIndexes) {
|
|
595
|
-
if (!txCpy.signIdx(key, idx,
|
|
597
|
+
if (!txCpy.signIdx(key, idx, ALLOWED_SIGHASH2)) {
|
|
596
598
|
throw new Error(`Failed to sign input #${idx}`);
|
|
597
599
|
}
|
|
598
600
|
}
|
|
@@ -917,6 +919,12 @@ var ForeignDescriptorError = class extends Error {
|
|
|
917
919
|
descriptor;
|
|
918
920
|
name = "ForeignDescriptorError";
|
|
919
921
|
};
|
|
922
|
+
var WalletCannotAllocateAddressError = class extends Error {
|
|
923
|
+
name = "WalletCannotAllocateAddressError";
|
|
924
|
+
constructor(reason, options) {
|
|
925
|
+
super(`cannot allocate a fresh address: ${reason}`, options);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
920
928
|
async function resolveDescriptorSigner(descriptor, identity, provider) {
|
|
921
929
|
const owner = provider?.isOurs(descriptor) ? provider : isHDCapableIdentity(identity) && identity.isOurs(descriptor) ? identity : void 0;
|
|
922
930
|
if (owner && parseHDDescriptor(descriptor)) {
|
|
@@ -941,6 +949,10 @@ function isHDAllocationCapable(value) {
|
|
|
941
949
|
const v = value;
|
|
942
950
|
return typeof v.getNextSigningDescriptor === "function" && typeof v.advanceSigningDescriptorWatermark === "function";
|
|
943
951
|
}
|
|
952
|
+
function isAddressAllocationCapable(value) {
|
|
953
|
+
if (typeof value !== "object" || value === null) return false;
|
|
954
|
+
return typeof value.getNewAddresses === "function";
|
|
955
|
+
}
|
|
944
956
|
|
|
945
957
|
// ../../node_modules/@noble/hashes/utils.js
|
|
946
958
|
function isBytes(a) {
|
|
@@ -1302,15 +1314,26 @@ async function provisionDescriptor(wallet) {
|
|
|
1302
1314
|
return allocated ?? await identityDescriptor(wallet.identity);
|
|
1303
1315
|
}
|
|
1304
1316
|
async function provisionRefundKey(wallet) {
|
|
1305
|
-
const descriptor = await
|
|
1317
|
+
const descriptor = await identityDescriptor(wallet.identity);
|
|
1306
1318
|
const signer = await contractSigner(wallet, descriptor);
|
|
1307
|
-
|
|
1319
|
+
const pubkey = await signer.xOnlyPublicKey();
|
|
1320
|
+
const identityPubkey = await wallet.identity.xOnlyPublicKey();
|
|
1321
|
+
if (!equalBytes(pubkey, identityPubkey)) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
"provisionRefundKey: descriptor pubkey does not match wallet identity \u2014 the refund key and the refund address would be on different keys"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
const address = await wallet.getAddress();
|
|
1327
|
+
const { pkScript } = ArkAddress.decode(address);
|
|
1328
|
+
return { descriptor, pubkey, pkScript, address };
|
|
1308
1329
|
}
|
|
1309
1330
|
async function provisionClaimSecret(wallet, opts = {}) {
|
|
1310
1331
|
if (opts.preimage && opts.preimage.length !== 32) {
|
|
1311
1332
|
throw new Error(`preimage must be 32 bytes, got ${opts.preimage.length}`);
|
|
1312
1333
|
}
|
|
1313
|
-
const
|
|
1334
|
+
const descriptor = await provisionDescriptor(wallet);
|
|
1335
|
+
const signer = await contractSigner(wallet, descriptor);
|
|
1336
|
+
const pubkey = await signer.xOnlyPublicKey();
|
|
1314
1337
|
const claim = async () => {
|
|
1315
1338
|
if (opts.preimage) return { preimage: opts.preimage, mustPersistPreimage: true };
|
|
1316
1339
|
if (isPerArtifactDescriptor(descriptor)) {
|
|
@@ -1610,8 +1633,6 @@ var RestDelegatorProvider = RestDelegateProvider;
|
|
|
1610
1633
|
function isDelegateInfo(data) {
|
|
1611
1634
|
return !!data && typeof data === "object" && "pubkey" in data && "fee" in data && typeof data.pubkey === "string" && typeof data.fee === "string" && data.pubkey !== "" && data.fee !== "" && (typeof data.delegateAddress === "string" && data.delegateAddress !== "" || typeof data.delegatorAddress === "string" && data.delegatorAddress !== "");
|
|
1612
1635
|
}
|
|
1613
|
-
|
|
1614
|
-
// src/providers/onchain.ts
|
|
1615
1636
|
var ESPLORA_URL = {
|
|
1616
1637
|
bitcoin: "https://mempool.arkade.sh/api",
|
|
1617
1638
|
testnet: "https://mempool.space/testnet/api",
|
|
@@ -1672,6 +1693,14 @@ var EsploraProvider = class {
|
|
|
1672
1693
|
}
|
|
1673
1694
|
return response.json();
|
|
1674
1695
|
}
|
|
1696
|
+
async getRawTransaction(txid) {
|
|
1697
|
+
const response = await baseFetch(`${this.baseUrl}/tx/${txid}/hex`);
|
|
1698
|
+
if (!response.ok) {
|
|
1699
|
+
const error = await response.text();
|
|
1700
|
+
throw new Error(`Failed to get raw transaction ${txid}: ${error}`);
|
|
1701
|
+
}
|
|
1702
|
+
return hex.decode((await response.text()).trim());
|
|
1703
|
+
}
|
|
1675
1704
|
async getTxStatus(txid) {
|
|
1676
1705
|
const txresponse = await baseFetch(`${this.baseUrl}/tx/${txid}`);
|
|
1677
1706
|
if (!txresponse.ok) {
|
|
@@ -1795,7 +1824,9 @@ var EsploraProvider = class {
|
|
|
1795
1824
|
const error = await response.text();
|
|
1796
1825
|
throw new Error(`Failed to broadcast package: ${error}`);
|
|
1797
1826
|
}
|
|
1798
|
-
|
|
1827
|
+
const result = await response.json();
|
|
1828
|
+
assertPackageAccepted(result);
|
|
1829
|
+
return result;
|
|
1799
1830
|
}
|
|
1800
1831
|
async broadcastTx(tx) {
|
|
1801
1832
|
const response = await baseFetch(`${this.baseUrl}/tx`, {
|
|
@@ -1812,6 +1843,24 @@ var EsploraProvider = class {
|
|
|
1812
1843
|
return response.text();
|
|
1813
1844
|
}
|
|
1814
1845
|
};
|
|
1846
|
+
function assertPackageAccepted(result) {
|
|
1847
|
+
if (!result || typeof result !== "object") return;
|
|
1848
|
+
const r = result;
|
|
1849
|
+
const msg = typeof r.package_msg === "string" ? r.package_msg : void 0;
|
|
1850
|
+
if (msg === void 0 || msg === "success") return;
|
|
1851
|
+
const reasons = [];
|
|
1852
|
+
const results = r["tx-results"];
|
|
1853
|
+
if (results && typeof results === "object") {
|
|
1854
|
+
for (const entry of Object.values(results)) {
|
|
1855
|
+
if (!entry || typeof entry !== "object" || entry.error === void 0) continue;
|
|
1856
|
+
const txid = typeof entry.txid === "string" ? entry.txid : "unknown tx";
|
|
1857
|
+
reasons.push(`${txid}: ${String(entry.error)}`);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
reasons.length > 0 ? `Package rejected (${msg}) \u2014 ${reasons.join("; ")}` : `Package rejected (${msg})`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1815
1864
|
function isValidBlocksTip(tip) {
|
|
1816
1865
|
return Array.isArray(tip) && tip.every((t) => {
|
|
1817
1866
|
return t && typeof t === "object" && typeof t.id === "string" && t.id.length > 0 && typeof t.height === "number" && t.height >= 0 && typeof t.mediantime === "number" && t.mediantime > 0;
|
|
@@ -3038,9 +3087,6 @@ function hasBoardingTxExpired(coin, boardingTimelock, chainTipHeight) {
|
|
|
3038
3087
|
const blockTime = BigInt(Math.floor(coin.status.block_time));
|
|
3039
3088
|
return blockTime + boardingTimelock.value <= now;
|
|
3040
3089
|
}
|
|
3041
|
-
function formatSighash(type) {
|
|
3042
|
-
return `0x${type.toString(16).padStart(2, "0")}`;
|
|
3043
|
-
}
|
|
3044
3090
|
function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, excludePubkeys = [], allowedSighashTypes = [SigHash.DEFAULT], expectedLeafHash) {
|
|
3045
3091
|
const input = tx.getInput(inputIndex);
|
|
3046
3092
|
const prevoutScripts = [];
|
|
@@ -3258,6 +3304,15 @@ async function submitOffchainTx(provider, offchainTx, signer, hooks, options) {
|
|
|
3258
3304
|
assertSubmittedArkTxid(response, signedArkTx, "submitTx");
|
|
3259
3305
|
const { arkTxid, signedCheckpointTxs } = response;
|
|
3260
3306
|
const matched = matchServerCheckpoints(signedCheckpointTxs, offchainTx.checkpoints, "submitTx");
|
|
3307
|
+
matched.forEach(({ server }, index) => {
|
|
3308
|
+
try {
|
|
3309
|
+
assertAllowedSighashTypes(server);
|
|
3310
|
+
} catch (error) {
|
|
3311
|
+
throw new ServerResponseMismatchError(
|
|
3312
|
+
`submitTx checkpoint ${index}: ${error instanceof Error ? error.message : String(error)}`
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
3261
3316
|
const verify = options?.verifyServerSignatures;
|
|
3262
3317
|
if (verify) {
|
|
3263
3318
|
if (response.finalArkTx === void 0) {
|
|
@@ -3709,7 +3764,9 @@ function selectPendingRecoveryOutpoints(contractsWithVtxos, signerSet, nowSecond
|
|
|
3709
3764
|
continue;
|
|
3710
3765
|
}
|
|
3711
3766
|
for (const v of vtxos) {
|
|
3712
|
-
if (!hasTerminalSpend(v) && !v.isSwept
|
|
3767
|
+
if (!hasTerminalSpend(v) && !v.isSwept && !v.isUnrolled) {
|
|
3768
|
+
out.add(`${v.txid}:${v.vout}`);
|
|
3769
|
+
}
|
|
3713
3770
|
}
|
|
3714
3771
|
}
|
|
3715
3772
|
return out;
|
|
@@ -3857,6 +3914,7 @@ function getRecoverableWithSubdust(vtxos, dustAmount, now) {
|
|
|
3857
3914
|
};
|
|
3858
3915
|
}
|
|
3859
3916
|
function isVtxoExpiringSoon(vtxo, thresholdMs) {
|
|
3917
|
+
if (vtxo.isUnrolled) return false;
|
|
3860
3918
|
const realThresholdMs = thresholdMs <= 100 ? DEFAULT_THRESHOLD_MS : thresholdMs;
|
|
3861
3919
|
const expiresAt = normalizeVtxo(vtxo).expiresAt;
|
|
3862
3920
|
if (expiresAt === void 0) return false;
|
|
@@ -3867,7 +3925,9 @@ function isVtxoExpiringSoon(vtxo, thresholdMs) {
|
|
|
3867
3925
|
}
|
|
3868
3926
|
function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount, now) {
|
|
3869
3927
|
return vtxos.filter(
|
|
3870
|
-
|
|
3928
|
+
// The leading exclusion covers the `isSubdust` arm too, which is a value
|
|
3929
|
+
// property and would otherwise re-admit an exited coin on its own.
|
|
3930
|
+
(vtxo) => !vtxo.isUnrolled && (isVtxoExpiringSoon(vtxo, thresholdMs) || canRecoverOnchain(vtxo, now) || isSubdust(vtxo, dustAmount))
|
|
3871
3931
|
);
|
|
3872
3932
|
}
|
|
3873
3933
|
function isMigrationCapable(wallet) {
|
|
@@ -4518,6 +4578,7 @@ var VtxoManager = class _VtxoManager {
|
|
|
4518
4578
|
async getDeprecatedSignerStatus() {
|
|
4519
4579
|
const wallet = this.requireMigrationCapableWallet();
|
|
4520
4580
|
const info = await wallet.arkProvider.getInfo();
|
|
4581
|
+
await wallet.settleServerInfoChanges?.();
|
|
4521
4582
|
const { reports: vtxoReports } = await this.classifyDeprecatedSignerContracts(info);
|
|
4522
4583
|
const { reports: boardingReports } = await this.classifyDeprecatedSignerBoarding(info);
|
|
4523
4584
|
return mergeSignerReports(vtxoReports, boardingReports);
|
|
@@ -4531,26 +4592,33 @@ var VtxoManager = class _VtxoManager {
|
|
|
4531
4592
|
*/
|
|
4532
4593
|
async migrateCore(options) {
|
|
4533
4594
|
const wallet = this.requireMigrationCapableWallet();
|
|
4595
|
+
const signerBeforeRefresh = hex.encode(wallet.arkServerPublicKey);
|
|
4534
4596
|
const info = await wallet.arkProvider.getInfo();
|
|
4597
|
+
await wallet.settleServerInfoChanges?.();
|
|
4598
|
+
const rotatedOnRefresh = signerBeforeRefresh !== hex.encode(wallet.arkServerPublicKey);
|
|
4535
4599
|
wallet.refreshDeprecatedSigners(info);
|
|
4536
4600
|
const signerSet = signerSetFromInfo(info);
|
|
4537
4601
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
4538
4602
|
const walletSignerHex = hex.encode(wallet.arkServerPublicKey);
|
|
4539
4603
|
const walletClass = classifyAgainstSignerSet(walletSignerHex, signerSet, nowSeconds);
|
|
4540
4604
|
if (signerSet.deprecated.size === 0 && walletClass.status === "CURRENT") {
|
|
4541
|
-
return { rotated:
|
|
4605
|
+
return { rotated: rotatedOnRefresh, expired: [], signers: [] };
|
|
4542
4606
|
}
|
|
4543
4607
|
if (walletClass.status === "UNKNOWN_SIGNER") {
|
|
4544
4608
|
const { reports: vtxoReports2 } = await this.classifyDeprecatedSignerContracts(info);
|
|
4545
4609
|
const { reports: boardingReports2 } = await this.classifyDeprecatedSignerBoarding(info);
|
|
4546
4610
|
return {
|
|
4547
|
-
|
|
4611
|
+
// Normally false here — a drained rotation targets the active
|
|
4612
|
+
// signer, which classifies CURRENT, not UNKNOWN. It is true only
|
|
4613
|
+
// when a second rotation landed inside the drain, leaving us on a
|
|
4614
|
+
// signer newer than `info`: still a rotation this pass applied.
|
|
4615
|
+
rotated: rotatedOnRefresh,
|
|
4548
4616
|
expired: [],
|
|
4549
4617
|
signers: mergeSignerReports(vtxoReports2, boardingReports2),
|
|
4550
4618
|
skipped: "unknown-wallet-signer"
|
|
4551
4619
|
};
|
|
4552
4620
|
}
|
|
4553
|
-
const rotated = await this.ensureReceiveOnActiveSigner(info);
|
|
4621
|
+
const rotated = await this.ensureReceiveOnActiveSigner(info) || rotatedOnRefresh;
|
|
4554
4622
|
const {
|
|
4555
4623
|
reports: vtxoReports,
|
|
4556
4624
|
migratable: vtxoMigratable,
|
|
@@ -4719,8 +4787,9 @@ var VtxoManager = class _VtxoManager {
|
|
|
4719
4787
|
if (!serverPubKey) continue;
|
|
4720
4788
|
const cls = classifyAgainstSignerSet(serverPubKey, signerSet, nowSeconds);
|
|
4721
4789
|
if (cls.status === "CURRENT") continue;
|
|
4722
|
-
const
|
|
4723
|
-
const
|
|
4790
|
+
const live = vtxos.filter((v) => !v.isUnrolled);
|
|
4791
|
+
const recoverable = live.filter((v) => v.isSwept && !hasTerminalSpend(v));
|
|
4792
|
+
const spendable = live.filter((v) => !hasTerminalSpend(v) && !v.isSwept);
|
|
4724
4793
|
const value = spendable.reduce((sum, v) => sum + v.value, 0);
|
|
4725
4794
|
let recoverableCount = 0;
|
|
4726
4795
|
let recoverableValue = 0;
|
|
@@ -6390,6 +6459,13 @@ var TxTree = class {
|
|
|
6390
6459
|
if (nbOfInputs !== 1) {
|
|
6391
6460
|
throw new Error(`unexpected number of inputs: ${nbOfInputs}, expected 1`);
|
|
6392
6461
|
}
|
|
6462
|
+
if (this.root.lockTime !== 0) {
|
|
6463
|
+
throw new Error(`unexpected locktime: ${this.root.lockTime}, expected 0`);
|
|
6464
|
+
}
|
|
6465
|
+
const sequence = this.root.getInput(0).sequence;
|
|
6466
|
+
if (sequence !== void 0 && sequence !== DEFAULT_SEQUENCE) {
|
|
6467
|
+
throw new Error(`unexpected sequence: ${sequence}, expected ${DEFAULT_SEQUENCE}`);
|
|
6468
|
+
}
|
|
6393
6469
|
if (this.children.size > nbOfOutputs - 1) {
|
|
6394
6470
|
throw new Error(
|
|
6395
6471
|
`unexpected number of children: ${this.children.size}, expected maximum ${nbOfOutputs - 1}`
|
|
@@ -8985,7 +9061,7 @@ var NON_TERMINAL_INTENT_STATES = ALL_INTENT_STATES.filter(
|
|
|
8985
9061
|
(s) => !isTerminalIntentState(s)
|
|
8986
9062
|
);
|
|
8987
9063
|
function isConsumed(vtxo) {
|
|
8988
|
-
return vtxo !== void 0 && (vtxo
|
|
9064
|
+
return vtxo !== void 0 && hasTerminalSpend(vtxo);
|
|
8989
9065
|
}
|
|
8990
9066
|
async function classifyIntent(intent, vtxosByOutpoint, now) {
|
|
8991
9067
|
if (isTerminalIntentState(intent.state)) return void 0;
|
|
@@ -8995,6 +9071,11 @@ async function classifyIntent(intent, vtxosByOutpoint, now) {
|
|
|
8995
9071
|
const commitmentTransactionId = intent.commitmentTransactionId ?? consumed.map((v) => v?.settledBy).find((s) => !!s);
|
|
8996
9072
|
return terminal(intent, "batch_succeeded", now, { commitmentTransactionId });
|
|
8997
9073
|
}
|
|
9074
|
+
if (consumed.some((vtxo) => vtxo?.isUnrolled)) {
|
|
9075
|
+
return terminal(intent, "cancelled", now, {
|
|
9076
|
+
cancellationReason: "reconciliation: an input was unilaterally exited"
|
|
9077
|
+
});
|
|
9078
|
+
}
|
|
8998
9079
|
if (intent.state === "waiting_to_submit") {
|
|
8999
9080
|
return terminal(intent, "cancelled", now, {
|
|
9000
9081
|
cancellationReason: "reconciliation: intent was never submitted before restart"
|
|
@@ -11604,7 +11685,9 @@ var WalletReceiveRotator = class _WalletReceiveRotator {
|
|
|
11604
11685
|
const { tapscript: newTapscript, params } = buildReceiveContract(
|
|
11605
11686
|
wallet.offchainTapscript,
|
|
11606
11687
|
descriptor,
|
|
11607
|
-
wallet.network.hrp
|
|
11688
|
+
wallet.network.hrp,
|
|
11689
|
+
true
|
|
11690
|
+
);
|
|
11608
11691
|
const newScript = params.script;
|
|
11609
11692
|
const manager = await wallet.getContractManager();
|
|
11610
11693
|
await manager.createContract(params);
|
|
@@ -11646,7 +11729,7 @@ function buildReceiveContract(current, descriptor, hrp, tagSource) {
|
|
|
11646
11729
|
// index-0 key. Without it, spends produce unsigned PSBTs that the
|
|
11647
11730
|
// server rejects with `INVALID_PSBT_INPUT (5): missing tapscript spend sig`.
|
|
11648
11731
|
metadata: {
|
|
11649
|
-
...{ source: WALLET_RECEIVE_SOURCE },
|
|
11732
|
+
...tagSource && { source: WALLET_RECEIVE_SOURCE },
|
|
11650
11733
|
signingDescriptor: descriptor
|
|
11651
11734
|
}
|
|
11652
11735
|
};
|
|
@@ -11730,6 +11813,7 @@ function computeOffchainBalance(vtxos, caps) {
|
|
|
11730
11813
|
let intentLocked = 0;
|
|
11731
11814
|
let recoverable = 0;
|
|
11732
11815
|
let pendingRecovery = 0;
|
|
11816
|
+
let unrolled = 0;
|
|
11733
11817
|
const owned = /* @__PURE__ */ new Map();
|
|
11734
11818
|
const spendable = /* @__PURE__ */ new Map();
|
|
11735
11819
|
const addAssets = (into, vtxo) => {
|
|
@@ -11740,6 +11824,10 @@ function computeOffchainBalance(vtxos, caps) {
|
|
|
11740
11824
|
for (const vtxo of vtxos) {
|
|
11741
11825
|
if (hasTerminalSpend(vtxo)) continue;
|
|
11742
11826
|
addAssets(owned, vtxo);
|
|
11827
|
+
if (vtxo.isUnrolled) {
|
|
11828
|
+
unrolled += vtxo.value;
|
|
11829
|
+
continue;
|
|
11830
|
+
}
|
|
11743
11831
|
if (isPendingRecovery(vtxo)) {
|
|
11744
11832
|
pendingRecovery += vtxo.value;
|
|
11745
11833
|
continue;
|
|
@@ -11772,7 +11860,8 @@ function computeOffchainBalance(vtxos, caps) {
|
|
|
11772
11860
|
intentLocked,
|
|
11773
11861
|
recoverable,
|
|
11774
11862
|
pendingRecovery,
|
|
11775
|
-
|
|
11863
|
+
unrolled,
|
|
11864
|
+
total: settled + preconfirmed + recoverable + pendingRecovery + unrolled,
|
|
11776
11865
|
assets: toAssets(owned),
|
|
11777
11866
|
availableAssets: toAssets(spendable)
|
|
11778
11867
|
};
|
|
@@ -11995,16 +12084,20 @@ function filterSnapshotVtxos(snapshot, filter, pendingSpendOutpoints) {
|
|
|
11995
12084
|
if (pendingSpendOutpoints.has(`${vtxo.txid}:${vtxo.vout}`)) {
|
|
11996
12085
|
return false;
|
|
11997
12086
|
}
|
|
11998
|
-
if (
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
return
|
|
12087
|
+
if (vtxo.isUnrolled) {
|
|
12088
|
+
return !!f.withUnrolled;
|
|
12089
|
+
}
|
|
12090
|
+
if (hasTerminalSpend(vtxo)) {
|
|
12091
|
+
return false;
|
|
12092
|
+
}
|
|
12093
|
+
if (!f.withRecoverable && canRecoverOnchain(vtxo, now)) {
|
|
12094
|
+
return false;
|
|
12003
12095
|
}
|
|
12004
|
-
return
|
|
12096
|
+
return true;
|
|
12005
12097
|
});
|
|
12006
12098
|
}
|
|
12007
12099
|
var PENDING_RECOVERY_REASON = "is past its operator signer's rotation cutoff \u2014 the operator will not co-sign it until it recovers";
|
|
12100
|
+
var UNROLLED_REASON = "was unilaterally exited and lives onchain \u2014 `Unroll.completeUnroll` is the only spend that reaches it";
|
|
12008
12101
|
function omittedOutpoints(before, after) {
|
|
12009
12102
|
if (before.length === after.length) return /* @__PURE__ */ new Set();
|
|
12010
12103
|
const kept = new Set(after.map((vtxo) => `${vtxo.txid}:${vtxo.vout}`));
|
|
@@ -12504,7 +12597,11 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
12504
12597
|
this.getBoardingUtxos(),
|
|
12505
12598
|
this.contractSnapshot()
|
|
12506
12599
|
]);
|
|
12507
|
-
const vtxos = filterSnapshotVtxos(
|
|
12600
|
+
const vtxos = filterSnapshotVtxos(
|
|
12601
|
+
snapshot,
|
|
12602
|
+
{ withRecoverable: true, withUnrolled: true },
|
|
12603
|
+
this._pendingSpendOutpoints
|
|
12604
|
+
);
|
|
12508
12605
|
let confirmed = 0;
|
|
12509
12606
|
let unconfirmed = 0;
|
|
12510
12607
|
for (const utxo of boardingUtxos) {
|
|
@@ -12532,6 +12629,7 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
12532
12629
|
intentLocked: offchain.intentLocked,
|
|
12533
12630
|
recoverable: offchain.recoverable,
|
|
12534
12631
|
pendingRecovery: offchain.pendingRecovery,
|
|
12632
|
+
unrolled: offchain.unrolled,
|
|
12535
12633
|
total: totalBoarding + offchain.total,
|
|
12536
12634
|
assets: offchain.assets,
|
|
12537
12635
|
availableAssets: offchain.availableAssets
|
|
@@ -12572,6 +12670,13 @@ var ReadonlyWallet = class _ReadonlyWallet {
|
|
|
12572
12670
|
"is locked by an in-flight settlement intent"
|
|
12573
12671
|
)
|
|
12574
12672
|
]);
|
|
12673
|
+
if (!filter?.withUnrolled) {
|
|
12674
|
+
logExcludedVtxos(
|
|
12675
|
+
"getSpendableVtxos",
|
|
12676
|
+
snapshot.flatMap((contract) => contract.vtxos).filter((vtxo) => vtxo.isUnrolled && !hasTerminalSpend(vtxo)),
|
|
12677
|
+
[() => UNROLLED_REASON]
|
|
12678
|
+
);
|
|
12679
|
+
}
|
|
12575
12680
|
return unlocked;
|
|
12576
12681
|
}
|
|
12577
12682
|
/**
|
|
@@ -13391,6 +13496,27 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
13391
13496
|
console.warn("server-signer rotation on info change failed", e);
|
|
13392
13497
|
}
|
|
13393
13498
|
}
|
|
13499
|
+
/**
|
|
13500
|
+
* Await an `onServerInfoChanged` handler still applying a rotation.
|
|
13501
|
+
*
|
|
13502
|
+
* The provider emits synchronously but {@link handleServerInfoChanged} runs
|
|
13503
|
+
* off the emit, so between the two a caller can observe a half-applied
|
|
13504
|
+
* rotation: {@link rotateServerSigner} persists the active signer's contract
|
|
13505
|
+
* rows *before* committing the tapscripts, so `arkServerPublicKey` still
|
|
13506
|
+
* reads the old key while the new key's rows are already in the repository.
|
|
13507
|
+
* A path that refreshes server info and then reads signer-derived state must
|
|
13508
|
+
* drain the chain first, or it both reads that torn state and races the
|
|
13509
|
+
* handler for the rotation itself.
|
|
13510
|
+
*
|
|
13511
|
+
* Resolves once the chain is idle; a handler that threw already logged, and
|
|
13512
|
+
* its rejection is swallowed here.
|
|
13513
|
+
*
|
|
13514
|
+
* @internal Invoked by {@link dispose} and the {@link VtxoManager} migration
|
|
13515
|
+
* pass; not part of the stable public API.
|
|
13516
|
+
*/
|
|
13517
|
+
async settleServerInfoChanges() {
|
|
13518
|
+
await this._serverInfoInFlight?.catch(() => void 0);
|
|
13519
|
+
}
|
|
13394
13520
|
_receiveRotatorInstalled = false;
|
|
13395
13521
|
/**
|
|
13396
13522
|
* Descriptor-aware signer used by {@link _signerRouter} to sign
|
|
@@ -13525,43 +13651,250 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
13525
13651
|
* provider and keeps a single index-0 boarding address for its lifetime,
|
|
13526
13652
|
* so this returns the existing {@link getBoardingAddress} unchanged
|
|
13527
13653
|
* (no rotation, no index burned).
|
|
13654
|
+
*
|
|
13655
|
+
* **Behaviour change.** A wallet configured with a custom
|
|
13656
|
+
* {@link DescriptorProvider} (`walletMode: <provider>`) now rotates here.
|
|
13657
|
+
* It previously did not: allocation went through the contract manager,
|
|
13658
|
+
* which only wires an `allocate` hook for {@link HDDescriptorProvider}, so
|
|
13659
|
+
* such a wallet read as "declined to allocate" and silently kept its
|
|
13660
|
+
* index-0 boarding address forever. Those wallets now burn an index per
|
|
13661
|
+
* call — including via {@link maybeRotateBoardingAfterBoard}, which fires
|
|
13662
|
+
* on every settle that consumes a boarding UTXO. Funds at retired
|
|
13663
|
+
* addresses stay reachable: each rotation persists its `boarding` contract
|
|
13664
|
+
* before swapping, and {@link getBoardingUtxos} fans out over the full
|
|
13665
|
+
* historical set.
|
|
13666
|
+
*
|
|
13667
|
+
* @deprecated Use {@link getNewAddresses} — it mints any combination of
|
|
13668
|
+
* address types at one shared index, and reports the contract row behind
|
|
13669
|
+
* each. Note the difference in display behaviour: this method *swaps* the
|
|
13670
|
+
* advertised boarding address, where `getNewAddresses` mints side
|
|
13671
|
+
* addresses and leaves {@link getBoardingAddress} alone. To keep this
|
|
13672
|
+
* method's behaviour, keep calling this method.
|
|
13528
13673
|
*/
|
|
13529
13674
|
async getNewBoardingAddress() {
|
|
13530
|
-
const
|
|
13531
|
-
if (!
|
|
13532
|
-
|
|
13675
|
+
const [minted] = await this.allocateAddresses(["boarding"], false, true);
|
|
13676
|
+
if (!minted?.allocated) return this.getBoardingAddress();
|
|
13677
|
+
if (minted.type === "boarding") this.setBoardingTapscriptForRotation(minted.tapscript);
|
|
13678
|
+
return minted.address;
|
|
13679
|
+
}
|
|
13680
|
+
/**
|
|
13681
|
+
* Allocate a *fresh* address of each requested type, all derived from one
|
|
13682
|
+
* newly allocated HD index.
|
|
13683
|
+
*
|
|
13684
|
+
* This is the explicit allocator the receive path otherwise lacks:
|
|
13685
|
+
* {@link getAddress} and {@link getBoardingAddress} are stable reads of the
|
|
13686
|
+
* wallet's display addresses, and the display receive address only advances
|
|
13687
|
+
* when a payment arrives ({@link WalletReceiveRotator} rotates on
|
|
13688
|
+
* `vtxo_received`). Issuing a second address before the first is paid — one
|
|
13689
|
+
* invoice for Alice, another for Bob — has to go through here.
|
|
13690
|
+
*
|
|
13691
|
+
* Each call:
|
|
13692
|
+
*
|
|
13693
|
+
* - allocates **one** index from the shared HD stream, however many types
|
|
13694
|
+
* were asked for, so a `default` + `boarding` pair are siblings rather
|
|
13695
|
+
* than two burnt indices;
|
|
13696
|
+
* - builds each requested script at that index, preserving every other
|
|
13697
|
+
* option of the wallet's current script for that flavour (including a
|
|
13698
|
+
* delegate wallet's `delegate` shape);
|
|
13699
|
+
* - persists each as an `active` contract carrying its `signingDescriptor`,
|
|
13700
|
+
* so the ContractWatcher monitors it, the balance counts it, and
|
|
13701
|
+
* {@link signerForDescriptor} can recover the key;
|
|
13702
|
+
* - slides the look-ahead band, since the watermark moved past indices an
|
|
13703
|
+
* external issuer may still be handing out.
|
|
13704
|
+
*
|
|
13705
|
+
* The minted rows are deliberately left **untagged**: the boot lookups
|
|
13706
|
+
* adopt the newest {@link WALLET_RECEIVE_SOURCE}-tagged row as the display
|
|
13707
|
+
* address, and a side address issued to one counterparty must not become
|
|
13708
|
+
* the address the wallet advertises to everyone else. {@link getAddress}
|
|
13709
|
+
* and {@link getBoardingAddress} are unchanged by this call.
|
|
13710
|
+
*
|
|
13711
|
+
* A wallet with no HD stream (`walletMode: 'static'` / `'auto'`) has one
|
|
13712
|
+
* address per flavour for its lifetime. Without `forceNew` it returns those
|
|
13713
|
+
* — the real persisted rows, no index burned; with `forceNew` it throws
|
|
13714
|
+
* {@link WalletCannotAllocateAddressError} rather than hand back an address
|
|
13715
|
+
* that is not in fact fresh.
|
|
13716
|
+
*
|
|
13717
|
+
* @example
|
|
13718
|
+
* ```typescript
|
|
13719
|
+
* const [invoice] = await wallet.getNewAddresses({ forceNew: true });
|
|
13720
|
+
* // hand `invoice.address` to Alice, keep the descriptor with the invoice
|
|
13721
|
+
* const signer = await wallet.signerForDescriptor(invoice.signingDescriptor);
|
|
13722
|
+
* ```
|
|
13723
|
+
*/
|
|
13724
|
+
async getNewAddresses(opts) {
|
|
13725
|
+
const minted = await this.allocateAddresses(
|
|
13726
|
+
opts?.types ?? ["default"],
|
|
13727
|
+
opts?.forceNew ?? false,
|
|
13728
|
+
false
|
|
13729
|
+
);
|
|
13730
|
+
return minted.map(({ address, signingDescriptor, contract }) => ({
|
|
13731
|
+
address,
|
|
13732
|
+
signingDescriptor,
|
|
13733
|
+
contract
|
|
13734
|
+
}));
|
|
13735
|
+
}
|
|
13736
|
+
/**
|
|
13737
|
+
* Shared allocation core behind {@link getNewAddresses} and the deprecated
|
|
13738
|
+
* {@link getNewBoardingAddress}, so the two cannot drift on what an
|
|
13739
|
+
* allocation costs or what gets persisted.
|
|
13740
|
+
*
|
|
13741
|
+
* @param tagSource - Tag the persisted rows {@link WALLET_RECEIVE_SOURCE},
|
|
13742
|
+
* making the next boot adopt them as the display address. Only the
|
|
13743
|
+
* deprecated boarding allocator passes `true`; `getNewAddresses` mints side
|
|
13744
|
+
* addresses and must not hijack what the wallet advertises.
|
|
13745
|
+
*/
|
|
13746
|
+
async allocateAddresses(types, forceNew, tagSource) {
|
|
13747
|
+
if (types.length === 0) {
|
|
13748
|
+
throw new Error("getNewAddresses: `types` must name at least one address type");
|
|
13533
13749
|
}
|
|
13534
13750
|
const manager = await this.getContractManager();
|
|
13535
|
-
const
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
|
|
13751
|
+
const provider = this._descriptorProvider;
|
|
13752
|
+
const descriptor = !provider ? void 0 : provider instanceof HDDescriptorProvider ? await manager.getNextSigningDescriptor() : await provider.getNextSigningDescriptor();
|
|
13753
|
+
if (!descriptor) {
|
|
13754
|
+
if (forceNew) {
|
|
13755
|
+
throw new WalletCannotAllocateAddressError(
|
|
13756
|
+
provider ? "the descriptor provider declined to allocate an index" : "this wallet has no HD stream (walletMode 'static' / 'auto')"
|
|
13757
|
+
);
|
|
13758
|
+
}
|
|
13759
|
+
return this.currentAddresses(manager, types);
|
|
13760
|
+
}
|
|
13761
|
+
const allocated = [];
|
|
13762
|
+
try {
|
|
13763
|
+
for (const type of types) {
|
|
13764
|
+
allocated.push(await this.mintAddress(manager, type, descriptor, tagSource, true));
|
|
13765
|
+
}
|
|
13766
|
+
} finally {
|
|
13767
|
+
try {
|
|
13768
|
+
await manager.refillLookAhead();
|
|
13769
|
+
} catch (e) {
|
|
13770
|
+
console.warn("look-ahead refill after address allocation failed", e);
|
|
13771
|
+
}
|
|
13772
|
+
}
|
|
13773
|
+
return allocated;
|
|
13774
|
+
}
|
|
13775
|
+
/**
|
|
13776
|
+
* The wallet's existing display addresses, as persisted contract rows —
|
|
13777
|
+
* the honest answer for a wallet with no index to burn.
|
|
13778
|
+
*
|
|
13779
|
+
* Each type is minted from the descriptor that already owns its display
|
|
13780
|
+
* script ({@link displayDescriptor}), not from the identity key, so the
|
|
13781
|
+
* returned `signingDescriptor` is the one that signs for the returned
|
|
13782
|
+
* `address` even on a wallet that has rotated.
|
|
13783
|
+
*/
|
|
13784
|
+
async currentAddresses(manager, types) {
|
|
13785
|
+
const out = [];
|
|
13786
|
+
for (const type of types) {
|
|
13787
|
+
const descriptor = await this.displayDescriptor(manager, type);
|
|
13788
|
+
out.push(await this.mintAddress(manager, type, descriptor, false, false));
|
|
13789
|
+
}
|
|
13790
|
+
return out;
|
|
13791
|
+
}
|
|
13792
|
+
/**
|
|
13793
|
+
* The descriptor that owns the wallet's current display script for `type`,
|
|
13794
|
+
* read off the row the wallet persisted when it adopted that script.
|
|
13795
|
+
*
|
|
13796
|
+
* Deriving it from the identity instead is only correct for a wallet that
|
|
13797
|
+
* never rotates. On one that has — an HD wallet whose provider later
|
|
13798
|
+
* declines to allocate — the display script belongs to an HD child, and
|
|
13799
|
+
* pairing its address with the identity descriptor hands the caller a
|
|
13800
|
+
* {@link NewAddress} whose `signingDescriptor` resolves, via
|
|
13801
|
+
* {@link signerForDescriptor}, to a signer holding the wrong key.
|
|
13802
|
+
*
|
|
13803
|
+
* Falls back to the identity's pathless `tr(pubkey)` when no row carries a
|
|
13804
|
+
* descriptor: the static / `auto` case, where the identity key genuinely
|
|
13805
|
+
* owns the display script and this is what `getNextSigningDescriptor`
|
|
13806
|
+
* answers too. (The index-0 baselines registered at construction carry no
|
|
13807
|
+
* `signingDescriptor` metadata, so those wallets take this path.)
|
|
13808
|
+
*/
|
|
13809
|
+
async displayDescriptor(manager, type) {
|
|
13810
|
+
const display = type === "boarding" ? this._boardingTapscript : this.offchainTapscript;
|
|
13811
|
+
const [row] = await manager.getContracts({ script: hex.encode(display.pkScript) });
|
|
13812
|
+
const persisted = row?.metadata?.signingDescriptor;
|
|
13813
|
+
if (typeof persisted === "string") return persisted;
|
|
13814
|
+
return identityDescriptor(this.identity);
|
|
13815
|
+
}
|
|
13816
|
+
/**
|
|
13817
|
+
* Build, persist and shape one address of `type` owned by `descriptor` —
|
|
13818
|
+
* the single build-and-register path behind both {@link allocateAddresses}
|
|
13819
|
+
* and {@link currentAddresses}, so a freshly allocated address and a
|
|
13820
|
+
* re-registered display address cannot disagree on what gets written.
|
|
13821
|
+
*
|
|
13822
|
+
* The descriptor is the sole source of the script's owner key in both
|
|
13823
|
+
* branches. Deriving one flavour from the descriptor and the other from a
|
|
13824
|
+
* tapscript field is what lets `address` and `signingDescriptor` drift
|
|
13825
|
+
* apart.
|
|
13826
|
+
*/
|
|
13827
|
+
async mintAddress(manager, type, descriptor, tagSource, allocated) {
|
|
13828
|
+
if (type === "boarding") {
|
|
13829
|
+
const built2 = this.buildBoardingContract(
|
|
13830
|
+
deriveDescriptorLeafPubKey(descriptor),
|
|
13831
|
+
descriptor,
|
|
13832
|
+
tagSource
|
|
13833
|
+
);
|
|
13834
|
+
const contract2 = await manager.createContract(built2.params);
|
|
13835
|
+
return {
|
|
13836
|
+
type,
|
|
13837
|
+
// `built.tapscript` is a `DefaultVtxo.Script` here without a
|
|
13838
|
+
// cast — the branch, not a widened local, does the narrowing.
|
|
13839
|
+
tapscript: built2.tapscript,
|
|
13840
|
+
// A boarding row persists the *ark* encoding of its script, so
|
|
13841
|
+
// `contract.address` is unusable for an onchain sender: the
|
|
13842
|
+
// onchain form has to be re-derived.
|
|
13843
|
+
address: built2.tapscript.onchainAddress(this.network),
|
|
13844
|
+
signingDescriptor: descriptor,
|
|
13845
|
+
contract: contract2,
|
|
13846
|
+
allocated
|
|
13847
|
+
};
|
|
13848
|
+
}
|
|
13849
|
+
const built = buildReceiveContract(
|
|
13850
|
+
this.offchainTapscript,
|
|
13851
|
+
descriptor,
|
|
13852
|
+
this.network.hrp,
|
|
13853
|
+
tagSource
|
|
13854
|
+
);
|
|
13855
|
+
const contract = await manager.createContract(built.params);
|
|
13856
|
+
return {
|
|
13857
|
+
type,
|
|
13858
|
+
tapscript: built.tapscript,
|
|
13859
|
+
// Offchain rows already store the address they advertise, and
|
|
13860
|
+
// reading it back keeps a coalesced row (`createContract` is
|
|
13861
|
+
// first-wins on script) honest about what was actually persisted.
|
|
13862
|
+
address: contract.address,
|
|
13863
|
+
signingDescriptor: descriptor,
|
|
13864
|
+
contract,
|
|
13865
|
+
allocated
|
|
13866
|
+
};
|
|
13867
|
+
}
|
|
13868
|
+
/**
|
|
13869
|
+
* Build the boarding contract owned by `pubKey`, keeping every other option
|
|
13870
|
+
* of the wallet's current boarding tapscript (notably the boarding-exit
|
|
13871
|
+
* CSV, which is index-independent). The offchain analogue lives in
|
|
13872
|
+
* {@link buildReceiveContract}.
|
|
13873
|
+
*/
|
|
13874
|
+
buildBoardingContract(pubKey, descriptor, tagSource) {
|
|
13875
|
+
const tapscript = new DefaultVtxo.Script({
|
|
13539
13876
|
...this._boardingTapscript.options,
|
|
13540
13877
|
pubKey
|
|
13541
13878
|
});
|
|
13542
|
-
const csvTimelock =
|
|
13543
|
-
|
|
13544
|
-
|
|
13879
|
+
const csvTimelock = tapscript.options.csvTimelock ?? DefaultVtxo.Script.DEFAULT_TIMELOCK;
|
|
13880
|
+
return {
|
|
13881
|
+
tapscript,
|
|
13545
13882
|
params: {
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13883
|
+
type: "boarding",
|
|
13884
|
+
params: {
|
|
13885
|
+
pubKey: hex.encode(pubKey),
|
|
13886
|
+
serverPubKey: hex.encode(tapscript.options.serverPubKey),
|
|
13887
|
+
csvTimelock: timelockToSequence(csvTimelock).toString()
|
|
13888
|
+
},
|
|
13889
|
+
script: hex.encode(tapscript.pkScript),
|
|
13890
|
+
address: tapscript.address(this.network.hrp, this.arkServerPublicKey).encode(),
|
|
13891
|
+
state: "active",
|
|
13892
|
+
metadata: {
|
|
13893
|
+
...tagSource && { source: WALLET_RECEIVE_SOURCE },
|
|
13894
|
+
signingDescriptor: descriptor
|
|
13895
|
+
}
|
|
13556
13896
|
}
|
|
13557
|
-
}
|
|
13558
|
-
this.setBoardingTapscriptForRotation(newBoarding);
|
|
13559
|
-
try {
|
|
13560
|
-
await manager.refillLookAhead();
|
|
13561
|
-
} catch (e) {
|
|
13562
|
-
console.warn("look-ahead refill after boarding allocation failed", e);
|
|
13563
|
-
}
|
|
13564
|
-
return newBoarding.onchainAddress(this.network);
|
|
13897
|
+
};
|
|
13565
13898
|
}
|
|
13566
13899
|
/**
|
|
13567
13900
|
* @see HDWalletCapable.getCurrentSigningDescriptor
|
|
@@ -13946,7 +14279,7 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
13946
14279
|
await this._restoreInFlight?.catch(() => void 0);
|
|
13947
14280
|
this._serverInfoUnsub?.();
|
|
13948
14281
|
this._serverInfoUnsub = void 0;
|
|
13949
|
-
await this.
|
|
14282
|
+
await this.settleServerInfoChanges();
|
|
13950
14283
|
let rotatorError;
|
|
13951
14284
|
try {
|
|
13952
14285
|
await this._receiveRotator?.dispose();
|
|
@@ -14478,7 +14811,10 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
14478
14811
|
* `typeof` guard skips the arknote strings settle also accepts.
|
|
14479
14812
|
*
|
|
14480
14813
|
* No-ops for static / `auto` wallets (no descriptor provider — boarding
|
|
14481
|
-
* stays on its fixed index-0 address).
|
|
14814
|
+
* stays on its fixed index-0 address). Wallets on a custom
|
|
14815
|
+
* {@link DescriptorProvider} DO rotate here now, where they previously
|
|
14816
|
+
* no-opped — see the behaviour-change note on {@link
|
|
14817
|
+
* getNewBoardingAddress}. Best-effort and non-fatal: the
|
|
14482
14818
|
* settle has already committed and its txid must be returned, so a
|
|
14483
14819
|
* rotation failure is logged and swallowed rather than thrown. Funds at
|
|
14484
14820
|
* the retired boarding address remain discoverable — the old `boarding`
|
|
@@ -14893,9 +15229,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
14893
15229
|
seen.add(pendingTx.arkTxid);
|
|
14894
15230
|
batchPending.push(pendingTx.arkTxid);
|
|
14895
15231
|
try {
|
|
14896
|
-
const checkpointTxs = pendingTx.signedCheckpointTxs.map(
|
|
14897
|
-
|
|
14898
|
-
|
|
15232
|
+
const checkpointTxs = pendingTx.signedCheckpointTxs.map((c) => {
|
|
15233
|
+
const tx = Transaction$2.fromPSBT(base64.decode(c));
|
|
15234
|
+
assertAllowedSighashTypes(tx);
|
|
15235
|
+
return tx;
|
|
15236
|
+
});
|
|
14899
15237
|
assertCheckpointsMatchInputs(
|
|
14900
15238
|
checkpointTxs,
|
|
14901
15239
|
checkpointInputs,
|
|
@@ -15063,7 +15401,9 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
15063
15401
|
const myPkScript = ArkAddress.decode(await this.getAddress()).pkScript;
|
|
15064
15402
|
let { spendable, unclaimed } = this.classifyCashVtxos(vtxos);
|
|
15065
15403
|
let swept = 0;
|
|
15066
|
-
const drainable = vtxos.filter(
|
|
15404
|
+
const drainable = vtxos.filter(
|
|
15405
|
+
(vtxo) => !isSubdust(vtxo, this.dustAmount) && !vtxo.isUnrolled
|
|
15406
|
+
);
|
|
15067
15407
|
if (drainable.length > 0) {
|
|
15068
15408
|
let drained = { count: 0, swept: 0, claimed: /* @__PURE__ */ new Set() };
|
|
15069
15409
|
try {
|
|
@@ -15136,6 +15476,8 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
15136
15476
|
for (const vtxo of vtxos) {
|
|
15137
15477
|
if (hasTerminalSpend(vtxo)) {
|
|
15138
15478
|
unclaimed.push(cashReport(vtxo, "already-spent"));
|
|
15479
|
+
} else if (vtxo.isUnrolled) {
|
|
15480
|
+
unclaimed.push(cashReport(vtxo, "exited"));
|
|
15139
15481
|
} else if (isSubdust(vtxo, this.dustAmount)) {
|
|
15140
15482
|
unclaimed.push(cashReport(vtxo, "subdust"));
|
|
15141
15483
|
} else if (vtxo.isSwept) {
|
|
@@ -15221,9 +15563,11 @@ var Wallet2 = class _Wallet extends ReadonlyWallet {
|
|
|
15221
15563
|
const unrollCandidates = this.checkpointUnrollCandidates(await this.arkProvider.getInfo());
|
|
15222
15564
|
for (const pendingTx of pendingTxs) {
|
|
15223
15565
|
try {
|
|
15224
|
-
const checkpointTxs = pendingTx.signedCheckpointTxs.map(
|
|
15225
|
-
|
|
15226
|
-
|
|
15566
|
+
const checkpointTxs = pendingTx.signedCheckpointTxs.map((checkpoint) => {
|
|
15567
|
+
const tx = Transaction$2.fromPSBT(base64.decode(checkpoint));
|
|
15568
|
+
assertAllowedSighashTypes(tx);
|
|
15569
|
+
return tx;
|
|
15570
|
+
});
|
|
15227
15571
|
assertCheckpointsMatchInputs(
|
|
15228
15572
|
checkpointTxs,
|
|
15229
15573
|
inputs.map((input) => ({
|
|
@@ -16026,6 +16370,11 @@ var MessageBus = class {
|
|
|
16026
16370
|
buildServicesFn;
|
|
16027
16371
|
boundOnMessage = this.onMessage.bind(this);
|
|
16028
16372
|
intentRepository;
|
|
16373
|
+
/** Pending broadcasts, drained FIFO by a single {@link drain} loop. */
|
|
16374
|
+
broadcastQueue = [];
|
|
16375
|
+
draining = false;
|
|
16376
|
+
/** Bumped wherever the generation owning the queued broadcasts ends. */
|
|
16377
|
+
broadcastEpoch = 0;
|
|
16029
16378
|
/** Start the message bus and attach service-worker event listeners. */
|
|
16030
16379
|
async start() {
|
|
16031
16380
|
if (this.running) return;
|
|
@@ -16048,6 +16397,7 @@ var MessageBus = class {
|
|
|
16048
16397
|
this.running = false;
|
|
16049
16398
|
this.tickInProgress = false;
|
|
16050
16399
|
this.initialized = false;
|
|
16400
|
+
this.invalidateBroadcasts();
|
|
16051
16401
|
if (this.tickTimeout !== null) {
|
|
16052
16402
|
self.clearTimeout(this.tickTimeout);
|
|
16053
16403
|
this.tickTimeout = null;
|
|
@@ -16060,6 +16410,78 @@ var MessageBus = class {
|
|
|
16060
16410
|
self.removeEventListener("message", this.boundOnMessage);
|
|
16061
16411
|
await Promise.all(Array.from(this.handlers.values()).map((updater) => updater.stop()));
|
|
16062
16412
|
}
|
|
16413
|
+
/** Invalidate every broadcast belonging to the outgoing generation. */
|
|
16414
|
+
invalidateBroadcasts() {
|
|
16415
|
+
this.broadcastEpoch++;
|
|
16416
|
+
this.broadcastQueue.length = 0;
|
|
16417
|
+
}
|
|
16418
|
+
broadcastToClients(responses) {
|
|
16419
|
+
this.broadcastQueue.push(...responses);
|
|
16420
|
+
void this.drain();
|
|
16421
|
+
}
|
|
16422
|
+
/**
|
|
16423
|
+
* Single fan-out loop, so pushes and tick responses keep one FIFO order.
|
|
16424
|
+
* A helper posting per response would race: `clients.matchAll()` gives no
|
|
16425
|
+
* cross-call resolution order, and it would de-batch `runTick`.
|
|
16426
|
+
*/
|
|
16427
|
+
async drain() {
|
|
16428
|
+
if (this.draining) return;
|
|
16429
|
+
this.draining = true;
|
|
16430
|
+
try {
|
|
16431
|
+
while (this.broadcastQueue.length) {
|
|
16432
|
+
const epoch = this.broadcastEpoch;
|
|
16433
|
+
const batch = this.broadcastQueue.splice(0);
|
|
16434
|
+
if (!this.running) {
|
|
16435
|
+
this.logDroppedBroadcasts(batch, "bus stopped");
|
|
16436
|
+
return;
|
|
16437
|
+
}
|
|
16438
|
+
let clients;
|
|
16439
|
+
try {
|
|
16440
|
+
clients = await self.clients.matchAll({
|
|
16441
|
+
includeUncontrolled: true,
|
|
16442
|
+
type: "window"
|
|
16443
|
+
});
|
|
16444
|
+
} catch (err) {
|
|
16445
|
+
this.logDroppedBroadcasts(batch, "clients.matchAll failed", err);
|
|
16446
|
+
continue;
|
|
16447
|
+
}
|
|
16448
|
+
if (!this.running) {
|
|
16449
|
+
this.logDroppedBroadcasts(batch, "bus stopped during matchAll");
|
|
16450
|
+
return;
|
|
16451
|
+
}
|
|
16452
|
+
if (this.broadcastEpoch !== epoch) {
|
|
16453
|
+
this.logDroppedBroadcasts(batch, "re-init landed during matchAll");
|
|
16454
|
+
continue;
|
|
16455
|
+
}
|
|
16456
|
+
for (const message of batch) {
|
|
16457
|
+
for (const client of clients) {
|
|
16458
|
+
try {
|
|
16459
|
+
client.postMessage(message);
|
|
16460
|
+
} catch (err) {
|
|
16461
|
+
if (this.debug)
|
|
16462
|
+
console.warn(`[${message.tag}] broadcast to client failed`, err);
|
|
16463
|
+
}
|
|
16464
|
+
}
|
|
16465
|
+
}
|
|
16466
|
+
}
|
|
16467
|
+
} catch {
|
|
16468
|
+
} finally {
|
|
16469
|
+
this.draining = false;
|
|
16470
|
+
if (this.running && this.broadcastQueue.length) void this.drain();
|
|
16471
|
+
}
|
|
16472
|
+
}
|
|
16473
|
+
/**
|
|
16474
|
+
* A dropped batch has no periodic recovery to fall back on now that events
|
|
16475
|
+
* no longer ride the tick, so name the loss under {@link Options.debug}.
|
|
16476
|
+
*/
|
|
16477
|
+
logDroppedBroadcasts(batch, reason, ...error) {
|
|
16478
|
+
if (!this.debug) return;
|
|
16479
|
+
console.warn(
|
|
16480
|
+
`dropping ${batch.length} broadcast(s): ${reason}`,
|
|
16481
|
+
batch.map((response) => response.tag),
|
|
16482
|
+
...error
|
|
16483
|
+
);
|
|
16484
|
+
}
|
|
16063
16485
|
scheduleNextTick() {
|
|
16064
16486
|
if (!this.running) return;
|
|
16065
16487
|
if (this.tickTimeout !== null) return;
|
|
@@ -16088,16 +16510,7 @@ var MessageBus = class {
|
|
|
16088
16510
|
if (this.debug)
|
|
16089
16511
|
console.log(`[${updater.messageTag}] outgoing tick response:`, response);
|
|
16090
16512
|
if (response && response.length > 0) {
|
|
16091
|
-
|
|
16092
|
-
includeUncontrolled: true,
|
|
16093
|
-
type: "window"
|
|
16094
|
-
}).then((clients) => {
|
|
16095
|
-
for (const message of response) {
|
|
16096
|
-
clients.forEach((client) => {
|
|
16097
|
-
client.postMessage(message);
|
|
16098
|
-
});
|
|
16099
|
-
}
|
|
16100
|
-
});
|
|
16513
|
+
this.broadcastToClients(response);
|
|
16101
16514
|
}
|
|
16102
16515
|
} catch (err) {
|
|
16103
16516
|
if (this.debug) console.error(`[${updater.messageTag}] tick failed`, err);
|
|
@@ -16136,6 +16549,14 @@ var MessageBus = class {
|
|
|
16136
16549
|
self.clearTimeout(this.tickTimeout);
|
|
16137
16550
|
this.tickTimeout = null;
|
|
16138
16551
|
}
|
|
16552
|
+
this.invalidateBroadcasts();
|
|
16553
|
+
const epoch = this.broadcastEpoch;
|
|
16554
|
+
const channel = {
|
|
16555
|
+
broadcast: (response) => {
|
|
16556
|
+
if (epoch !== this.broadcastEpoch) return;
|
|
16557
|
+
this.broadcastToClients([response]);
|
|
16558
|
+
}
|
|
16559
|
+
};
|
|
16139
16560
|
if (this.initialized) {
|
|
16140
16561
|
this.initialized = false;
|
|
16141
16562
|
await Promise.all(
|
|
@@ -16152,9 +16573,7 @@ var MessageBus = class {
|
|
|
16152
16573
|
try {
|
|
16153
16574
|
for (const updater of this.handlers.values()) {
|
|
16154
16575
|
if (this.debug) console.log(`Starting updater: ${updater.messageTag}`);
|
|
16155
|
-
await updater.start(services, {
|
|
16156
|
-
walletRepository: this.walletRepository
|
|
16157
|
-
});
|
|
16576
|
+
await updater.start(services, { walletRepository: this.walletRepository }, channel);
|
|
16158
16577
|
started.push(updater);
|
|
16159
16578
|
}
|
|
16160
16579
|
} catch (err) {
|
|
@@ -16824,6 +17243,17 @@ var WalletMessageHandler = class {
|
|
|
16824
17243
|
incomingFundsSubscription;
|
|
16825
17244
|
contractEventsSubscription;
|
|
16826
17245
|
onNextTick = [];
|
|
17246
|
+
channel;
|
|
17247
|
+
// One counter per subscription field, bumped exactly where that field is
|
|
17248
|
+
// dropped — never where a neighbour's is. They cannot be merged:
|
|
17249
|
+
// `onWalletInitialized` re-subscribes incoming funds while
|
|
17250
|
+
// `ensureContractEventBroadcasting` deliberately keeps its subscription, so
|
|
17251
|
+
// a shared bump would leave the live contract-event emitter permanently
|
|
17252
|
+
// stale, and folding requests in would truncate an in-flight settle.
|
|
17253
|
+
incomingFundsEpoch = 0;
|
|
17254
|
+
contractEventsEpoch = 0;
|
|
17255
|
+
/** Per-request progress callbacks: ends with the handler, not with a re-init. */
|
|
17256
|
+
requestEpoch = 0;
|
|
16827
17257
|
/**
|
|
16828
17258
|
* Instantiate a new WalletUpdater.
|
|
16829
17259
|
* Can override the default `messageTag` allowing more than one updater to run in parallel.
|
|
@@ -16834,13 +17264,20 @@ var WalletMessageHandler = class {
|
|
|
16834
17264
|
}
|
|
16835
17265
|
// lifecycle methods
|
|
16836
17266
|
async start(...params) {
|
|
16837
|
-
const [services, repositories] = params;
|
|
17267
|
+
const [services, repositories, channel] = params;
|
|
16838
17268
|
this.readonlyWallet = services.readonlyWallet;
|
|
16839
17269
|
this.wallet = services.wallet;
|
|
16840
17270
|
this.arkProvider = services.arkProvider;
|
|
16841
17271
|
this.walletRepository = repositories.walletRepository;
|
|
17272
|
+
this.channel = channel;
|
|
17273
|
+
this.onNextTick = [];
|
|
16842
17274
|
}
|
|
16843
17275
|
async stop() {
|
|
17276
|
+
this.requestEpoch++;
|
|
17277
|
+
this.incomingFundsEpoch++;
|
|
17278
|
+
this.contractEventsEpoch++;
|
|
17279
|
+
this.channel = void 0;
|
|
17280
|
+
this.onNextTick = [];
|
|
16844
17281
|
if (this.incomingFundsSubscription) {
|
|
16845
17282
|
this.incomingFundsSubscription();
|
|
16846
17283
|
this.incomingFundsSubscription = void 0;
|
|
@@ -16863,8 +17300,8 @@ var WalletMessageHandler = class {
|
|
|
16863
17300
|
this.indexerProvider = void 0;
|
|
16864
17301
|
}
|
|
16865
17302
|
async tick(_now) {
|
|
16866
|
-
const
|
|
16867
|
-
|
|
17303
|
+
const pending = this.onNextTick.splice(0);
|
|
17304
|
+
const results = await Promise.allSettled(pending.map((fn) => fn()));
|
|
16868
17305
|
return results.map((result) => {
|
|
16869
17306
|
if (result.status === "fulfilled") {
|
|
16870
17307
|
return result.value;
|
|
@@ -16874,8 +17311,46 @@ var WalletMessageHandler = class {
|
|
|
16874
17311
|
}
|
|
16875
17312
|
}).filter((response) => response !== null);
|
|
16876
17313
|
}
|
|
16877
|
-
|
|
16878
|
-
|
|
17314
|
+
epochOf(scope) {
|
|
17315
|
+
switch (scope) {
|
|
17316
|
+
case "incomingFunds":
|
|
17317
|
+
return this.incomingFundsEpoch;
|
|
17318
|
+
case "contractEvents":
|
|
17319
|
+
return this.contractEventsEpoch;
|
|
17320
|
+
case "request":
|
|
17321
|
+
return this.requestEpoch;
|
|
17322
|
+
default: {
|
|
17323
|
+
const _exhaustive = scope;
|
|
17324
|
+
throw new Error(`Unknown scope: ${_exhaustive}`);
|
|
17325
|
+
}
|
|
17326
|
+
}
|
|
17327
|
+
}
|
|
17328
|
+
newGeneration(scope) {
|
|
17329
|
+
const born = this.epochOf(scope);
|
|
17330
|
+
const stale = () => born !== this.epochOf(scope);
|
|
17331
|
+
return {
|
|
17332
|
+
get stale() {
|
|
17333
|
+
return stale();
|
|
17334
|
+
}
|
|
17335
|
+
};
|
|
17336
|
+
}
|
|
17337
|
+
/**
|
|
17338
|
+
* A handle rather than a `broadcast(response, epoch)` call, because it can
|
|
17339
|
+
* only be written where the closure it guards is created — an epoch passed
|
|
17340
|
+
* at the call site reads identically whether it guards anything or not.
|
|
17341
|
+
*/
|
|
17342
|
+
newEmitter(scope) {
|
|
17343
|
+
const gen = this.newGeneration(scope);
|
|
17344
|
+
return {
|
|
17345
|
+
get stale() {
|
|
17346
|
+
return gen.stale;
|
|
17347
|
+
},
|
|
17348
|
+
emit: (response) => {
|
|
17349
|
+
if (!response || gen.stale) return;
|
|
17350
|
+
if (this.channel) return this.channel.broadcast(response);
|
|
17351
|
+
this.onNextTick.push(() => response);
|
|
17352
|
+
}
|
|
17353
|
+
};
|
|
16879
17354
|
}
|
|
16880
17355
|
requireWallet() {
|
|
16881
17356
|
if (!this.wallet) {
|
|
@@ -16904,6 +17379,7 @@ var WalletMessageHandler = class {
|
|
|
16904
17379
|
}
|
|
16905
17380
|
async handleMessage(message) {
|
|
16906
17381
|
const id = message.id;
|
|
17382
|
+
const emitter = this.newEmitter("request");
|
|
16907
17383
|
if (message.type === "INIT_WALLET") {
|
|
16908
17384
|
await this.handleInitWallet(message);
|
|
16909
17385
|
return this.tagged({
|
|
@@ -17172,6 +17648,18 @@ var WalletMessageHandler = class {
|
|
|
17172
17648
|
payload: { descriptor }
|
|
17173
17649
|
});
|
|
17174
17650
|
}
|
|
17651
|
+
case "GET_NEW_ADDRESSES": {
|
|
17652
|
+
const wallet = this.wallet;
|
|
17653
|
+
if (!wallet) throw new WalletNotInitializedError();
|
|
17654
|
+
const addresses = await wallet.getNewAddresses(
|
|
17655
|
+
message.payload
|
|
17656
|
+
);
|
|
17657
|
+
return this.tagged({
|
|
17658
|
+
id,
|
|
17659
|
+
type: "NEW_ADDRESSES",
|
|
17660
|
+
payload: { addresses }
|
|
17661
|
+
});
|
|
17662
|
+
}
|
|
17175
17663
|
case "GET_USED_SIGNING_DESCRIPTORS": {
|
|
17176
17664
|
const wallet = this.wallet;
|
|
17177
17665
|
const descriptors = isHDWalletCapable(wallet) ? await wallet.getUsedSigningDescriptors(
|
|
@@ -17262,8 +17750,8 @@ var WalletMessageHandler = class {
|
|
|
17262
17750
|
const wallet = this.requireWallet();
|
|
17263
17751
|
const vtxoManager = await wallet.getVtxoManager();
|
|
17264
17752
|
const txid = await vtxoManager.recoverVtxos((e) => {
|
|
17265
|
-
|
|
17266
|
-
|
|
17753
|
+
emitter.emit(
|
|
17754
|
+
this.tagged({
|
|
17267
17755
|
id,
|
|
17268
17756
|
type: "RECOVER_VTXOS_EVENT",
|
|
17269
17757
|
payload: e
|
|
@@ -17307,8 +17795,8 @@ var WalletMessageHandler = class {
|
|
|
17307
17795
|
const wallet = this.requireWallet();
|
|
17308
17796
|
const vtxoManager = await wallet.getVtxoManager();
|
|
17309
17797
|
const txid = await vtxoManager.renewVtxos((e) => {
|
|
17310
|
-
|
|
17311
|
-
|
|
17798
|
+
emitter.emit(
|
|
17799
|
+
this.tagged({
|
|
17312
17800
|
id,
|
|
17313
17801
|
type: "RENEW_VTXOS_EVENT",
|
|
17314
17802
|
payload: e
|
|
@@ -17346,8 +17834,8 @@ var WalletMessageHandler = class {
|
|
|
17346
17834
|
const vtxoManager = await wallet.getVtxoManager();
|
|
17347
17835
|
const report = await vtxoManager.migrateDeprecatedSignerVtxos({
|
|
17348
17836
|
eventCallback: (e) => {
|
|
17349
|
-
|
|
17350
|
-
|
|
17837
|
+
emitter.emit(
|
|
17838
|
+
this.tagged({
|
|
17351
17839
|
id,
|
|
17352
17840
|
type: "MIGRATE_DEPRECATED_SIGNER_VTXOS_EVENT",
|
|
17353
17841
|
payload: e
|
|
@@ -17450,6 +17938,7 @@ var WalletMessageHandler = class {
|
|
|
17450
17938
|
intentLocked: offchain.intentLocked,
|
|
17451
17939
|
recoverable: offchain.recoverable,
|
|
17452
17940
|
pendingRecovery: offchain.pendingRecovery,
|
|
17941
|
+
unrolled: offchain.unrolled,
|
|
17453
17942
|
total: totalBoarding + offchain.total,
|
|
17454
17943
|
assets: offchain.assets,
|
|
17455
17944
|
availableAssets: offchain.availableAssets
|
|
@@ -17460,11 +17949,15 @@ var WalletMessageHandler = class {
|
|
|
17460
17949
|
return this.readonlyWallet.getBoardingUtxos();
|
|
17461
17950
|
}
|
|
17462
17951
|
async onWalletInitialized() {
|
|
17463
|
-
|
|
17952
|
+
const wallet = this.readonlyWallet;
|
|
17953
|
+
if (!wallet || !this.arkProvider || !this.indexerProvider || !this.walletRepository) {
|
|
17464
17954
|
return;
|
|
17465
17955
|
}
|
|
17956
|
+
const init = this.newGeneration("incomingFunds");
|
|
17466
17957
|
await this.ensureContractEventBroadcasting();
|
|
17958
|
+
if (init.stale) return;
|
|
17467
17959
|
await this.refreshCachedData();
|
|
17960
|
+
if (init.stale) return;
|
|
17468
17961
|
if (this.wallet) {
|
|
17469
17962
|
try {
|
|
17470
17963
|
const vtxos = await this.getVtxosFromRepo();
|
|
@@ -17477,72 +17970,81 @@ var WalletMessageHandler = class {
|
|
|
17477
17970
|
} catch (error) {
|
|
17478
17971
|
console.error("Error recovering pending transactions:", error);
|
|
17479
17972
|
}
|
|
17973
|
+
if (init.stale) return;
|
|
17480
17974
|
}
|
|
17481
17975
|
if (this.incomingFundsSubscription) this.incomingFundsSubscription();
|
|
17482
|
-
|
|
17483
|
-
this.
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
const arr = byScript.get(v.script) ?? [];
|
|
17497
|
-
arr.push(v);
|
|
17498
|
-
byScript.set(v.script, arr);
|
|
17499
|
-
}
|
|
17500
|
-
let walletScript;
|
|
17501
|
-
try {
|
|
17502
|
-
walletScript = scriptFromArkAddress(address);
|
|
17503
|
-
} catch {
|
|
17504
|
-
walletScript = void 0;
|
|
17505
|
-
}
|
|
17506
|
-
const cm = await this.readonlyWallet.getContractManager();
|
|
17507
|
-
const contracts = await cm.getContracts();
|
|
17508
|
-
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
17509
|
-
for (const [script, vtxos] of byScript) {
|
|
17510
|
-
const filtered = warnAndFilterVtxosForScript(
|
|
17511
|
-
vtxos,
|
|
17512
|
-
script,
|
|
17513
|
-
"WalletMessageHandler.notifyIncomingFunds"
|
|
17976
|
+
this.incomingFundsSubscription = void 0;
|
|
17977
|
+
this.incomingFundsEpoch++;
|
|
17978
|
+
const emitter = this.newEmitter("incomingFunds");
|
|
17979
|
+
const address = await wallet.getAddress();
|
|
17980
|
+
const unsubscribe = await wallet.notifyIncomingFunds(async (funds) => {
|
|
17981
|
+
if (emitter.stale) return;
|
|
17982
|
+
if (funds.type === "vtxo") {
|
|
17983
|
+
const { newVtxos, spentVtxos } = funds;
|
|
17984
|
+
if (newVtxos.length + spentVtxos.length === 0) return;
|
|
17985
|
+
const byScript = /* @__PURE__ */ new Map();
|
|
17986
|
+
for (const v of [...newVtxos, ...spentVtxos]) {
|
|
17987
|
+
if (!v.script) {
|
|
17988
|
+
console.warn(
|
|
17989
|
+
`WalletMessageHandler.notifyIncomingFunds: dropping VTXO without script ${v.txid}:${v.vout}`
|
|
17514
17990
|
);
|
|
17515
|
-
|
|
17516
|
-
const targetAddress = script === walletScript ? address : addrByScript.get(script);
|
|
17517
|
-
if (!targetAddress) continue;
|
|
17518
|
-
if (this.walletRepository) {
|
|
17519
|
-
await saveVtxosForContract(
|
|
17520
|
-
this.walletRepository,
|
|
17521
|
-
{ script, address: targetAddress },
|
|
17522
|
-
filtered
|
|
17523
|
-
);
|
|
17524
|
-
}
|
|
17991
|
+
continue;
|
|
17525
17992
|
}
|
|
17526
|
-
|
|
17527
|
-
|
|
17528
|
-
|
|
17529
|
-
broadcast: true,
|
|
17530
|
-
payload: { newVtxos, spentVtxos }
|
|
17531
|
-
})
|
|
17532
|
-
);
|
|
17993
|
+
const arr = byScript.get(v.script) ?? [];
|
|
17994
|
+
arr.push(v);
|
|
17995
|
+
byScript.set(v.script, arr);
|
|
17533
17996
|
}
|
|
17534
|
-
|
|
17535
|
-
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
|
|
17539
|
-
|
|
17540
|
-
|
|
17541
|
-
|
|
17997
|
+
let walletScript;
|
|
17998
|
+
try {
|
|
17999
|
+
walletScript = scriptFromArkAddress(address);
|
|
18000
|
+
} catch {
|
|
18001
|
+
walletScript = void 0;
|
|
18002
|
+
}
|
|
18003
|
+
const cm = await wallet.getContractManager();
|
|
18004
|
+
if (emitter.stale) return;
|
|
18005
|
+
const contracts = await cm.getContracts();
|
|
18006
|
+
if (emitter.stale) return;
|
|
18007
|
+
const addrByScript = new Map(contracts.map((c) => [c.script, c.address]));
|
|
18008
|
+
for (const [script, vtxos] of byScript) {
|
|
18009
|
+
const filtered = warnAndFilterVtxosForScript(
|
|
18010
|
+
vtxos,
|
|
18011
|
+
script,
|
|
18012
|
+
"WalletMessageHandler.notifyIncomingFunds"
|
|
17542
18013
|
);
|
|
18014
|
+
if (filtered.length === 0) continue;
|
|
18015
|
+
const targetAddress = script === walletScript ? address : addrByScript.get(script);
|
|
18016
|
+
if (!targetAddress) continue;
|
|
18017
|
+
if (this.walletRepository) {
|
|
18018
|
+
if (emitter.stale) return;
|
|
18019
|
+
await saveVtxosForContract(
|
|
18020
|
+
this.walletRepository,
|
|
18021
|
+
{ script, address: targetAddress },
|
|
18022
|
+
filtered
|
|
18023
|
+
);
|
|
18024
|
+
}
|
|
17543
18025
|
}
|
|
18026
|
+
emitter.emit(
|
|
18027
|
+
this.tagged({
|
|
18028
|
+
type: "VTXO_UPDATE",
|
|
18029
|
+
broadcast: true,
|
|
18030
|
+
payload: { newVtxos, spentVtxos }
|
|
18031
|
+
})
|
|
18032
|
+
);
|
|
17544
18033
|
}
|
|
17545
|
-
|
|
18034
|
+
if (funds.type === "utxo") {
|
|
18035
|
+
if (emitter.stale) return;
|
|
18036
|
+
const utxos = await wallet.getBoardingUtxos();
|
|
18037
|
+
emitter.emit(
|
|
18038
|
+
this.tagged({
|
|
18039
|
+
type: "UTXO_UPDATE",
|
|
18040
|
+
broadcast: true,
|
|
18041
|
+
payload: { coins: utxos }
|
|
18042
|
+
})
|
|
18043
|
+
);
|
|
18044
|
+
}
|
|
18045
|
+
});
|
|
18046
|
+
if (emitter.stale) return unsubscribe();
|
|
18047
|
+
this.incomingFundsSubscription = unsubscribe;
|
|
17546
18048
|
if (this.wallet) {
|
|
17547
18049
|
try {
|
|
17548
18050
|
await this.wallet.getVtxoManager();
|
|
@@ -17587,10 +18089,11 @@ var WalletMessageHandler = class {
|
|
|
17587
18089
|
await this.refreshCachedData();
|
|
17588
18090
|
}
|
|
17589
18091
|
async handleSettle(message) {
|
|
18092
|
+
const emitter = this.newEmitter("request");
|
|
17590
18093
|
const wallet = this.requireWallet();
|
|
17591
18094
|
const txid = await wallet.settle(message.payload.params, (e) => {
|
|
17592
|
-
|
|
17593
|
-
|
|
18095
|
+
emitter.emit(
|
|
18096
|
+
this.tagged({
|
|
17594
18097
|
id: message.id,
|
|
17595
18098
|
type: "SETTLE_EVENT",
|
|
17596
18099
|
payload: e
|
|
@@ -17667,10 +18170,19 @@ var WalletMessageHandler = class {
|
|
|
17667
18170
|
throw new WalletNotInitializedError();
|
|
17668
18171
|
}
|
|
17669
18172
|
const allVtxos = await this.getVtxosFromRepo();
|
|
17670
|
-
const vtxos = allVtxos.filter((v) => !hasTerminalSpend(v));
|
|
17671
18173
|
const dustAmount = this.readonlyWallet.dustAmount;
|
|
18174
|
+
const withUnrolled = message.payload.filter?.withUnrolled ?? false;
|
|
17672
18175
|
const includeRecoverable = message.payload.filter?.withRecoverable ?? false;
|
|
17673
|
-
|
|
18176
|
+
return allVtxos.filter((v) => {
|
|
18177
|
+
if (v.isUnrolled) {
|
|
18178
|
+
return withUnrolled;
|
|
18179
|
+
}
|
|
18180
|
+
if (hasTerminalSpend(v)) {
|
|
18181
|
+
return false;
|
|
18182
|
+
}
|
|
18183
|
+
if (includeRecoverable) {
|
|
18184
|
+
return true;
|
|
18185
|
+
}
|
|
17674
18186
|
if (dustAmount != null && isSubdust(v, dustAmount)) {
|
|
17675
18187
|
return false;
|
|
17676
18188
|
}
|
|
@@ -17682,15 +18194,15 @@ var WalletMessageHandler = class {
|
|
|
17682
18194
|
}
|
|
17683
18195
|
return true;
|
|
17684
18196
|
});
|
|
17685
|
-
if (!message.payload.filter?.withUnrolled) {
|
|
17686
|
-
return filteredVtxos;
|
|
17687
|
-
}
|
|
17688
|
-
return filteredVtxos.concat(allVtxos.filter((v) => hasTerminalSpend(v) && v.isUnrolled));
|
|
17689
18197
|
}
|
|
17690
18198
|
/** Tear down handler subscriptions, then delegate the full wipe to the wallet. */
|
|
17691
18199
|
async clear() {
|
|
17692
18200
|
const wallet = this.wallet ?? this.readonlyWallet;
|
|
17693
18201
|
if (!wallet) return;
|
|
18202
|
+
this.requestEpoch++;
|
|
18203
|
+
this.incomingFundsEpoch++;
|
|
18204
|
+
this.contractEventsEpoch++;
|
|
18205
|
+
this.onNextTick = [];
|
|
17694
18206
|
if (this.incomingFundsSubscription) {
|
|
17695
18207
|
this.incomingFundsSubscription();
|
|
17696
18208
|
this.incomingFundsSubscription = void 0;
|
|
@@ -17779,13 +18291,17 @@ var WalletMessageHandler = class {
|
|
|
17779
18291
|
return buildTransactionHistory(vtxos, boardingTxs, commitmentsToIgnore, resolveTxCreatedAt);
|
|
17780
18292
|
}
|
|
17781
18293
|
async ensureContractEventBroadcasting() {
|
|
17782
|
-
|
|
18294
|
+
const wallet = this.readonlyWallet;
|
|
18295
|
+
if (!wallet) return;
|
|
17783
18296
|
if (this.contractEventsSubscription) return;
|
|
18297
|
+
const gen = this.newGeneration("contractEvents");
|
|
17784
18298
|
try {
|
|
17785
|
-
const manager = await
|
|
18299
|
+
const manager = await wallet.getContractManager();
|
|
18300
|
+
if (gen.stale || this.contractEventsSubscription) return;
|
|
18301
|
+
const emitter = this.newEmitter("contractEvents");
|
|
17786
18302
|
this.contractEventsSubscription = manager.onContractEvent((event) => {
|
|
17787
|
-
|
|
17788
|
-
|
|
18303
|
+
emitter.emit(
|
|
18304
|
+
this.tagged({
|
|
17789
18305
|
type: "CONTRACT_EVENT",
|
|
17790
18306
|
broadcast: true,
|
|
17791
18307
|
payload: { event }
|
|
@@ -17816,6 +18332,9 @@ var DEFAULT_MESSAGE_TIMEOUTS = {
|
|
|
17816
18332
|
// Allocation is a local repository write plus a fire-and-forget band
|
|
17817
18333
|
// slide — no indexer round trip on the request path.
|
|
17818
18334
|
GET_NEXT_SIGNING_DESCRIPTOR: 1e4,
|
|
18335
|
+
// Same shape as the bare allocation above, plus one contract write per
|
|
18336
|
+
// requested type — still local, still no indexer round trip.
|
|
18337
|
+
GET_NEW_ADDRESSES: 1e4,
|
|
17819
18338
|
GET_USED_SIGNING_DESCRIPTORS: 2e4,
|
|
17820
18339
|
ADVANCE_SIGNING_DESCRIPTOR_WATERMARK: 1e4,
|
|
17821
18340
|
// Medium reads — may involve indexer queries
|
|
@@ -18961,6 +19480,35 @@ var ServiceWorkerWallet = class _ServiceWorkerWallet extends ServiceWorkerReadon
|
|
|
18961
19480
|
throw new Error(`Failed to allocate next signing descriptor: ${error}`);
|
|
18962
19481
|
}
|
|
18963
19482
|
}
|
|
19483
|
+
/**
|
|
19484
|
+
* @see Wallet.getNewAddresses
|
|
19485
|
+
*
|
|
19486
|
+
* Proxied as a single request rather than rebuilt page-side out of
|
|
19487
|
+
* {@link getNextSigningDescriptor}: the worker owns the HD watermark *and*
|
|
19488
|
+
* the contract repository, so splitting the operation would let a page burn
|
|
19489
|
+
* an index and then fail to register the script, leaving an address nothing
|
|
19490
|
+
* watches.
|
|
19491
|
+
*
|
|
19492
|
+
* Note the error shape — `WalletCannotAllocateAddressError` is thrown
|
|
19493
|
+
* worker-side and cannot cross the message boundary as a class, so a
|
|
19494
|
+
* `forceNew` refusal surfaces here as a plain `Error` carrying its message
|
|
19495
|
+
* (and the worker-side error as `cause`). Callers that must branch on the
|
|
19496
|
+
* refusal match the message rather than `instanceof`.
|
|
19497
|
+
*/
|
|
19498
|
+
async getNewAddresses(opts) {
|
|
19499
|
+
const message = {
|
|
19500
|
+
id: getRandomId(),
|
|
19501
|
+
tag: this.messageTag,
|
|
19502
|
+
type: "GET_NEW_ADDRESSES",
|
|
19503
|
+
...opts ? { payload: opts } : {}
|
|
19504
|
+
};
|
|
19505
|
+
try {
|
|
19506
|
+
const response = await this.sendMessage(message);
|
|
19507
|
+
return response.payload.addresses;
|
|
19508
|
+
} catch (error) {
|
|
19509
|
+
throw new Error(`Failed to allocate new addresses: ${error}`, { cause: error });
|
|
19510
|
+
}
|
|
19511
|
+
}
|
|
18964
19512
|
/** @see HDWalletCapable.getUsedSigningDescriptors */
|
|
18965
19513
|
async getUsedSigningDescriptors(opts) {
|
|
18966
19514
|
const message = {
|
|
@@ -19927,6 +20475,10 @@ var ElectrumOnchainProvider = class {
|
|
|
19927
20475
|
}
|
|
19928
20476
|
throw new Error("Only 1 or 1P1C package can be broadcast");
|
|
19929
20477
|
}
|
|
20478
|
+
async getRawTransaction(txid) {
|
|
20479
|
+
const [tx] = await this.chain.fetchTransactions([txid]);
|
|
20480
|
+
return hex.decode(tx.hex);
|
|
20481
|
+
}
|
|
19930
20482
|
async getTxOutspends(txid) {
|
|
19931
20483
|
const [txResult] = await this.chain.fetchTransactions([txid]);
|
|
19932
20484
|
const tx = Transaction$2.fromRaw(hex.decode(txResult.hex), {
|
|
@@ -20232,6 +20784,183 @@ function childTxidFromHex(txHex) {
|
|
|
20232
20784
|
});
|
|
20233
20785
|
return tx.id;
|
|
20234
20786
|
}
|
|
20787
|
+
|
|
20788
|
+
// src/providers/cachingArk.ts
|
|
20789
|
+
var DEFAULT_TTL_MS = 6e4;
|
|
20790
|
+
var CachingArkProvider = class {
|
|
20791
|
+
constructor(inner, ttlMs = DEFAULT_TTL_MS) {
|
|
20792
|
+
this.inner = inner;
|
|
20793
|
+
this.ttlMs = ttlMs;
|
|
20794
|
+
this.unsubscribe = inner.onServerInfoChanged?.((info) => {
|
|
20795
|
+
this.cached = info;
|
|
20796
|
+
this.expiresAt = Date.now() + this.ttlMs;
|
|
20797
|
+
this.generation++;
|
|
20798
|
+
}) ?? (() => {
|
|
20799
|
+
});
|
|
20800
|
+
}
|
|
20801
|
+
inner;
|
|
20802
|
+
ttlMs;
|
|
20803
|
+
cached;
|
|
20804
|
+
expiresAt = 0;
|
|
20805
|
+
inflight;
|
|
20806
|
+
/** Bumped on every event-driven cache write; see {@link getInfo}. */
|
|
20807
|
+
generation = 0;
|
|
20808
|
+
unsubscribe;
|
|
20809
|
+
/** Releases the inner provider's server-info subscription. */
|
|
20810
|
+
dispose() {
|
|
20811
|
+
this.unsubscribe();
|
|
20812
|
+
}
|
|
20813
|
+
get serverUrl() {
|
|
20814
|
+
const url = this.inner.serverUrl;
|
|
20815
|
+
return typeof url === "string" ? url : void 0;
|
|
20816
|
+
}
|
|
20817
|
+
async getInfo() {
|
|
20818
|
+
if (this.cached && Date.now() < this.expiresAt) return this.cached;
|
|
20819
|
+
if (!this.inflight) {
|
|
20820
|
+
const generation = this.generation;
|
|
20821
|
+
this.inflight = this.inner.getInfo().then((info) => {
|
|
20822
|
+
if (this.generation !== generation) return this.cached ?? info;
|
|
20823
|
+
this.cached = info;
|
|
20824
|
+
this.expiresAt = Date.now() + this.ttlMs;
|
|
20825
|
+
return info;
|
|
20826
|
+
}).finally(() => {
|
|
20827
|
+
this.inflight = void 0;
|
|
20828
|
+
});
|
|
20829
|
+
}
|
|
20830
|
+
return this.inflight;
|
|
20831
|
+
}
|
|
20832
|
+
submitTx(signedArkTx, checkpointTxs) {
|
|
20833
|
+
return this.inner.submitTx(signedArkTx, checkpointTxs);
|
|
20834
|
+
}
|
|
20835
|
+
finalizeTx(arkTxid, finalCheckpointTxs) {
|
|
20836
|
+
return this.inner.finalizeTx(arkTxid, finalCheckpointTxs);
|
|
20837
|
+
}
|
|
20838
|
+
registerIntent(intent) {
|
|
20839
|
+
return this.inner.registerIntent(intent);
|
|
20840
|
+
}
|
|
20841
|
+
deleteIntent(intent) {
|
|
20842
|
+
return this.inner.deleteIntent(intent);
|
|
20843
|
+
}
|
|
20844
|
+
confirmRegistration(intentId) {
|
|
20845
|
+
return this.inner.confirmRegistration(intentId);
|
|
20846
|
+
}
|
|
20847
|
+
submitTreeNonces(batchId, pubkey, nonces) {
|
|
20848
|
+
return this.inner.submitTreeNonces(batchId, pubkey, nonces);
|
|
20849
|
+
}
|
|
20850
|
+
submitTreeSignatures(batchId, pubkey, signatures) {
|
|
20851
|
+
return this.inner.submitTreeSignatures(batchId, pubkey, signatures);
|
|
20852
|
+
}
|
|
20853
|
+
submitSignedForfeitTxs(signedForfeitTxs, signedCommitmentTx) {
|
|
20854
|
+
return this.inner.submitSignedForfeitTxs(signedForfeitTxs, signedCommitmentTx);
|
|
20855
|
+
}
|
|
20856
|
+
getEventStream(signal, topics) {
|
|
20857
|
+
return this.inner.getEventStream(signal, topics);
|
|
20858
|
+
}
|
|
20859
|
+
getTransactionsStream(signal) {
|
|
20860
|
+
return this.inner.getTransactionsStream(signal);
|
|
20861
|
+
}
|
|
20862
|
+
getPendingTxs(intent) {
|
|
20863
|
+
return this.inner.getPendingTxs(intent);
|
|
20864
|
+
}
|
|
20865
|
+
onServerInfoChanged(listener) {
|
|
20866
|
+
return this.inner.onServerInfoChanged?.(listener) ?? (() => {
|
|
20867
|
+
});
|
|
20868
|
+
}
|
|
20869
|
+
};
|
|
20870
|
+
var PrevTxUnavailableError = class extends Error {
|
|
20871
|
+
constructor(message, txids = []) {
|
|
20872
|
+
super(message);
|
|
20873
|
+
this.txids = txids;
|
|
20874
|
+
this.name = "PrevTxUnavailableError";
|
|
20875
|
+
}
|
|
20876
|
+
txids;
|
|
20877
|
+
};
|
|
20878
|
+
async function resolvePrevTxs(txids, source, onchain) {
|
|
20879
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
20880
|
+
const wanted = [...new Set(txids)];
|
|
20881
|
+
if (wanted.length === 0) return resolved;
|
|
20882
|
+
try {
|
|
20883
|
+
const { txs } = await source.getVirtualTxs(wanted);
|
|
20884
|
+
for (const psbt of txs) {
|
|
20885
|
+
const tx = Transaction.fromPSBT(base64.decode(psbt));
|
|
20886
|
+
resolved.set(tx.id, tx.toBytes());
|
|
20887
|
+
}
|
|
20888
|
+
} catch (err) {
|
|
20889
|
+
if (!onchain) throw err;
|
|
20890
|
+
}
|
|
20891
|
+
let missing = wanted.filter((txid) => !resolved.has(txid));
|
|
20892
|
+
if (missing.length > 0 && onchain) {
|
|
20893
|
+
const raw = await Promise.all(
|
|
20894
|
+
missing.map(async (txid) => {
|
|
20895
|
+
try {
|
|
20896
|
+
return await onchain.getRawTransaction(txid);
|
|
20897
|
+
} catch {
|
|
20898
|
+
return void 0;
|
|
20899
|
+
}
|
|
20900
|
+
})
|
|
20901
|
+
);
|
|
20902
|
+
for (const [i, bytes] of raw.entries()) {
|
|
20903
|
+
if (bytes) resolved.set(missing[i], bytes);
|
|
20904
|
+
}
|
|
20905
|
+
missing = missing.filter((txid) => !resolved.has(txid));
|
|
20906
|
+
}
|
|
20907
|
+
if (missing.length > 0) {
|
|
20908
|
+
throw new PrevTxUnavailableError(
|
|
20909
|
+
`cannot resolve the previous transaction of ${missing.join(", ")}` + (onchain ? "" : " (no onchain source configured)"),
|
|
20910
|
+
missing
|
|
20911
|
+
);
|
|
20912
|
+
}
|
|
20913
|
+
return resolved;
|
|
20914
|
+
}
|
|
20915
|
+
async function attachPrevArkTxs(tx, txids, source, onchain) {
|
|
20916
|
+
const pending = [];
|
|
20917
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
20918
|
+
if (getArkPsbtFields(tx, i, PrevArkTxField).length === 0) pending.push(i);
|
|
20919
|
+
}
|
|
20920
|
+
if (pending.length === 0) return;
|
|
20921
|
+
const resolved = await resolvePrevTxs(
|
|
20922
|
+
pending.map((i) => txidAt(txids, i)),
|
|
20923
|
+
source,
|
|
20924
|
+
onchain
|
|
20925
|
+
);
|
|
20926
|
+
for (const i of pending) {
|
|
20927
|
+
setArkPsbtField(tx, i, PrevArkTxField, resolved.get(txidAt(txids, i)));
|
|
20928
|
+
}
|
|
20929
|
+
}
|
|
20930
|
+
async function attachPrevoutTxs(tx, source) {
|
|
20931
|
+
const pending = [];
|
|
20932
|
+
for (let i = 0; i < tx.inputsLength; i++) {
|
|
20933
|
+
if (getArkPsbtFields(tx, i, PrevoutTxField).length > 0) continue;
|
|
20934
|
+
const input = tx.getInput(i);
|
|
20935
|
+
if (!input?.txid) throw new PrevTxUnavailableError(`input ${i} has no outpoint`);
|
|
20936
|
+
pending.push({ index: i, txid: hex.encode(input.txid) });
|
|
20937
|
+
}
|
|
20938
|
+
if (pending.length === 0) return;
|
|
20939
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
20940
|
+
for (const txid of new Set(pending.map((p) => p.txid))) {
|
|
20941
|
+
resolved.set(txid, await source.getRawTransaction(txid));
|
|
20942
|
+
}
|
|
20943
|
+
for (const { index, txid } of pending) {
|
|
20944
|
+
setArkPsbtField(tx, index, PrevoutTxField, resolved.get(txid));
|
|
20945
|
+
}
|
|
20946
|
+
}
|
|
20947
|
+
async function withPrevTxs(coins, source, onchain) {
|
|
20948
|
+
const pending = coins.filter((c) => !c.prevTx);
|
|
20949
|
+
if (pending.length === 0) return coins;
|
|
20950
|
+
const resolved = await resolvePrevTxs(
|
|
20951
|
+
pending.map((c) => c.txid),
|
|
20952
|
+
source,
|
|
20953
|
+
onchain
|
|
20954
|
+
);
|
|
20955
|
+
return coins.map((c) => c.prevTx ? c : { ...c, prevTx: resolved.get(c.txid) });
|
|
20956
|
+
}
|
|
20957
|
+
function txidAt(txids, index) {
|
|
20958
|
+
const txid = txids[index];
|
|
20959
|
+
if (txid === void 0) {
|
|
20960
|
+
throw new PrevTxUnavailableError(`no source txid supplied for input ${index}`);
|
|
20961
|
+
}
|
|
20962
|
+
return txid;
|
|
20963
|
+
}
|
|
20235
20964
|
var TAG_BIP322 = "BIP0322-signed-message";
|
|
20236
20965
|
var BIP322;
|
|
20237
20966
|
((BIP3222) => {
|
|
@@ -20609,6 +21338,22 @@ async function resolveUnilateralPath(params) {
|
|
|
20609
21338
|
return { selection: best.selection, label: "default:exit" };
|
|
20610
21339
|
}
|
|
20611
21340
|
|
|
21341
|
+
// src/wallet/exitObserver.ts
|
|
21342
|
+
function exitObserverFor(manager) {
|
|
21343
|
+
return (outpoint) => manager.refreshOutpoints([outpoint]);
|
|
21344
|
+
}
|
|
21345
|
+
async function notifyExitObserved(onExitObserved, outpoint) {
|
|
21346
|
+
if (!onExitObserved) return;
|
|
21347
|
+
try {
|
|
21348
|
+
await onExitObserved(outpoint);
|
|
21349
|
+
} catch (e) {
|
|
21350
|
+
console.error(
|
|
21351
|
+
`exit observer failed for ${outpoint.txid}:${outpoint.vout} \u2014 the exit is unaffected`,
|
|
21352
|
+
e
|
|
21353
|
+
);
|
|
21354
|
+
}
|
|
21355
|
+
}
|
|
21356
|
+
|
|
20612
21357
|
// src/wallet/unroll.ts
|
|
20613
21358
|
function chainTxTypeToChainedExit2(t) {
|
|
20614
21359
|
switch (t) {
|
|
@@ -20633,27 +21378,30 @@ var Unroll;
|
|
|
20633
21378
|
})(Unroll2.StepType || (Unroll2.StepType = {}));
|
|
20634
21379
|
class Session2 {
|
|
20635
21380
|
/** Create an unroll session from a virtual output outpoint and its dependency chain. */
|
|
20636
|
-
constructor(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
|
|
21381
|
+
constructor(toUnroll, bumper, explorer, indexer, virtualTxRepository, onExitObserved) {
|
|
20637
21382
|
this.toUnroll = toUnroll;
|
|
20638
21383
|
this.bumper = bumper;
|
|
20639
21384
|
this.explorer = explorer;
|
|
20640
21385
|
this.indexer = indexer;
|
|
20641
21386
|
this.virtualTxRepository = virtualTxRepository;
|
|
21387
|
+
this.onExitObserved = onExitObserved;
|
|
20642
21388
|
}
|
|
20643
21389
|
toUnroll;
|
|
20644
21390
|
bumper;
|
|
20645
21391
|
explorer;
|
|
20646
21392
|
indexer;
|
|
20647
21393
|
virtualTxRepository;
|
|
21394
|
+
onExitObserved;
|
|
20648
21395
|
/** Create an unroll session by loading the virtual output chain from the indexer. */
|
|
20649
|
-
static async create(toUnroll, bumper, explorer, indexer, virtualTxRepository) {
|
|
21396
|
+
static async create(toUnroll, bumper, explorer, indexer, virtualTxRepository, onExitObserved) {
|
|
20650
21397
|
const { chain } = await indexer.getVtxoChain(toUnroll);
|
|
20651
21398
|
return new Session2(
|
|
20652
21399
|
{ ...toUnroll, chain },
|
|
20653
21400
|
bumper,
|
|
20654
21401
|
explorer,
|
|
20655
21402
|
indexer,
|
|
20656
|
-
virtualTxRepository
|
|
21403
|
+
virtualTxRepository,
|
|
21404
|
+
onExitObserved
|
|
20657
21405
|
);
|
|
20658
21406
|
}
|
|
20659
21407
|
/**
|
|
@@ -20689,6 +21437,14 @@ var Unroll;
|
|
|
20689
21437
|
}
|
|
20690
21438
|
/**
|
|
20691
21439
|
* Get the next step to be executed
|
|
21440
|
+
*
|
|
21441
|
+
* @remarks
|
|
21442
|
+
* `do()` is the step, not a formality: every effect lives there, and at
|
|
21443
|
+
* `StepType.DONE` that includes firing {@link onExitObserved}. A caller
|
|
21444
|
+
* driving `next()` by hand must always await it — the async iterator
|
|
21445
|
+
* below does. Reading `type` and skipping `do()` leaves the exit
|
|
21446
|
+
* unobserved, and on the other step kinds unbroadcast.
|
|
21447
|
+
*
|
|
20692
21448
|
* @returns The next step to be executed + the function to execute it
|
|
20693
21449
|
*/
|
|
20694
21450
|
async next() {
|
|
@@ -20717,7 +21473,12 @@ var Unroll;
|
|
|
20717
21473
|
return {
|
|
20718
21474
|
type: 2 /* DONE */,
|
|
20719
21475
|
vtxoTxid: this.toUnroll.txid,
|
|
20720
|
-
do
|
|
21476
|
+
// The observation rides on `do` rather than the iterator so
|
|
21477
|
+
// a caller driving `next()` by hand fires it too.
|
|
21478
|
+
do: () => notifyExitObserved(this.onExitObserved, {
|
|
21479
|
+
txid: this.toUnroll.txid,
|
|
21480
|
+
vout: this.toUnroll.vout
|
|
21481
|
+
})
|
|
20721
21482
|
};
|
|
20722
21483
|
}
|
|
20723
21484
|
const virtualTxBase64 = await this.resolveVirtualTxBase64(nextTxToBroadcast);
|
|
@@ -20751,9 +21512,22 @@ var Unroll;
|
|
|
20751
21512
|
}
|
|
20752
21513
|
}
|
|
20753
21514
|
Unroll2.Session = Session2;
|
|
21515
|
+
async function sessionFor(wallet, toUnroll, bumper) {
|
|
21516
|
+
const manager = await wallet.getContractManager();
|
|
21517
|
+
return Session2.create(
|
|
21518
|
+
toUnroll,
|
|
21519
|
+
bumper,
|
|
21520
|
+
wallet.onchainProvider,
|
|
21521
|
+
wallet.indexerProvider,
|
|
21522
|
+
wallet.virtualTxRepository,
|
|
21523
|
+
exitObserverFor(manager)
|
|
21524
|
+
);
|
|
21525
|
+
}
|
|
21526
|
+
Unroll2.sessionFor = sessionFor;
|
|
20754
21527
|
async function completeUnroll(wallet, vtxoTxids, outputAddress) {
|
|
20755
21528
|
const signedTx = await prepareUnrollTransaction(wallet, vtxoTxids, outputAddress);
|
|
20756
21529
|
await wallet.onchainProvider.broadcastTransaction(signedTx.hex);
|
|
21530
|
+
await refreshSpentExitOutputs(wallet, signedTx);
|
|
20757
21531
|
return signedTx.id;
|
|
20758
21532
|
}
|
|
20759
21533
|
Unroll2.completeUnroll = completeUnroll;
|
|
@@ -20769,9 +21543,9 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
|
|
|
20769
21543
|
let totalAmount = 0n;
|
|
20770
21544
|
const txWeightEstimator = TxWeightEstimator.create();
|
|
20771
21545
|
for (const vtxo of vtxos) {
|
|
20772
|
-
if (!vtxo
|
|
21546
|
+
if (!canSweepOnchain(vtxo)) {
|
|
20773
21547
|
throw new Error(
|
|
20774
|
-
`Vtxo ${vtxo.txid}:${vtxo.vout} is not fully unrolled
|
|
21548
|
+
`Vtxo ${vtxo.txid}:${vtxo.vout} cannot be swept onchain: it is not fully unrolled (use unroll first), or its exit output was already spent`
|
|
20775
21549
|
);
|
|
20776
21550
|
}
|
|
20777
21551
|
const txStatus = await wallet.onchainProvider.getTxStatus(vtxo.txid);
|
|
@@ -20832,6 +21606,22 @@ async function prepareUnrollTransaction(wallet, vtxoTxIds, outputAddress) {
|
|
|
20832
21606
|
signedTx.finalize();
|
|
20833
21607
|
return signedTx;
|
|
20834
21608
|
}
|
|
21609
|
+
async function refreshSpentExitOutputs(wallet, signedTx) {
|
|
21610
|
+
const outpoints = [];
|
|
21611
|
+
for (let i = 0; i < signedTx.inputsLength; i++) {
|
|
21612
|
+
const input = signedTx.getInput(i);
|
|
21613
|
+
if (input.txid && input.index !== void 0) {
|
|
21614
|
+
outpoints.push({ txid: hex.encode(input.txid), vout: input.index });
|
|
21615
|
+
}
|
|
21616
|
+
}
|
|
21617
|
+
if (outpoints.length === 0) return;
|
|
21618
|
+
try {
|
|
21619
|
+
const manager = await wallet.getContractManager();
|
|
21620
|
+
await manager.refreshOutpoints(outpoints);
|
|
21621
|
+
} catch (e) {
|
|
21622
|
+
console.error("completeUnroll: failed to refresh the spent exit outputs", e);
|
|
21623
|
+
}
|
|
21624
|
+
}
|
|
20835
21625
|
function sleep2(ms) {
|
|
20836
21626
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
20837
21627
|
}
|
|
@@ -20910,7 +21700,7 @@ async function buildSignedSweep(params) {
|
|
|
20910
21700
|
}
|
|
20911
21701
|
|
|
20912
21702
|
// src/wallet/exit/estimate.ts
|
|
20913
|
-
var CHILD_OUTPUT_DUST =
|
|
21703
|
+
var CHILD_OUTPUT_DUST = CHILD_DUST_AMOUNT;
|
|
20914
21704
|
function stepFundingAmount(stepFee) {
|
|
20915
21705
|
return stepFee + CHILD_OUTPUT_DUST;
|
|
20916
21706
|
}
|
|
@@ -21071,10 +21861,18 @@ async function computeExitLayout(opts, feeRate) {
|
|
|
21071
21861
|
// splitter. funded: + 1 splitter tx and pre-signed children.
|
|
21072
21862
|
txCount: (graph ? 0 : steps.length > 0 ? 1 : 0) + steps.length * 2 + sweeps.length,
|
|
21073
21863
|
totalFeeSats: splitterFee + stepFees + sweepFees,
|
|
21074
|
-
// graph funding is what the executor sends to its own fee address:
|
|
21075
|
-
//
|
|
21076
|
-
//
|
|
21077
|
-
|
|
21864
|
+
// graph funding is what the executor sends to its own fee address: the
|
|
21865
|
+
// CPFP fees, plus one dust floor. Change recycles through that single
|
|
21866
|
+
// address, so each bump hands its remainder to the next and only the
|
|
21867
|
+
// *final* change has to clear `CHILD_DUST_AMOUNT` — but it does have to,
|
|
21868
|
+
// and `buildAnchorChild` throws rather than absorbing a short remainder
|
|
21869
|
+
// into the fee. Quoting bare fees quotes an amount that strands the last
|
|
21870
|
+
// bump and every sweep behind it.
|
|
21871
|
+
//
|
|
21872
|
+
// funded reserves the same floor per step instead of once overall (see
|
|
21873
|
+
// `stepFundingAmount`), because it pre-funds each child separately
|
|
21874
|
+
// through the splitter rather than recycling one output.
|
|
21875
|
+
fundingRequiredSats: graph ? steps.length > 0 ? stepFees + CHILD_OUTPUT_DUST : 0 : splitterFee + fundingTotal,
|
|
21078
21876
|
recoveredSats: sweeps.reduce((s, x) => s + (x.vtxo.value - x.sweepFee), 0)
|
|
21079
21877
|
};
|
|
21080
21878
|
const expiries = pendingNodes.map((n) => n.expiresAt).filter((x) => x !== void 0);
|
|
@@ -21134,12 +21932,32 @@ var Executor = class {
|
|
|
21134
21932
|
this.pollIntervalMs = opts?.pollIntervalMs ?? 5e3;
|
|
21135
21933
|
this.feeWallet = opts?.feeWallet;
|
|
21136
21934
|
this.signal = opts?.signal;
|
|
21935
|
+
this.onExitObserved = opts?.onExitObserved;
|
|
21137
21936
|
}
|
|
21138
21937
|
pkg;
|
|
21139
21938
|
provider;
|
|
21140
21939
|
pollIntervalMs;
|
|
21141
21940
|
feeWallet;
|
|
21142
21941
|
signal;
|
|
21942
|
+
onExitObserved;
|
|
21943
|
+
/**
|
|
21944
|
+
* Per-VTXO branch progress, which the executor otherwise does not track:
|
|
21945
|
+
* `dead` / `done` are per-step, and a VTXO is only onchain once EVERY
|
|
21946
|
+
* `package`/`bump` step naming it has confirmed. (`broadcast` — the funding
|
|
21947
|
+
* splitter — carries no `forVtxos` and serves no single VTXO.)
|
|
21948
|
+
*/
|
|
21949
|
+
branchSteps() {
|
|
21950
|
+
const byVtxo = /* @__PURE__ */ new Map();
|
|
21951
|
+
this.pkg.steps.forEach((step, i) => {
|
|
21952
|
+
if (step.kind !== "package" && step.kind !== "bump") return;
|
|
21953
|
+
for (const vtxo of step.forVtxos) {
|
|
21954
|
+
let steps = byVtxo.get(vtxo);
|
|
21955
|
+
if (!steps) byVtxo.set(vtxo, steps = /* @__PURE__ */ new Set());
|
|
21956
|
+
steps.add(i);
|
|
21957
|
+
}
|
|
21958
|
+
});
|
|
21959
|
+
return byVtxo;
|
|
21960
|
+
}
|
|
21143
21961
|
/**
|
|
21144
21962
|
* Poll delay. Nearly all wall-clock time is spent here, so it must be the
|
|
21145
21963
|
* thing that reacts to abort — checking only between polls would leave
|
|
@@ -21179,6 +21997,19 @@ var Executor = class {
|
|
|
21179
21997
|
async *[Symbol.asyncIterator]() {
|
|
21180
21998
|
throwIfAborted(this.signal);
|
|
21181
21999
|
const dead = /* @__PURE__ */ new Set();
|
|
22000
|
+
const branchSteps = this.branchSteps();
|
|
22001
|
+
const onchainSteps = /* @__PURE__ */ new Set();
|
|
22002
|
+
const branchObserved = /* @__PURE__ */ new Set();
|
|
22003
|
+
const observeBranch = async (stepIndex, forVtxos) => {
|
|
22004
|
+
onchainSteps.add(stepIndex);
|
|
22005
|
+
for (const vtxo of forVtxos ?? []) {
|
|
22006
|
+
if (branchObserved.has(vtxo)) continue;
|
|
22007
|
+
const steps = branchSteps.get(vtxo);
|
|
22008
|
+
if (!steps || ![...steps].every((i) => onchainSteps.has(i))) continue;
|
|
22009
|
+
branchObserved.add(vtxo);
|
|
22010
|
+
await this.observe(vtxo);
|
|
22011
|
+
}
|
|
22012
|
+
};
|
|
21182
22013
|
if (this.pkg.validUntil && Date.now() / 1e3 > this.pkg.validUntil) {
|
|
21183
22014
|
yield {
|
|
21184
22015
|
stepIndex: -1,
|
|
@@ -21208,6 +22039,7 @@ var Executor = class {
|
|
|
21208
22039
|
const anchorTxid = step.kind === "package" || step.kind === "bump" ? step.parentTxid : step.txid;
|
|
21209
22040
|
const existing = await this.status(anchorTxid);
|
|
21210
22041
|
if (existing?.confirmed) {
|
|
22042
|
+
await observeBranch(i, forVtxos);
|
|
21211
22043
|
yield {
|
|
21212
22044
|
stepIndex: i,
|
|
21213
22045
|
kind: step.kind,
|
|
@@ -21263,6 +22095,7 @@ var Executor = class {
|
|
|
21263
22095
|
}
|
|
21264
22096
|
}
|
|
21265
22097
|
await this.waitConfirmed(anchorTxid);
|
|
22098
|
+
await observeBranch(i, forVtxos);
|
|
21266
22099
|
yield {
|
|
21267
22100
|
stepIndex: i,
|
|
21268
22101
|
kind: step.kind,
|
|
@@ -21293,6 +22126,7 @@ var Executor = class {
|
|
|
21293
22126
|
const swept = await this.status(step.txid);
|
|
21294
22127
|
if (swept?.confirmed) {
|
|
21295
22128
|
done.add(index);
|
|
22129
|
+
await this.observe(step.vtxo);
|
|
21296
22130
|
yield {
|
|
21297
22131
|
stepIndex: index,
|
|
21298
22132
|
kind: "sweep",
|
|
@@ -21349,7 +22183,25 @@ var Executor = class {
|
|
|
21349
22183
|
if (done.size < pending.length) await this.sleep();
|
|
21350
22184
|
}
|
|
21351
22185
|
}
|
|
22186
|
+
/** `"txid:vout"` -> outpoint, then hand it to the hook. A malformed entry is skipped. */
|
|
22187
|
+
async observe(outpoint) {
|
|
22188
|
+
if (!this.onExitObserved) return;
|
|
22189
|
+
const sep = outpoint.lastIndexOf(":");
|
|
22190
|
+
const rest = outpoint.slice(sep + 1);
|
|
22191
|
+
const vout = Number(rest);
|
|
22192
|
+
if (sep <= 0 || rest === "" || !Number.isInteger(vout)) return;
|
|
22193
|
+
await notifyExitObserved(this.onExitObserved, { txid: outpoint.slice(0, sep), vout });
|
|
22194
|
+
}
|
|
21352
22195
|
};
|
|
22196
|
+
|
|
22197
|
+
// src/wallet/exit/execute.ts
|
|
22198
|
+
async function execute(wallet, pkg, opts) {
|
|
22199
|
+
const manager = await wallet.getContractManager();
|
|
22200
|
+
return new Executor(pkg, opts?.provider ?? wallet.onchainProvider, {
|
|
22201
|
+
...opts,
|
|
22202
|
+
onExitObserved: opts?.onExitObserved ?? exitObserverFor(manager)
|
|
22203
|
+
});
|
|
22204
|
+
}
|
|
21353
22205
|
async function prepare(opts) {
|
|
21354
22206
|
const { wallet, onchainWallet } = opts;
|
|
21355
22207
|
const feeRate = await resolveFeeRate(opts);
|
|
@@ -21424,7 +22276,10 @@ async function prepare(opts) {
|
|
|
21424
22276
|
totals: {
|
|
21425
22277
|
txCount: bumpSteps.length * 2 + sweepSteps.length,
|
|
21426
22278
|
totalFeeSats: stepFees2 + sweepFees2,
|
|
21427
|
-
|
|
22279
|
+
// Fees plus the single dust floor the recycling change output
|
|
22280
|
+
// must keep clear; see the note in `estimate.ts`. Must match
|
|
22281
|
+
// the quote `estimate` gives for the same package.
|
|
22282
|
+
fundingRequiredSats: bumpSteps.length > 0 ? stepFees2 + CHILD_OUTPUT_DUST : 0,
|
|
21428
22283
|
recoveredSats: recovered2
|
|
21429
22284
|
},
|
|
21430
22285
|
vtxos: layout.infos,
|
|
@@ -21593,6 +22448,7 @@ function isValidStep(step) {
|
|
|
21593
22448
|
var UnilateralExit = {
|
|
21594
22449
|
estimate,
|
|
21595
22450
|
prepare,
|
|
22451
|
+
execute,
|
|
21596
22452
|
Executor
|
|
21597
22453
|
};
|
|
21598
22454
|
|
|
@@ -22010,6 +22866,10 @@ var ArkadeContract = class _ArkadeContract {
|
|
|
22010
22866
|
* When a `contractManager` is configured and this contract is registered,
|
|
22011
22867
|
* reads the repository-backed state (offline-first, kept fresh by the
|
|
22012
22868
|
* contract watcher). Otherwise falls back to a direct indexer query.
|
|
22869
|
+
*
|
|
22870
|
+
* Both branches refuse a spent or unilaterally exited output. They are not
|
|
22871
|
+
* otherwise interchangeable: the fallback asks `spendableOnly`, which also
|
|
22872
|
+
* drops swept coins — the manager branch keeps those.
|
|
22013
22873
|
*/
|
|
22014
22874
|
async getUtxos() {
|
|
22015
22875
|
const manager = this.client.contractManager;
|
|
@@ -22018,7 +22878,9 @@ var ArkadeContract = class _ArkadeContract {
|
|
|
22018
22878
|
const [registered] = await manager.getContracts({ script: scriptHex });
|
|
22019
22879
|
if (registered) {
|
|
22020
22880
|
const [withVtxos] = await manager.getContractsWithVtxos({ script: scriptHex });
|
|
22021
|
-
return (withVtxos?.vtxos ?? []).filter(
|
|
22881
|
+
return (withVtxos?.vtxos ?? []).filter(
|
|
22882
|
+
(v) => !hasTerminalSpend(v) && !v.isUnrolled
|
|
22883
|
+
);
|
|
22022
22884
|
}
|
|
22023
22885
|
}
|
|
22024
22886
|
if (!this.client.indexer) {
|
|
@@ -22028,7 +22890,7 @@ var ArkadeContract = class _ArkadeContract {
|
|
|
22028
22890
|
scripts: [scriptHex],
|
|
22029
22891
|
spendableOnly: true
|
|
22030
22892
|
});
|
|
22031
|
-
return vtxos;
|
|
22893
|
+
return vtxos.filter((v) => !hasTerminalSpend(v) && !v.isUnrolled);
|
|
22032
22894
|
}
|
|
22033
22895
|
/** Total spendable balance (requires an indexer). */
|
|
22034
22896
|
async getBalance() {
|
|
@@ -22128,6 +22990,19 @@ var ArkadeTransactionBuilder = class {
|
|
|
22128
22990
|
if (coin.sourceTx) {
|
|
22129
22991
|
setArkPsbtField(arkTx, 0, PrevArkTxField, coin.sourceTx);
|
|
22130
22992
|
}
|
|
22993
|
+
if (this.fn.arkadeScript) {
|
|
22994
|
+
const indexer = this.contract.client.indexer;
|
|
22995
|
+
if (!indexer) {
|
|
22996
|
+
throw new PrevTxUnavailableError(
|
|
22997
|
+
"covenant spends require an `indexer` on the Arkade client to resolve the previous ark tx of each input"
|
|
22998
|
+
);
|
|
22999
|
+
}
|
|
23000
|
+
await attachPrevArkTxs(
|
|
23001
|
+
arkTx,
|
|
23002
|
+
[coin.txid, ...this.fundingCoins.map((c) => c.txid)],
|
|
23003
|
+
indexer
|
|
23004
|
+
);
|
|
23005
|
+
}
|
|
22131
23006
|
const condition = (def.tapscript.witness ?? []).map((w) => this.witnessBytes(w));
|
|
22132
23007
|
if (condition.length > 0) {
|
|
22133
23008
|
setArkPsbtField(arkTx, 0, ConditionWitness, condition);
|
|
@@ -22633,6 +23508,6 @@ function createDefaultPaymentRouter(wallet) {
|
|
|
22633
23508
|
(*! noble-hashes - MIT License (c) 2022 Paul Miller (paulmillr.com) *)
|
|
22634
23509
|
*/
|
|
22635
23510
|
|
|
22636
|
-
export { ARKADE_MAGIC, ARKADE_SALTED_PREIMAGE_TAG, ARKADE_SWAP_PREIMAGE_TAG, ActivityRegistry, AmbiguousRouteError, ArkNote, ArkadeCash, ArkadeCashCreateError, AssetManager, BIP21, BIP21Error, BIP322, BLOCKED_UPGRADE_TIMEOUT_MS, Batch, ChainedTxType, ConnectionDisposedError, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DEFAULT_MIN_BATCH_EXPIRY_SECONDS, DEFAULT_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, DatabaseUpgradeBlockedError, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorIdentity, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, ForeignDescriptorError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MAX_USED_SIGNING_DESCRIPTORS_LOOK_AHEAD, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MUTINYNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, PaymentRouter, REGTEST_MIN_BATCH_EXPIRY_SECONDS, REGTEST_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SIGNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnannotatableInputError, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletCannotSignError, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, adoptContractDescriptor, arkRail, arkTarget, arkade_exports, assertFinalCommitmentMatchesValidated, assertIntentIdUnique, assertSendableAmount, assertSubmittedArkTxid, assertValidBatchExpiry, assertValidServerUnrollScript, assetMintResolver, awaitTransaction, boardingResolver, btcTarget, buildForfeitTx, buildOffchainTx, buildPreimageMessage, buildSaltedPreimageMessage, claimWithPreimageIdentity, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, collabExitResolver, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, contractPreimage, contractSigner, createAssetPacket, createDefaultActivityRegistry, createDefaultPaymentRouter, createExitChainResolver, createManagedConnection, decodeArkContract, defaultBatchExpiryPolicy, defaultCheckpointExitDelayPolicy, deleteByIndex, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, extendVirtualCoinForContract, gateExclusion, gatedContracts, getAllByIndexValues, getMigrationStatus, getRandomId, hasBoardingTxExpired, hasCandidates, intentMatchesFilter, intentPageBounds, invoiceTarget, isArkContract, isBatchSignable, isBtcAddress, isContractGenericallySpendable, isCooperativelyMigratable, isDeterministicSigner, isDiscoverable, isHDAllocationCapable, isHDDeterministicSignCapable, isHDWalletCapable, isLightningInvoice, isLnurl, isMessageBusInitializingError, isMessageBusNotInitializedError, isPerArtifactDescriptor, isSigningIdentity, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, isWatchedContract, logExcludedVtxos, makeHandle, matchServerCheckpoints, mergeChainedTxType, migrateWalletRepository, onchainRail, openDatabase, outpointExclusion, outpointReasons, promisifyRequest, provisionClaimSecret, provisionRefundKey, requiresMigration, resolveBatchExpiryPolicy, resolveCheckpointExitDelayPolicy, resolveDescriptorSigner, resolveSendAmount, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signAndSubmitOffchainTx, signerSetFromInfo, signingDescriptorIndex, strictSigningDescriptorIndex, toXOnlySignerHex, tryResolveSendAmount, validateBatchRecipients, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript };
|
|
22637
|
-
//# sourceMappingURL=chunk-
|
|
22638
|
-
//# sourceMappingURL=chunk-
|
|
23511
|
+
export { ARKADE_MAGIC, ARKADE_SALTED_PREIMAGE_TAG, ARKADE_SWAP_PREIMAGE_TAG, ActivityRegistry, AmbiguousRouteError, ArkNote, ArkadeCash, ArkadeCashCreateError, AssetManager, BIP21, BIP21Error, BIP322, BLOCKED_UPGRADE_TIMEOUT_MS, Batch, CachingArkProvider, ChainedTxType, ConnectionDisposedError, ContractManager, ContractRepositoryImpl, ContractWatcher, DB_VERSION, DEFAULT_MESSAGE_TIMEOUTS, DEFAULT_MIN_BATCH_EXPIRY_SECONDS, DEFAULT_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, DatabaseUpgradeBlockedError, DelegateManagerImpl, DelegateNotConfiguredError, DelegatorManagerImpl, DelegatorNotConfiguredError, DescriptorIdentity, DescriptorSigningProviderMissingError, DustChangeError, ELECTRUM_TCP_HOST, ELECTRUM_WS_URL, ESPLORA_URL, ElectrumOnchainProvider, EmulatorPacket, EsploraProvider, Estimator, Extension, ExtensionNotFoundError, ForeignDescriptorError, HDDescriptorProvider, INTENT_TERMINAL_STATES, InMemoryContractRepository, InMemoryIntentRepository, InMemoryVirtualTxRepository, InMemoryWalletRepository, IndexedDBContractRepository, IndexedDBIntentRepository, IndexedDBVirtualTxRepository, IndexedDBWalletRepository, MAX_USED_SIGNING_DESCRIPTORS_LOOK_AHEAD, MESSAGE_BUS_INITIALIZING, MESSAGE_BUS_NOT_INITIALIZED, MIGRATION_KEY, MUTINYNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, MessageBus, MessageBusInitializingError, MessageBusNotInitializedError, MissingSigningDescriptorError, MnemonicIdentity, OnchainWallet, P2A, PaymentRouter, PrevTxUnavailableError, REGTEST_MIN_BATCH_EXPIRY_SECONDS, REGTEST_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, Ramps, ReadonlyAssetManager, ReadonlyDescriptorIdentity, ReadonlySingleKey, ReadonlyWallet, ReadonlyWalletError, RestDelegateProvider, RestDelegatorProvider, RestEmulatorProvider, SIGNET_MIN_CHECKPOINT_EXIT_DELAY_SECONDS, SeedIdentity, ServiceWorkerReadonlyWallet, ServiceWorkerTimeoutError, ServiceWorkerWallet, SingleKey, TxTree, TxType, TxWeightEstimator, UnannotatableInputError, UnilateralExit, UnknownPacket, Unroll, VtxoManager, Wallet2 as Wallet, WalletCannotAllocateAddressError, WalletCannotSignError, WalletMessageHandler, WalletNotInitializedError, WalletRepositoryImpl, WsElectrumChainSource, adoptContractDescriptor, arkRail, arkTarget, arkade_exports, assertFinalCommitmentMatchesValidated, assertIntentIdUnique, assertSendableAmount, assertSubmittedArkTxid, assertValidBatchExpiry, assertValidServerUnrollScript, assetMintResolver, attachPrevArkTxs, attachPrevoutTxs, awaitTransaction, boardingResolver, btcTarget, buildForfeitTx, buildOffchainTx, buildPreimageMessage, buildSaltedPreimageMessage, claimWithPreimageIdentity, classifyAgainstSignerSet, classifyContractSigner, closeDatabase, collabExitResolver, combineTapscriptSigs, contractFromArkContract, contractFromArkContractWithAddress, contractPreimage, contractSigner, createAssetPacket, createDefaultActivityRegistry, createDefaultPaymentRouter, createExitChainResolver, createManagedConnection, decodeArkContract, defaultBatchExpiryPolicy, defaultCheckpointExitDelayPolicy, deleteByIndex, deserializeAssets, deserializeExitPackage, deserializeUtxo, deserializeVtxo, encodeArkContract, exitObserverFor, extendVirtualCoinForContract, extractArkProviderUrl, gateExclusion, gatedContracts, getAllByIndexValues, getMigrationStatus, getRandomId, hasBoardingTxExpired, hasCandidates, intentMatchesFilter, intentPageBounds, invoiceTarget, isAddressAllocationCapable, isArkContract, isBatchSignable, isBtcAddress, isContractGenericallySpendable, isCooperativelyMigratable, isDeterministicSigner, isDiscoverable, isHDAllocationCapable, isHDDeterministicSignCapable, isHDWalletCapable, isLightningInvoice, isLnurl, isMessageBusInitializingError, isMessageBusNotInitializedError, isPerArtifactDescriptor, isSigningIdentity, isSubdust, isTerminalIntentState, isValidArkAddress, isVtxoExpiringSoon, isVtxoForScript, isWatchedContract, logExcludedVtxos, makeHandle, matchServerCheckpoints, mergeChainedTxType, migrateWalletRepository, notifyExitObserved, onchainRail, openDatabase, outpointExclusion, outpointReasons, promisifyRequest, provisionClaimSecret, provisionRefundKey, requiresMigration, resolveBatchExpiryPolicy, resolveCheckpointExitDelayPolicy, resolveDescriptorSigner, resolvePrevTxs, resolveSendAmount, rollbackMigration, saveVtxosForContract, scriptFromArkAddress, selectCoinsWithAsset, selectVirtualCoins, serializeAssets, serializeExitPackage, serializeUtxo, serializeVtxo, setupServiceWorker, signAndSubmitOffchainTx, signerSetFromInfo, signingDescriptorIndex, strictSigningDescriptorIndex, toXOnlySignerHex, tryResolveSendAmount, validateBatchRecipients, validateConnectorsTxGraph, validateVtxoTxGraph, verifyTapscriptSignatures, waitForIncomingFunds, warnAndFilterVtxosForScript, withPrevTxs };
|
|
23512
|
+
//# sourceMappingURL=chunk-VBHXBI7F.js.map
|
|
23513
|
+
//# sourceMappingURL=chunk-VBHXBI7F.js.map
|