@arkade-os/sdk 0.4.14 → 0.4.15
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 +208 -142
- package/dist/cjs/contracts/handlers/helpers.js +24 -0
- package/dist/cjs/contracts/handlers/vhtlc.js +2 -4
- package/dist/cjs/identity/index.js +6 -0
- package/dist/cjs/identity/seedIdentity.js +3 -3
- package/dist/cjs/index.js +5 -3
- package/dist/cjs/script/tapscript.js +8 -2
- package/dist/cjs/wallet/vtxo-manager.js +1 -0
- package/dist/cjs/wallet/wallet.js +112 -37
- package/dist/esm/contracts/handlers/helpers.js +23 -0
- package/dist/esm/contracts/handlers/vhtlc.js +3 -5
- package/dist/esm/identity/index.js +5 -0
- package/dist/esm/identity/seedIdentity.js +3 -3
- package/dist/esm/index.js +2 -1
- package/dist/esm/script/tapscript.js +8 -2
- package/dist/esm/wallet/vtxo-manager.js +1 -0
- package/dist/esm/wallet/wallet.js +113 -38
- package/dist/types/contracts/handlers/helpers.d.ts +10 -0
- package/dist/types/identity/index.d.ts +20 -0
- package/dist/types/identity/seedIdentity.d.ts +2 -2
- package/dist/types/index.d.ts +3 -3
- package/package.json +18 -10
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.timelockToSequence = timelockToSequence;
|
|
37
37
|
exports.sequenceToTimelock = sequenceToTimelock;
|
|
38
38
|
exports.resolveRole = resolveRole;
|
|
39
|
+
exports.isCltvSatisfied = isCltvSatisfied;
|
|
39
40
|
exports.isCsvSpendable = isCsvSpendable;
|
|
40
41
|
const bip68 = __importStar(require("bip68"));
|
|
41
42
|
/**
|
|
@@ -78,6 +79,29 @@ function resolveRole(contract, context) {
|
|
|
78
79
|
}
|
|
79
80
|
return undefined;
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* BIP65 threshold: locktime values below this are interpreted as block heights,
|
|
84
|
+
* values at or above are interpreted as Unix timestamps (seconds).
|
|
85
|
+
*/
|
|
86
|
+
const CLTV_HEIGHT_THRESHOLD = 500000000n;
|
|
87
|
+
/**
|
|
88
|
+
* Check if an absolute (CLTV) locktime is currently satisfied.
|
|
89
|
+
*
|
|
90
|
+
* Following the BIP65 convention:
|
|
91
|
+
* - locktime < 500_000_000 → interpreted as a block height; compared against `context.blockHeight`
|
|
92
|
+
* - locktime >= 500_000_000 → interpreted as a Unix timestamp (seconds); compared against `context.currentTime`
|
|
93
|
+
*
|
|
94
|
+
* Returns false if the relevant context field is missing.
|
|
95
|
+
*/
|
|
96
|
+
function isCltvSatisfied(context, locktime) {
|
|
97
|
+
if (locktime < CLTV_HEIGHT_THRESHOLD) {
|
|
98
|
+
if (context.blockHeight === undefined)
|
|
99
|
+
return false;
|
|
100
|
+
return BigInt(context.blockHeight) >= locktime;
|
|
101
|
+
}
|
|
102
|
+
const currentTimeSec = BigInt(Math.floor(context.currentTime / 1000));
|
|
103
|
+
return currentTimeSec >= locktime;
|
|
104
|
+
}
|
|
81
105
|
/**
|
|
82
106
|
* Check if a CSV timelock is currently satisfied for the given context/VTXO.
|
|
83
107
|
*/
|
|
@@ -59,7 +59,6 @@ exports.VHTLCContractHandler = {
|
|
|
59
59
|
const role = (0, helpers_1.resolveRole)(contract, context);
|
|
60
60
|
const preimage = contract.params?.preimage;
|
|
61
61
|
const refundLocktime = BigInt(contract.params.refundLocktime);
|
|
62
|
-
const currentTimeSec = Math.floor(context.currentTime / 1000);
|
|
63
62
|
if (!role) {
|
|
64
63
|
return null;
|
|
65
64
|
}
|
|
@@ -70,7 +69,7 @@ exports.VHTLCContractHandler = {
|
|
|
70
69
|
extraWitness: [base_1.hex.decode(preimage)],
|
|
71
70
|
};
|
|
72
71
|
}
|
|
73
|
-
if (role === "sender" &&
|
|
72
|
+
if (role === "sender" && (0, helpers_1.isCltvSatisfied)(context, refundLocktime)) {
|
|
74
73
|
return {
|
|
75
74
|
leaf: script.refundWithoutReceiver(),
|
|
76
75
|
};
|
|
@@ -154,7 +153,6 @@ exports.VHTLCContractHandler = {
|
|
|
154
153
|
}
|
|
155
154
|
const preimage = contract.params?.preimage;
|
|
156
155
|
const refundLocktime = BigInt(contract.params.refundLocktime);
|
|
157
|
-
const currentTimeSec = Math.floor(context.currentTime / 1000);
|
|
158
156
|
if (context.collaborative) {
|
|
159
157
|
if (role === "receiver" && preimage) {
|
|
160
158
|
paths.push({
|
|
@@ -162,7 +160,7 @@ exports.VHTLCContractHandler = {
|
|
|
162
160
|
extraWitness: [base_1.hex.decode(preimage)],
|
|
163
161
|
});
|
|
164
162
|
}
|
|
165
|
-
if (role === "sender" &&
|
|
163
|
+
if (role === "sender" && (0, helpers_1.isCltvSatisfied)(context, refundLocktime)) {
|
|
166
164
|
paths.push({
|
|
167
165
|
leaf: script.refundWithoutReceiver(),
|
|
168
166
|
});
|
|
@@ -14,5 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.isBatchSignable = isBatchSignable;
|
|
18
|
+
/** Type guard for identities that support batch signing. */
|
|
19
|
+
function isBatchSignable(identity) {
|
|
20
|
+
return ("signMultiple" in identity &&
|
|
21
|
+
typeof identity.signMultiple === "function");
|
|
22
|
+
}
|
|
17
23
|
__exportStar(require("./singleKey"), exports);
|
|
18
24
|
__exportStar(require("./seedIdentity"), exports);
|
|
@@ -17,7 +17,7 @@ const ALL_SIGHASH = Object.values(btc_signer_1.SigHash).filter((x) => typeof x =
|
|
|
17
17
|
function detectNetwork(descriptor) {
|
|
18
18
|
return descriptor.includes("tpub") ? descriptors_scure_1.networks.testnet : descriptors_scure_1.networks.bitcoin;
|
|
19
19
|
}
|
|
20
|
-
function hasDescriptor(opts) {
|
|
20
|
+
function hasDescriptor(opts = {}) {
|
|
21
21
|
return "descriptor" in opts && typeof opts.descriptor === "string";
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -101,7 +101,7 @@ class SeedIdentity {
|
|
|
101
101
|
* @param seed - 64-byte seed (typically from mnemonicToSeedSync)
|
|
102
102
|
* @param opts - Network selection or custom descriptor.
|
|
103
103
|
*/
|
|
104
|
-
static fromSeed(seed, opts) {
|
|
104
|
+
static fromSeed(seed, opts = {}) {
|
|
105
105
|
const descriptor = hasDescriptor(opts)
|
|
106
106
|
? opts.descriptor
|
|
107
107
|
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
@@ -185,7 +185,7 @@ class MnemonicIdentity extends SeedIdentity {
|
|
|
185
185
|
* @param phrase - BIP39 mnemonic phrase (12 or 24 words)
|
|
186
186
|
* @param opts - Network selection or custom descriptor, plus optional passphrase
|
|
187
187
|
*/
|
|
188
|
-
static fromMnemonic(phrase, opts) {
|
|
188
|
+
static fromMnemonic(phrase, opts = {}) {
|
|
189
189
|
if (!(0, bip39_1.validateMnemonic)(phrase, english_js_1.wordlist)) {
|
|
190
190
|
throw new Error("Invalid mnemonic");
|
|
191
191
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -36,9 +36,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
41
|
-
exports.isArkContract = exports.contractFromArkContractWithAddress = exports.contractFromArkContract = void 0;
|
|
39
|
+
exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.DEFAULT_MESSAGE_TIMEOUTS = exports.ServiceWorkerReadonlyWallet = exports.ServiceWorkerWallet = exports.ServiceWorkerTimeoutError = exports.MessageBusNotInitializedError = exports.MESSAGE_BUS_NOT_INITIALIZED = exports.DelegatorNotConfiguredError = exports.ReadonlyWalletError = exports.WalletNotInitializedError = exports.WalletMessageHandler = exports.MessageBus = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DelegateVtxo = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.RestDelegatorProvider = exports.DelegatorManagerImpl = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.isBatchSignable = exports.ReadonlyDescriptorIdentity = exports.MnemonicIdentity = exports.SeedIdentity = exports.ReadonlySingleKey = exports.SingleKey = exports.ReadonlyWallet = exports.Wallet = exports.asset = void 0;
|
|
40
|
+
exports.encodeArkContract = exports.VHTLCContractHandler = exports.DelegateContractHandler = exports.DefaultContractHandler = exports.contractHandlers = exports.ContractWatcher = exports.ContractManager = exports.getSequence = exports.isExpired = exports.isSubdust = exports.isSpendable = exports.isRecoverable = exports.buildForfeitTx = exports.validateConnectorsTxGraph = exports.validateVtxoTxGraph = exports.Batch = exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.BIP322 = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.rollbackMigration = exports.getMigrationStatus = exports.requiresMigration = exports.migrateWalletRepository = exports.MIGRATION_KEY = exports.InMemoryContractRepository = exports.InMemoryWalletRepository = exports.IndexedDBContractRepository = exports.IndexedDBWalletRepository = exports.openDatabase = exports.closeDatabase = exports.networks = exports.ArkNote = exports.isValidArkAddress = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = void 0;
|
|
41
|
+
exports.isArkContract = exports.contractFromArkContractWithAddress = exports.contractFromArkContract = exports.decodeArkContract = void 0;
|
|
42
42
|
const transaction_1 = require("./utils/transaction");
|
|
43
43
|
Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
|
|
44
44
|
const singleKey_1 = require("./identity/singleKey");
|
|
@@ -48,6 +48,8 @@ const seedIdentity_1 = require("./identity/seedIdentity");
|
|
|
48
48
|
Object.defineProperty(exports, "SeedIdentity", { enumerable: true, get: function () { return seedIdentity_1.SeedIdentity; } });
|
|
49
49
|
Object.defineProperty(exports, "MnemonicIdentity", { enumerable: true, get: function () { return seedIdentity_1.MnemonicIdentity; } });
|
|
50
50
|
Object.defineProperty(exports, "ReadonlyDescriptorIdentity", { enumerable: true, get: function () { return seedIdentity_1.ReadonlyDescriptorIdentity; } });
|
|
51
|
+
const identity_1 = require("./identity");
|
|
52
|
+
Object.defineProperty(exports, "isBatchSignable", { enumerable: true, get: function () { return identity_1.isBatchSignable; } });
|
|
51
53
|
const address_1 = require("./script/address");
|
|
52
54
|
Object.defineProperty(exports, "ArkAddress", { enumerable: true, get: function () { return address_1.ArkAddress; } });
|
|
53
55
|
const vhtlc_1 = require("./script/vhtlc");
|
|
@@ -535,7 +535,7 @@ var CLTVMultisigTapscript;
|
|
|
535
535
|
throw new Error(`Invalid script: too short (expected at least 3)`);
|
|
536
536
|
}
|
|
537
537
|
const locktime = asm[0];
|
|
538
|
-
if (typeof locktime === "string"
|
|
538
|
+
if (typeof locktime === "string") {
|
|
539
539
|
throw new Error("Invalid script: expected locktime number");
|
|
540
540
|
}
|
|
541
541
|
if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
|
|
@@ -549,7 +549,13 @@ var CLTVMultisigTapscript;
|
|
|
549
549
|
catch (error) {
|
|
550
550
|
throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
|
|
551
551
|
}
|
|
552
|
-
|
|
552
|
+
let absoluteTimelock;
|
|
553
|
+
if (typeof locktime === "number") {
|
|
554
|
+
absoluteTimelock = BigInt(locktime);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
absoluteTimelock = MinimalScriptNum.decode(locktime);
|
|
558
|
+
}
|
|
553
559
|
const reconstructed = encode({
|
|
554
560
|
absoluteTimelock,
|
|
555
561
|
...multisig.params,
|
|
@@ -581,6 +581,7 @@ class VtxoManager {
|
|
|
581
581
|
return;
|
|
582
582
|
}
|
|
583
583
|
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
584
|
+
e.message.includes("VTXO_ALREADY_SPENT") ||
|
|
584
585
|
e.message.includes("duplicated input")) {
|
|
585
586
|
// VTXO is already being used in a concurrent
|
|
586
587
|
// user-initiated operation. Skip silently — the
|
|
@@ -15,6 +15,7 @@ const ark_1 = require("../providers/ark");
|
|
|
15
15
|
const forfeit_1 = require("../forfeit");
|
|
16
16
|
const validation_1 = require("../tree/validation");
|
|
17
17
|
const validation_2 = require("./validation");
|
|
18
|
+
const identity_1 = require("../identity");
|
|
18
19
|
const _1 = require(".");
|
|
19
20
|
const asset_1 = require("./asset");
|
|
20
21
|
const base_2 = require("../script/base");
|
|
@@ -345,6 +346,19 @@ class ReadonlyWallet {
|
|
|
345
346
|
}
|
|
346
347
|
const requestStartedAt = Date.now();
|
|
347
348
|
const allVtxos = [];
|
|
349
|
+
const extendWithScript = (vtxo) => {
|
|
350
|
+
const vtxoScript = vtxo.script
|
|
351
|
+
? scriptMap.get(vtxo.script)
|
|
352
|
+
: undefined;
|
|
353
|
+
if (!vtxoScript)
|
|
354
|
+
return undefined;
|
|
355
|
+
return {
|
|
356
|
+
...vtxo,
|
|
357
|
+
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
358
|
+
intentTapLeafScript: vtxoScript.forfeit(),
|
|
359
|
+
tapTree: vtxoScript.encode(),
|
|
360
|
+
};
|
|
361
|
+
};
|
|
348
362
|
// Full fetch for scripts with no cursor.
|
|
349
363
|
if (bootstrapScripts.length > 0) {
|
|
350
364
|
const response = await this.indexerProvider.getVtxos({
|
|
@@ -369,46 +383,76 @@ class ReadonlyWallet {
|
|
|
369
383
|
// Extend every fetched VTXO and upsert into the cache.
|
|
370
384
|
const fetchedExtended = [];
|
|
371
385
|
for (const vtxo of allVtxos) {
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!vtxoScript)
|
|
376
|
-
continue;
|
|
377
|
-
fetchedExtended.push({
|
|
378
|
-
...vtxo,
|
|
379
|
-
forfeitTapLeafScript: vtxoScript.forfeit(),
|
|
380
|
-
intentTapLeafScript: vtxoScript.forfeit(),
|
|
381
|
-
tapTree: vtxoScript.encode(),
|
|
382
|
-
});
|
|
386
|
+
const extended = extendWithScript(vtxo);
|
|
387
|
+
if (extended)
|
|
388
|
+
fetchedExtended.push(extended);
|
|
383
389
|
}
|
|
384
390
|
// Save VTXOs first, then advance cursors only on success.
|
|
385
391
|
const cutoff = (0, syncCursors_1.cursorCutoff)(requestStartedAt);
|
|
386
392
|
await this.walletRepository.saveVtxos(address, fetchedExtended);
|
|
387
393
|
await (0, syncCursors_1.advanceSyncCursors)(this.walletRepository, Object.fromEntries(allScripts.map((s) => [s, cutoff])));
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
394
|
+
// Delta-sync reconciliation: full re-fetch for delta scripts.
|
|
395
|
+
//
|
|
396
|
+
// The delta fetch (above) only returns VTXOs changed after the
|
|
397
|
+
// cursor, so it can miss preconfirmed VTXOs that were consumed
|
|
398
|
+
// by a round between syncs. Rather than layering targeted
|
|
399
|
+
// queries (pendingOnly, spendableOnly) with pagination guards
|
|
400
|
+
// and set algebra, we perform a single unfiltered re-fetch for
|
|
401
|
+
// delta scripts. This is slightly more data over the wire but
|
|
402
|
+
// gives us complete, authoritative state in one call and keeps
|
|
403
|
+
// the reconciliation logic simple.
|
|
404
|
+
//
|
|
405
|
+
// Any cached non-spent VTXO that is absent from the full
|
|
406
|
+
// result set is marked spent; any VTXO whose state changed
|
|
407
|
+
// (e.g. preconfirmed → settled) is updated in place.
|
|
391
408
|
if (hasDelta) {
|
|
392
|
-
const { vtxos:
|
|
409
|
+
const { vtxos: fullVtxos, page: fullPage } = await this.indexerProvider.getVtxos({
|
|
393
410
|
scripts: deltaScripts,
|
|
394
|
-
pendingOnly: true,
|
|
395
411
|
});
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
412
|
+
// Reconciliation is best-effort: if the response is
|
|
413
|
+
// paginated we don't have a complete picture, so we skip
|
|
414
|
+
// rather than act on partial data. Wallets with enough
|
|
415
|
+
// VTXOs to exceed a single page rely solely on the
|
|
416
|
+
// cursor-based delta mechanism for state updates.
|
|
417
|
+
const fullSetComplete = !fullPage || fullPage.total <= 1;
|
|
418
|
+
if (fullSetComplete) {
|
|
419
|
+
const fullOutpoints = new Map(fullVtxos.map((v) => [`${v.txid}:${v.vout}`, v]));
|
|
420
|
+
const deltaScriptSet = new Set(deltaScripts);
|
|
421
|
+
const cachedVtxos = await this.walletRepository.getVtxos(address);
|
|
422
|
+
const reconciledExtended = [];
|
|
423
|
+
for (const cached of cachedVtxos) {
|
|
424
|
+
if (!cached.script ||
|
|
425
|
+
!deltaScriptSet.has(cached.script) ||
|
|
426
|
+
cached.isSpent) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const outpoint = `${cached.txid}:${cached.vout}`;
|
|
430
|
+
const fresh = fullOutpoints.get(outpoint);
|
|
431
|
+
if (!fresh) {
|
|
432
|
+
// Server no longer knows about this VTXO —
|
|
433
|
+
// it was spent between syncs.
|
|
434
|
+
reconciledExtended.push({
|
|
435
|
+
...cached,
|
|
436
|
+
isSpent: true,
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const extended = extendWithScript(fresh);
|
|
441
|
+
if (extended &&
|
|
442
|
+
extended.virtualStatus.state !==
|
|
443
|
+
cached.virtualStatus.state) {
|
|
444
|
+
// State transitioned (e.g. preconfirmed →
|
|
445
|
+
// settled) — update the cached entry.
|
|
446
|
+
reconciledExtended.push(extended);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (reconciledExtended.length > 0) {
|
|
450
|
+
console.warn(`[ark-sdk] delta sync: reconciled ${reconciledExtended.length} stale VTXO(s) via full re-fetch`);
|
|
451
|
+
await this.walletRepository.saveVtxos(address, reconciledExtended);
|
|
452
|
+
}
|
|
409
453
|
}
|
|
410
|
-
|
|
411
|
-
|
|
454
|
+
else {
|
|
455
|
+
console.warn("[ark-sdk] delta sync: skipping reconciliation — full re-fetch was paginated");
|
|
412
456
|
}
|
|
413
457
|
}
|
|
414
458
|
return {
|
|
@@ -1719,16 +1763,47 @@ class Wallet extends ReadonlyWallet {
|
|
|
1719
1763
|
tapLeafScript: input.forfeitTapLeafScript,
|
|
1720
1764
|
};
|
|
1721
1765
|
}), outputs, this.serverUnrollScript);
|
|
1722
|
-
|
|
1766
|
+
let signedVirtualTx;
|
|
1767
|
+
let userSignedCheckpoints;
|
|
1768
|
+
if ((0, identity_1.isBatchSignable)(this.identity)) {
|
|
1769
|
+
// Batch-sign arkTx + all checkpoints in one wallet popup.
|
|
1770
|
+
// Clone so the provider can't mutate originals before submitTx.
|
|
1771
|
+
const requests = [
|
|
1772
|
+
{ tx: offchainTx.arkTx.clone() },
|
|
1773
|
+
...offchainTx.checkpoints.map((c) => ({ tx: c.clone() })),
|
|
1774
|
+
];
|
|
1775
|
+
const signed = await this.identity.signMultiple(requests);
|
|
1776
|
+
if (signed.length !== requests.length) {
|
|
1777
|
+
throw new Error(`signMultiple returned ${signed.length} transactions, expected ${requests.length}`);
|
|
1778
|
+
}
|
|
1779
|
+
const [firstSignedTx, ...signedCheckpoints] = signed;
|
|
1780
|
+
signedVirtualTx = firstSignedTx;
|
|
1781
|
+
userSignedCheckpoints = signedCheckpoints;
|
|
1782
|
+
}
|
|
1783
|
+
else {
|
|
1784
|
+
signedVirtualTx = await this.identity.sign(offchainTx.arkTx);
|
|
1785
|
+
}
|
|
1723
1786
|
// Mark pending before submitting — if we crash between submit and
|
|
1724
1787
|
// finalize, the next init will recover via finalizePendingTxs.
|
|
1725
1788
|
await this.setPendingTxFlag(true);
|
|
1726
1789
|
const { arkTxid, signedCheckpointTxs } = await this.arkProvider.submitTx(base_1.base64.encode(signedVirtualTx.toPSBT()), offchainTx.checkpoints.map((c) => base_1.base64.encode(c.toPSBT())));
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1790
|
+
let finalCheckpoints;
|
|
1791
|
+
if (userSignedCheckpoints) {
|
|
1792
|
+
// Merge pre-signed user signatures onto server-signed checkpoints
|
|
1793
|
+
finalCheckpoints = signedCheckpointTxs.map((c, i) => {
|
|
1794
|
+
const serverSigned = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1795
|
+
(0, arkTransaction_1.combineTapscriptSigs)(userSignedCheckpoints[i], serverSigned);
|
|
1796
|
+
return base_1.base64.encode(serverSigned.toPSBT());
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
else {
|
|
1800
|
+
// Legacy: sign each checkpoint individually (N popups)
|
|
1801
|
+
finalCheckpoints = await Promise.all(signedCheckpointTxs.map(async (c) => {
|
|
1802
|
+
const tx = btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(c));
|
|
1803
|
+
const signedCheckpoint = await this.identity.sign(tx);
|
|
1804
|
+
return base_1.base64.encode(signedCheckpoint.toPSBT());
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1732
1807
|
await this.arkProvider.finalizeTx(arkTxid, finalCheckpoints);
|
|
1733
1808
|
try {
|
|
1734
1809
|
await this.setPendingTxFlag(false);
|
|
@@ -39,6 +39,29 @@ export function resolveRole(contract, context) {
|
|
|
39
39
|
}
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* BIP65 threshold: locktime values below this are interpreted as block heights,
|
|
44
|
+
* values at or above are interpreted as Unix timestamps (seconds).
|
|
45
|
+
*/
|
|
46
|
+
const CLTV_HEIGHT_THRESHOLD = 500000000n;
|
|
47
|
+
/**
|
|
48
|
+
* Check if an absolute (CLTV) locktime is currently satisfied.
|
|
49
|
+
*
|
|
50
|
+
* Following the BIP65 convention:
|
|
51
|
+
* - locktime < 500_000_000 → interpreted as a block height; compared against `context.blockHeight`
|
|
52
|
+
* - locktime >= 500_000_000 → interpreted as a Unix timestamp (seconds); compared against `context.currentTime`
|
|
53
|
+
*
|
|
54
|
+
* Returns false if the relevant context field is missing.
|
|
55
|
+
*/
|
|
56
|
+
export function isCltvSatisfied(context, locktime) {
|
|
57
|
+
if (locktime < CLTV_HEIGHT_THRESHOLD) {
|
|
58
|
+
if (context.blockHeight === undefined)
|
|
59
|
+
return false;
|
|
60
|
+
return BigInt(context.blockHeight) >= locktime;
|
|
61
|
+
}
|
|
62
|
+
const currentTimeSec = BigInt(Math.floor(context.currentTime / 1000));
|
|
63
|
+
return currentTimeSec >= locktime;
|
|
64
|
+
}
|
|
42
65
|
/**
|
|
43
66
|
* Check if a CSV timelock is currently satisfied for the given context/VTXO.
|
|
44
67
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { hex } from "@scure/base";
|
|
2
2
|
import { VHTLC } from '../../script/vhtlc.js';
|
|
3
|
-
import { isCsvSpendable, resolveRole, sequenceToTimelock, timelockToSequence, } from './helpers.js';
|
|
3
|
+
import { isCltvSatisfied, isCsvSpendable, resolveRole, sequenceToTimelock, timelockToSequence, } from './helpers.js';
|
|
4
4
|
/**
|
|
5
5
|
* Handler for Virtual Hash Time Lock Contract (VHTLC).
|
|
6
6
|
*
|
|
@@ -56,7 +56,6 @@ export const VHTLCContractHandler = {
|
|
|
56
56
|
const role = resolveRole(contract, context);
|
|
57
57
|
const preimage = contract.params?.preimage;
|
|
58
58
|
const refundLocktime = BigInt(contract.params.refundLocktime);
|
|
59
|
-
const currentTimeSec = Math.floor(context.currentTime / 1000);
|
|
60
59
|
if (!role) {
|
|
61
60
|
return null;
|
|
62
61
|
}
|
|
@@ -67,7 +66,7 @@ export const VHTLCContractHandler = {
|
|
|
67
66
|
extraWitness: [hex.decode(preimage)],
|
|
68
67
|
};
|
|
69
68
|
}
|
|
70
|
-
if (role === "sender" &&
|
|
69
|
+
if (role === "sender" && isCltvSatisfied(context, refundLocktime)) {
|
|
71
70
|
return {
|
|
72
71
|
leaf: script.refundWithoutReceiver(),
|
|
73
72
|
};
|
|
@@ -151,7 +150,6 @@ export const VHTLCContractHandler = {
|
|
|
151
150
|
}
|
|
152
151
|
const preimage = contract.params?.preimage;
|
|
153
152
|
const refundLocktime = BigInt(contract.params.refundLocktime);
|
|
154
|
-
const currentTimeSec = Math.floor(context.currentTime / 1000);
|
|
155
153
|
if (context.collaborative) {
|
|
156
154
|
if (role === "receiver" && preimage) {
|
|
157
155
|
paths.push({
|
|
@@ -159,7 +157,7 @@ export const VHTLCContractHandler = {
|
|
|
159
157
|
extraWitness: [hex.decode(preimage)],
|
|
160
158
|
});
|
|
161
159
|
}
|
|
162
|
-
if (role === "sender" &&
|
|
160
|
+
if (role === "sender" && isCltvSatisfied(context, refundLocktime)) {
|
|
163
161
|
paths.push({
|
|
164
162
|
leaf: script.refundWithoutReceiver(),
|
|
165
163
|
});
|
|
@@ -1,2 +1,7 @@
|
|
|
1
|
+
/** Type guard for identities that support batch signing. */
|
|
2
|
+
export function isBatchSignable(identity) {
|
|
3
|
+
return ("signMultiple" in identity &&
|
|
4
|
+
typeof identity.signMultiple === "function");
|
|
5
|
+
}
|
|
1
6
|
export * from './singleKey.js';
|
|
2
7
|
export * from './seedIdentity.js';
|
|
@@ -14,7 +14,7 @@ const ALL_SIGHASH = Object.values(SigHash).filter((x) => typeof x === "number");
|
|
|
14
14
|
function detectNetwork(descriptor) {
|
|
15
15
|
return descriptor.includes("tpub") ? networks.testnet : networks.bitcoin;
|
|
16
16
|
}
|
|
17
|
-
function hasDescriptor(opts) {
|
|
17
|
+
function hasDescriptor(opts = {}) {
|
|
18
18
|
return "descriptor" in opts && typeof opts.descriptor === "string";
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -98,7 +98,7 @@ export class SeedIdentity {
|
|
|
98
98
|
* @param seed - 64-byte seed (typically from mnemonicToSeedSync)
|
|
99
99
|
* @param opts - Network selection or custom descriptor.
|
|
100
100
|
*/
|
|
101
|
-
static fromSeed(seed, opts) {
|
|
101
|
+
static fromSeed(seed, opts = {}) {
|
|
102
102
|
const descriptor = hasDescriptor(opts)
|
|
103
103
|
? opts.descriptor
|
|
104
104
|
: buildDescriptor(seed, opts.isMainnet ?? true);
|
|
@@ -181,7 +181,7 @@ export class MnemonicIdentity extends SeedIdentity {
|
|
|
181
181
|
* @param phrase - BIP39 mnemonic phrase (12 or 24 words)
|
|
182
182
|
* @param opts - Network selection or custom descriptor, plus optional passphrase
|
|
183
183
|
*/
|
|
184
|
-
static fromMnemonic(phrase, opts) {
|
|
184
|
+
static fromMnemonic(phrase, opts = {}) {
|
|
185
185
|
if (!validateMnemonic(phrase, wordlist)) {
|
|
186
186
|
throw new Error("Invalid mnemonic");
|
|
187
187
|
}
|
package/dist/esm/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Transaction } from './utils/transaction.js';
|
|
2
2
|
import { SingleKey, ReadonlySingleKey } from './identity/singleKey.js';
|
|
3
3
|
import { SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, } from './identity/seedIdentity.js';
|
|
4
|
+
import { isBatchSignable, } from './identity/index.js';
|
|
4
5
|
import { ArkAddress } from './script/address.js';
|
|
5
6
|
import { VHTLC } from './script/vhtlc.js';
|
|
6
7
|
import { DefaultVtxo } from './script/default.js';
|
|
@@ -43,7 +44,7 @@ import { WalletMessageHandler, WalletNotInitializedError, ReadonlyWalletError, D
|
|
|
43
44
|
import { MESSAGE_BUS_NOT_INITIALIZED, MessageBusNotInitializedError, ServiceWorkerTimeoutError, } from './worker/errors.js';
|
|
44
45
|
export {
|
|
45
46
|
// Wallets
|
|
46
|
-
Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
|
|
47
|
+
Wallet, ReadonlyWallet, SingleKey, ReadonlySingleKey, SeedIdentity, MnemonicIdentity, ReadonlyDescriptorIdentity, isBatchSignable, OnchainWallet, Ramps, VtxoManager, DelegatorManagerImpl, RestDelegatorProvider,
|
|
47
48
|
// Providers
|
|
48
49
|
ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider,
|
|
49
50
|
// Script-related
|
|
@@ -498,7 +498,7 @@ export var CLTVMultisigTapscript;
|
|
|
498
498
|
throw new Error(`Invalid script: too short (expected at least 3)`);
|
|
499
499
|
}
|
|
500
500
|
const locktime = asm[0];
|
|
501
|
-
if (typeof locktime === "string"
|
|
501
|
+
if (typeof locktime === "string") {
|
|
502
502
|
throw new Error("Invalid script: expected locktime number");
|
|
503
503
|
}
|
|
504
504
|
if (asm[1] !== "CHECKLOCKTIMEVERIFY" || asm[2] !== "DROP") {
|
|
@@ -512,7 +512,13 @@ export var CLTVMultisigTapscript;
|
|
|
512
512
|
catch (error) {
|
|
513
513
|
throw new Error(`Invalid multisig script: ${error instanceof Error ? error.message : String(error)}`);
|
|
514
514
|
}
|
|
515
|
-
|
|
515
|
+
let absoluteTimelock;
|
|
516
|
+
if (typeof locktime === "number") {
|
|
517
|
+
absoluteTimelock = BigInt(locktime);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
absoluteTimelock = MinimalScriptNum.decode(locktime);
|
|
521
|
+
}
|
|
516
522
|
const reconstructed = encode({
|
|
517
523
|
absoluteTimelock,
|
|
518
524
|
...multisig.params,
|
|
@@ -576,6 +576,7 @@ export class VtxoManager {
|
|
|
576
576
|
return;
|
|
577
577
|
}
|
|
578
578
|
if (e.message.includes("VTXO_ALREADY_REGISTERED") ||
|
|
579
|
+
e.message.includes("VTXO_ALREADY_SPENT") ||
|
|
579
580
|
e.message.includes("duplicated input")) {
|
|
580
581
|
// VTXO is already being used in a concurrent
|
|
581
582
|
// user-initiated operation. Skip silently — the
|