@arkade-os/sdk 0.4.13 → 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 +209 -143
- 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 +6 -3
- package/dist/cjs/script/tapscript.js +8 -2
- package/dist/cjs/wallet/serviceWorker/wallet.js +67 -8
- 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 +4 -3
- package/dist/esm/script/tapscript.js +8 -2
- package/dist/esm/wallet/serviceWorker/wallet.js +66 -7
- 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 +5 -4
- package/dist/types/wallet/serviceWorker/wallet.d.ts +6 -0
- package/package.json +19 -11
|
@@ -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 = 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");
|
|
@@ -84,6 +86,7 @@ Object.defineProperty(exports, "VtxoManager", { enumerable: true, get: function
|
|
|
84
86
|
const wallet_3 = require("./wallet/serviceWorker/wallet");
|
|
85
87
|
Object.defineProperty(exports, "ServiceWorkerWallet", { enumerable: true, get: function () { return wallet_3.ServiceWorkerWallet; } });
|
|
86
88
|
Object.defineProperty(exports, "ServiceWorkerReadonlyWallet", { enumerable: true, get: function () { return wallet_3.ServiceWorkerReadonlyWallet; } });
|
|
89
|
+
Object.defineProperty(exports, "DEFAULT_MESSAGE_TIMEOUTS", { enumerable: true, get: function () { return wallet_3.DEFAULT_MESSAGE_TIMEOUTS; } });
|
|
87
90
|
const onchain_1 = require("./wallet/onchain");
|
|
88
91
|
Object.defineProperty(exports, "OnchainWallet", { enumerable: true, get: function () { return onchain_1.OnchainWallet; } });
|
|
89
92
|
const utils_1 = require("./worker/browser/utils");
|
|
@@ -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,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ServiceWorkerWallet = exports.ServiceWorkerReadonlyWallet = void 0;
|
|
3
|
+
exports.ServiceWorkerWallet = exports.ServiceWorkerReadonlyWallet = exports.DEFAULT_MESSAGE_TIMEOUTS = void 0;
|
|
4
4
|
const base_1 = require("@scure/base");
|
|
5
5
|
const utils_1 = require("../../worker/browser/utils");
|
|
6
6
|
const repositories_1 = require("../../repositories");
|
|
@@ -14,6 +14,47 @@ function isMessageBusNotInitializedError(error) {
|
|
|
14
14
|
return (error instanceof Error &&
|
|
15
15
|
error.message.includes(errors_1.MESSAGE_BUS_NOT_INITIALIZED));
|
|
16
16
|
}
|
|
17
|
+
exports.DEFAULT_MESSAGE_TIMEOUTS = {
|
|
18
|
+
// Fast reads — fail quickly
|
|
19
|
+
GET_ADDRESS: 10000,
|
|
20
|
+
GET_BALANCE: 10000,
|
|
21
|
+
GET_BOARDING_ADDRESS: 10000,
|
|
22
|
+
GET_STATUS: 10000,
|
|
23
|
+
GET_DELEGATE_INFO: 10000,
|
|
24
|
+
IS_CONTRACT_MANAGER_WATCHING: 10000,
|
|
25
|
+
// Medium reads — may involve indexer queries
|
|
26
|
+
GET_VTXOS: 20000,
|
|
27
|
+
GET_BOARDING_UTXOS: 20000,
|
|
28
|
+
GET_TRANSACTION_HISTORY: 20000,
|
|
29
|
+
GET_CONTRACTS: 20000,
|
|
30
|
+
GET_CONTRACTS_WITH_VTXOS: 20000,
|
|
31
|
+
GET_SPENDABLE_PATHS: 20000,
|
|
32
|
+
GET_ALL_SPENDING_PATHS: 20000,
|
|
33
|
+
GET_ASSET_DETAILS: 20000,
|
|
34
|
+
GET_EXPIRING_VTXOS: 20000,
|
|
35
|
+
GET_EXPIRED_BOARDING_UTXOS: 20000,
|
|
36
|
+
GET_RECOVERABLE_BALANCE: 20000,
|
|
37
|
+
RELOAD_WALLET: 20000,
|
|
38
|
+
// Transactions — need more headroom
|
|
39
|
+
SEND_BITCOIN: 50000,
|
|
40
|
+
SEND: 50000,
|
|
41
|
+
SETTLE: 50000,
|
|
42
|
+
ISSUE: 50000,
|
|
43
|
+
REISSUE: 50000,
|
|
44
|
+
BURN: 50000,
|
|
45
|
+
DELEGATE: 50000,
|
|
46
|
+
RECOVER_VTXOS: 50000,
|
|
47
|
+
RENEW_VTXOS: 50000,
|
|
48
|
+
SWEEP_EXPIRED_BOARDING_UTXOS: 50000,
|
|
49
|
+
// Misc writes
|
|
50
|
+
INIT_WALLET: 30000,
|
|
51
|
+
CLEAR: 10000,
|
|
52
|
+
SIGN_TRANSACTION: 30000,
|
|
53
|
+
CREATE_CONTRACT: 30000,
|
|
54
|
+
UPDATE_CONTRACT: 30000,
|
|
55
|
+
DELETE_CONTRACT: 10000,
|
|
56
|
+
REFRESH_VTXOS: 30000,
|
|
57
|
+
};
|
|
17
58
|
const DEDUPABLE_REQUEST_TYPES = new Set([
|
|
18
59
|
"GET_ADDRESS",
|
|
19
60
|
"GET_BALANCE",
|
|
@@ -129,6 +170,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
129
170
|
this.messageTag = messageTag;
|
|
130
171
|
this.initConfig = null;
|
|
131
172
|
this.initWalletPayload = null;
|
|
173
|
+
this.messageTimeouts = exports.DEFAULT_MESSAGE_TIMEOUTS;
|
|
132
174
|
this.reinitPromise = null;
|
|
133
175
|
this.pingPromise = null;
|
|
134
176
|
this.inflightRequests = new Map();
|
|
@@ -137,6 +179,9 @@ class ServiceWorkerReadonlyWallet {
|
|
|
137
179
|
this.contractRepository = contractRepository;
|
|
138
180
|
this._readonlyAssetManager = new ServiceWorkerReadonlyAssetManager((msg) => this.sendMessage(msg), messageTag);
|
|
139
181
|
}
|
|
182
|
+
getTimeoutForRequest(request) {
|
|
183
|
+
return this.messageTimeouts[request.type] ?? 30000;
|
|
184
|
+
}
|
|
140
185
|
static async create(options) {
|
|
141
186
|
const walletRepository = options.storage?.walletRepository ??
|
|
142
187
|
new repositories_1.IndexedDBWalletRepository();
|
|
@@ -188,6 +233,12 @@ class ServiceWorkerReadonlyWallet {
|
|
|
188
233
|
};
|
|
189
234
|
wallet.initWalletPayload = initConfig;
|
|
190
235
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
236
|
+
if (options.messageTimeouts) {
|
|
237
|
+
wallet.messageTimeouts = {
|
|
238
|
+
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
239
|
+
...options.messageTimeouts,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
191
242
|
return wallet;
|
|
192
243
|
}
|
|
193
244
|
/**
|
|
@@ -223,7 +274,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
223
274
|
serviceWorker,
|
|
224
275
|
});
|
|
225
276
|
}
|
|
226
|
-
sendMessageDirect(request) {
|
|
277
|
+
sendMessageDirect(request, timeoutMs) {
|
|
227
278
|
return new Promise((resolve, reject) => {
|
|
228
279
|
const cleanup = () => {
|
|
229
280
|
clearTimeout(timeoutId);
|
|
@@ -232,7 +283,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
232
283
|
const timeoutId = setTimeout(() => {
|
|
233
284
|
cleanup();
|
|
234
285
|
reject(new errors_1.ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
|
|
235
|
-
},
|
|
286
|
+
}, timeoutMs);
|
|
236
287
|
const messageHandler = (event) => {
|
|
237
288
|
const response = event.data;
|
|
238
289
|
if (request.id !== response.id) {
|
|
@@ -255,14 +306,14 @@ class ServiceWorkerReadonlyWallet {
|
|
|
255
306
|
// first response for which isComplete returns true. The timeout resets
|
|
256
307
|
// on every intermediate event so long-running but progressing operations
|
|
257
308
|
// don't time out prematurely.
|
|
258
|
-
sendMessageStreaming(request, onEvent, isComplete) {
|
|
309
|
+
sendMessageStreaming(request, onEvent, isComplete, timeoutMs) {
|
|
259
310
|
return new Promise((resolve, reject) => {
|
|
260
311
|
const resetTimeout = () => {
|
|
261
312
|
clearTimeout(timeoutId);
|
|
262
313
|
timeoutId = setTimeout(() => {
|
|
263
314
|
cleanup();
|
|
264
315
|
reject(new errors_1.ServiceWorkerTimeoutError(`Service worker message timed out (${request.type})`));
|
|
265
|
-
},
|
|
316
|
+
}, timeoutMs);
|
|
266
317
|
};
|
|
267
318
|
const cleanup = () => {
|
|
268
319
|
clearTimeout(timeoutId);
|
|
@@ -348,10 +399,11 @@ class ServiceWorkerReadonlyWallet {
|
|
|
348
399
|
await this.reinitialize();
|
|
349
400
|
}
|
|
350
401
|
}
|
|
402
|
+
const timeoutMs = this.getTimeoutForRequest(request);
|
|
351
403
|
const maxRetries = 2;
|
|
352
404
|
for (let attempt = 0;; attempt++) {
|
|
353
405
|
try {
|
|
354
|
-
return await this.sendMessageDirect(request);
|
|
406
|
+
return await this.sendMessageDirect(request, timeoutMs);
|
|
355
407
|
}
|
|
356
408
|
catch (error) {
|
|
357
409
|
if (!isMessageBusNotInitializedError(error) ||
|
|
@@ -373,10 +425,11 @@ class ServiceWorkerReadonlyWallet {
|
|
|
373
425
|
await this.reinitialize();
|
|
374
426
|
}
|
|
375
427
|
}
|
|
428
|
+
const timeoutMs = this.getTimeoutForRequest(request);
|
|
376
429
|
const maxRetries = 2;
|
|
377
430
|
for (let attempt = 0;; attempt++) {
|
|
378
431
|
try {
|
|
379
|
-
return await this.sendMessageStreaming(request, onEvent, isComplete);
|
|
432
|
+
return await this.sendMessageStreaming(request, onEvent, isComplete, timeoutMs);
|
|
380
433
|
}
|
|
381
434
|
catch (error) {
|
|
382
435
|
if (!isMessageBusNotInitializedError(error) ||
|
|
@@ -401,7 +454,7 @@ class ServiceWorkerReadonlyWallet {
|
|
|
401
454
|
id: (0, utils_2.getRandomId)(),
|
|
402
455
|
payload: this.initWalletPayload,
|
|
403
456
|
};
|
|
404
|
-
await this.sendMessageDirect(initMessage);
|
|
457
|
+
await this.sendMessageDirect(initMessage, this.getTimeoutForRequest(initMessage));
|
|
405
458
|
})().finally(() => {
|
|
406
459
|
this.reinitPromise = null;
|
|
407
460
|
});
|
|
@@ -793,6 +846,12 @@ class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet {
|
|
|
793
846
|
};
|
|
794
847
|
wallet.initWalletPayload = initConfig;
|
|
795
848
|
wallet.messageBusTimeoutMs = options.messageBusTimeoutMs;
|
|
849
|
+
if (options.messageTimeouts) {
|
|
850
|
+
wallet.messageTimeouts = {
|
|
851
|
+
...exports.DEFAULT_MESSAGE_TIMEOUTS,
|
|
852
|
+
...options.messageTimeouts,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
796
855
|
return wallet;
|
|
797
856
|
}
|
|
798
857
|
/**
|
|
@@ -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
|
}
|