@arkade-os/sdk 0.3.5 → 0.3.7

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 CHANGED
@@ -120,8 +120,8 @@ import { VtxoManager } from '@arkade-os/sdk'
120
120
 
121
121
  // Create manager with optional renewal configuration
122
122
  const manager = new VtxoManager(wallet, {
123
- enabled: true, // Enable expiration monitoring
124
- thresholdPercentage: 10 // Alert when 10% of lifetime remains (default)
123
+ enabled: true, // Enable expiration monitoring
124
+ thresholdMs: 24 * 60 * 60 * 1000 // Alert when 24h hours % of lifetime remains (default)
125
125
  })
126
126
  ```
127
127
 
@@ -137,8 +137,8 @@ console.log('Renewed:', txid)
137
137
 
138
138
  // Check which VTXOs are expiring soon
139
139
  const expiringVtxos = await manager.getExpiringVtxos()
140
- // Override threshold percentage (e.g., renew when 5% of time remains)
141
- const urgentlyExpiring = await manager.getExpiringVtxos(5)
140
+ // Override thresholdMs (e.g., renew when 5 seconds of time remains)
141
+ const urgentlyExpiring = await manager.getExpiringVtxos(5_000)
142
142
  ```
143
143
 
144
144
 
package/dist/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Unroll = exports.P2A = exports.TxTree = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.networks = exports.ArkNote = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.Response = exports.Request = exports.ServiceWorkerWallet = exports.Worker = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.SingleKey = exports.Wallet = void 0;
4
- exports.maybeArkError = exports.ArkError = exports.Transaction = void 0;
3
+ exports.P2A = exports.TxTree = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.networks = exports.ArkNote = exports.isVtxoExpiringSoon = exports.combineTapscriptSigs = exports.hasBoardingTxExpired = exports.waitForIncomingFunds = exports.verifyTapscriptSignatures = exports.buildOffchainTx = exports.ConditionWitness = exports.VtxoTaprootTree = exports.VtxoTreeExpiry = exports.CosignerPublicKey = exports.getArkPsbtFields = exports.setArkPsbtField = exports.ArkPsbtFieldKeyType = exports.ArkPsbtFieldKey = exports.TapTreeCoder = exports.CLTVMultisigTapscript = exports.ConditionMultisigTapscript = exports.ConditionCSVMultisigTapscript = exports.CSVMultisigTapscript = exports.MultisigTapscript = exports.decodeTapscript = exports.Response = exports.Request = exports.ServiceWorkerWallet = exports.Worker = exports.setupServiceWorker = exports.SettlementEventType = exports.ChainTxType = exports.IndexerTxType = exports.TxType = exports.VHTLC = exports.VtxoScript = exports.DefaultVtxo = exports.ArkAddress = exports.RestIndexerProvider = exports.RestArkProvider = exports.EsploraProvider = exports.ESPLORA_URL = exports.VtxoManager = exports.Ramps = exports.OnchainWallet = exports.SingleKey = exports.Wallet = void 0;
4
+ exports.maybeArkError = exports.ArkError = exports.Transaction = exports.Unroll = void 0;
5
5
  const transaction_1 = require("./utils/transaction");
6
6
  Object.defineProperty(exports, "Transaction", { enumerable: true, get: function () { return transaction_1.Transaction; } });
7
7
  const singleKey_1 = require("./identity/singleKey");
@@ -25,6 +25,7 @@ Object.defineProperty(exports, "TxTree", { enumerable: true, get: function () {
25
25
  const ramps_1 = require("./wallet/ramps");
26
26
  Object.defineProperty(exports, "Ramps", { enumerable: true, get: function () { return ramps_1.Ramps; } });
27
27
  const vtxo_manager_1 = require("./wallet/vtxo-manager");
28
+ Object.defineProperty(exports, "isVtxoExpiringSoon", { enumerable: true, get: function () { return vtxo_manager_1.isVtxoExpiringSoon; } });
28
29
  Object.defineProperty(exports, "VtxoManager", { enumerable: true, get: function () { return vtxo_manager_1.VtxoManager; } });
29
30
  const wallet_3 = require("./wallet/serviceWorker/wallet");
30
31
  Object.defineProperty(exports, "ServiceWorkerWallet", { enumerable: true, get: function () { return wallet_3.ServiceWorkerWallet; } });
@@ -14,10 +14,6 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
14
14
  // All vtxos are received unless:
15
15
  // - they resulted from a settlement (either boarding or refresh)
16
16
  // - they are the change of a spend tx
17
- // - they were spent in a payment (have arkTxId set)
18
- // - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
19
- // First, collect all arkTxIds from spent vtxos to identify payment transactions
20
- const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
21
17
  let vtxosLeftToCheck = [...spent];
22
18
  for (const vtxo of [...spendable, ...spent]) {
23
19
  if (vtxo.virtualStatus.state !== "preconfirmed" &&
@@ -25,16 +21,6 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
25
21
  vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
26
22
  continue;
27
23
  }
28
- // Skip vtxos that were spent in a payment transaction
29
- // These will be handled in the sent transaction section below
30
- if (vtxo.arkTxId) {
31
- continue;
32
- }
33
- // Skip vtxos that resulted from a payment transaction
34
- // (their txid matches an arkTxId from a spent vtxo)
35
- if (paymentArkTxIds.has(vtxo.txid)) {
36
- continue;
37
- }
38
24
  const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
39
25
  vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
40
26
  const settleAmount = reduceVtxosAmount(settleVtxos);
@@ -48,7 +34,7 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
48
34
  continue; // settlement or change, ignore
49
35
  }
50
36
  const txKey = {
51
- commitmentTxid: vtxo.spentBy || "",
37
+ commitmentTxid: "",
52
38
  boardingTxid: "",
53
39
  arkTxid: "",
54
40
  };
@@ -59,6 +45,10 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
59
45
  settled = true;
60
46
  }
61
47
  }
48
+ else {
49
+ txKey.commitmentTxid =
50
+ vtxo.virtualStatus.commitmentTxIds?.[0] || "";
51
+ }
62
52
  txs.push({
63
53
  key: txKey,
64
54
  amount: vtxo.value - settleAmount - spentAmount,
@@ -70,17 +60,21 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
70
60
  // vtxos by settled by or ark txid
71
61
  const vtxosByTxid = new Map();
72
62
  for (const v of spent) {
73
- // Prefer arkTxId over settledBy to avoid duplicates
74
- // A vtxo should only be grouped once
75
- const groupKey = v.arkTxId || v.settledBy;
76
- if (!groupKey) {
63
+ if (v.settledBy) {
64
+ if (!vtxosByTxid.has(v.settledBy)) {
65
+ vtxosByTxid.set(v.settledBy, []);
66
+ }
67
+ const currentVtxos = vtxosByTxid.get(v.settledBy);
68
+ vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
69
+ }
70
+ if (!v.arkTxId) {
77
71
  continue;
78
72
  }
79
- if (!vtxosByTxid.has(groupKey)) {
80
- vtxosByTxid.set(groupKey, []);
73
+ if (!vtxosByTxid.has(v.arkTxId)) {
74
+ vtxosByTxid.set(v.arkTxId, []);
81
75
  }
82
- const currentVtxos = vtxosByTxid.get(groupKey);
83
- vtxosByTxid.set(groupKey, [...currentVtxos, v]);
76
+ const currentVtxos = vtxosByTxid.get(v.arkTxId);
77
+ vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
84
78
  }
85
79
  for (const [sb, vtxos] of vtxosByTxid) {
86
80
  const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
@@ -91,18 +85,16 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
91
85
  }
92
86
  const vtxo = getVtxo(resultedVtxos, vtxos);
93
87
  const txKey = {
94
- commitmentTxid: vtxo.virtualStatus.commitmentTxIds?.[0] || "",
88
+ commitmentTxid: "",
95
89
  boardingTxid: "",
96
90
  arkTxid: "",
97
91
  };
98
- // Use the grouping key (sb) as arkTxid if it looks like an arkTxId
99
- // (i.e., if the spent vtxos had arkTxId set, use that instead of result vtxo's txid)
100
- const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
101
- if (isArkTxId) {
102
- txKey.arkTxid = sb;
92
+ if (vtxo.virtualStatus.state === "preconfirmed") {
93
+ txKey.arkTxid = resultedAmount === 0 ? vtxo.arkTxId : vtxo.txid;
103
94
  }
104
- else if (vtxo.virtualStatus.state === "preconfirmed") {
105
- txKey.arkTxid = vtxo.txid;
95
+ else {
96
+ txKey.commitmentTxid =
97
+ vtxo.virtualStatus.commitmentTxIds?.[0] || "";
106
98
  }
107
99
  txs.push({
108
100
  key: txKey,
@@ -1,14 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = void 0;
3
+ exports.VtxoManager = exports.DEFAULT_RENEWAL_CONFIG = exports.DEFAULT_THRESHOLD_MS = void 0;
4
4
  exports.isVtxoExpiringSoon = isVtxoExpiringSoon;
5
5
  exports.getExpiringAndRecoverableVtxos = getExpiringAndRecoverableVtxos;
6
6
  const _1 = require(".");
7
+ exports.DEFAULT_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
7
8
  /**
8
9
  * Default renewal configuration values
9
10
  */
10
11
  exports.DEFAULT_RENEWAL_CONFIG = {
11
- thresholdPercentage: 10,
12
+ thresholdMs: exports.DEFAULT_THRESHOLD_MS, // 3 days
12
13
  };
13
14
  function getDustAmount(wallet) {
14
15
  return "dustAmount" in wallet ? wallet.dustAmount : 330n;
@@ -82,22 +83,16 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
82
83
  * @param thresholdMs - Threshold in milliseconds from now
83
84
  * @returns true if VTXO expires within threshold, false otherwise
84
85
  */
85
- function isVtxoExpiringSoon(vtxo, percentage) {
86
+ function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
87
+ ) {
88
+ const realThresholdMs = thresholdMs <= 100 ? exports.DEFAULT_THRESHOLD_MS : thresholdMs;
86
89
  const { batchExpiry } = vtxo.virtualStatus;
87
- if (!batchExpiry) {
90
+ if (!batchExpiry)
88
91
  return false; // it doesn't expire
89
- }
90
92
  const now = Date.now();
91
- if (batchExpiry <= now) {
93
+ if (batchExpiry <= now)
92
94
  return false; // already expired
93
- }
94
- // It shouldn't happen, but let's be safe
95
- if (!vtxo.createdAt) {
96
- return false;
97
- }
98
- const duration = batchExpiry - vtxo.createdAt.getTime();
99
- const softExpiry = batchExpiry - (duration * percentage) / 100;
100
- return softExpiry > 0 && softExpiry <= now;
95
+ return batchExpiry - now <= realThresholdMs;
101
96
  }
102
97
  /**
103
98
  * Filter VTXOs that are expiring soon or are recoverable/subdust
@@ -107,8 +102,8 @@ function isVtxoExpiringSoon(vtxo, percentage) {
107
102
  * @param dustAmount - Dust threshold amount in satoshis
108
103
  * @returns Array of VTXOs expiring within threshold
109
104
  */
110
- function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
111
- return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, percentage) ||
105
+ function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
106
+ return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) ||
112
107
  (0, _1.isRecoverable)(vtxo) ||
113
108
  (0, _1.isSubdust)(vtxo, dustAmount));
114
109
  }
@@ -131,7 +126,7 @@ function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
131
126
  * // Initialize with renewal config
132
127
  * const manager = new VtxoManager(wallet, {
133
128
  * enabled: true,
134
- * thresholdPercentage: 10
129
+ * thresholdMs: 86400000
135
130
  * });
136
131
  *
137
132
  * // Check recoverable balance
@@ -251,24 +246,24 @@ class VtxoManager {
251
246
  /**
252
247
  * Get VTXOs that are expiring soon based on renewal configuration
253
248
  *
254
- * @param thresholdPercentage - Optional override for threshold percentage (0-100)
249
+ * @param thresholdMs - Optional override for threshold in milliseconds
255
250
  * @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
256
251
  *
257
252
  * @example
258
253
  * ```typescript
259
- * const manager = new VtxoManager(wallet, { enabled: true, thresholdPercentage: 10 });
254
+ * const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
260
255
  * const expiringVtxos = await manager.getExpiringVtxos();
261
256
  * if (expiringVtxos.length > 0) {
262
257
  * console.log(`${expiringVtxos.length} VTXOs expiring soon`);
263
258
  * }
264
259
  * ```
265
260
  */
266
- async getExpiringVtxos(thresholdPercentage) {
261
+ async getExpiringVtxos(thresholdMs) {
267
262
  const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
268
- const percentage = thresholdPercentage ??
269
- this.renewalConfig?.thresholdPercentage ??
270
- exports.DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
271
- return getExpiringAndRecoverableVtxos(vtxos, percentage, getDustAmount(this.wallet));
263
+ const threshold = thresholdMs ??
264
+ this.renewalConfig?.thresholdMs ??
265
+ exports.DEFAULT_RENEWAL_CONFIG.thresholdMs;
266
+ return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
272
267
  }
273
268
  /**
274
269
  * Renew expiring VTXOs by settling them back to the wallet's address
@@ -62,6 +62,7 @@ const inMemory_1 = require("../storage/inMemory");
62
62
  const walletRepository_1 = require("../repositories/walletRepository");
63
63
  const contractRepository_1 = require("../repositories/contractRepository");
64
64
  const utils_1 = require("./utils");
65
+ const errors_1 = require("../providers/errors");
65
66
  /**
66
67
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
67
68
  * The wallet does not store any data locally and relies on Ark and onchain
@@ -589,7 +590,7 @@ class Wallet {
589
590
  this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
590
591
  this.makeDeleteIntentSignature(params.inputs),
591
592
  ]);
592
- const intentId = await this.arkProvider.registerIntent(intent);
593
+ const intentId = await this.safeRegisterIntent(intent);
593
594
  const abortController = new AbortController();
594
595
  // listen to settlement events
595
596
  try {
@@ -741,7 +742,9 @@ class Wallet {
741
742
  // delete the intent to not be stuck in the queue
742
743
  await this.arkProvider.deleteIntent(deleteIntent);
743
744
  }
744
- catch { }
745
+ catch (error) {
746
+ console.error("failed to delete intent: ", error);
747
+ }
745
748
  throw error;
746
749
  }
747
750
  throw new Error("Settlement failed");
@@ -956,6 +959,27 @@ class Wallet {
956
959
  : undefined);
957
960
  }
958
961
  }
962
+ async safeRegisterIntent(intent) {
963
+ try {
964
+ return this.arkProvider.registerIntent(intent);
965
+ }
966
+ catch (error) {
967
+ // catch the "already registered by another intent" error
968
+ if (error instanceof errors_1.ArkError &&
969
+ error.code === 0 &&
970
+ error.message.includes("duplicated input")) {
971
+ // delete all intents spending one of the wallet coins
972
+ const allSpendableCoins = await this.getVtxos({
973
+ withRecoverable: true,
974
+ });
975
+ const deleteIntent = await this.makeDeleteIntentSignature(allSpendableCoins);
976
+ await this.arkProvider.deleteIntent(deleteIntent);
977
+ // try again
978
+ return this.arkProvider.registerIntent(intent);
979
+ }
980
+ throw error;
981
+ }
982
+ }
959
983
  async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
960
984
  const inputs = this.prepareIntentProofInputs(coins);
961
985
  const message = {
package/dist/esm/index.js CHANGED
@@ -8,7 +8,7 @@ import { TxType, } from './wallet/index.js';
8
8
  import { Wallet, waitForIncomingFunds } from './wallet/wallet.js';
9
9
  import { TxTree } from './tree/txTree.js';
10
10
  import { Ramps } from './wallet/ramps.js';
11
- import { VtxoManager } from './wallet/vtxo-manager.js';
11
+ import { isVtxoExpiringSoon, VtxoManager } from './wallet/vtxo-manager.js';
12
12
  import { ServiceWorkerWallet } from './wallet/serviceWorker/wallet.js';
13
13
  import { OnchainWallet } from './wallet/onchain.js';
14
14
  import { setupServiceWorker } from './wallet/serviceWorker/utils.js';
@@ -45,7 +45,7 @@ decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTa
45
45
  // Ark PSBT fields
46
46
  ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness,
47
47
  // Utils
48
- buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs,
48
+ buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon,
49
49
  // Arknote
50
50
  ArkNote,
51
51
  // Network
@@ -11,10 +11,6 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
11
11
  // All vtxos are received unless:
12
12
  // - they resulted from a settlement (either boarding or refresh)
13
13
  // - they are the change of a spend tx
14
- // - they were spent in a payment (have arkTxId set)
15
- // - they resulted from a payment (their txid matches an arkTxId of a spent vtxo)
16
- // First, collect all arkTxIds from spent vtxos to identify payment transactions
17
- const paymentArkTxIds = new Set(spent.filter((v) => v.arkTxId).map((v) => v.arkTxId));
18
14
  let vtxosLeftToCheck = [...spent];
19
15
  for (const vtxo of [...spendable, ...spent]) {
20
16
  if (vtxo.virtualStatus.state !== "preconfirmed" &&
@@ -22,16 +18,6 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
22
18
  vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
23
19
  continue;
24
20
  }
25
- // Skip vtxos that were spent in a payment transaction
26
- // These will be handled in the sent transaction section below
27
- if (vtxo.arkTxId) {
28
- continue;
29
- }
30
- // Skip vtxos that resulted from a payment transaction
31
- // (their txid matches an arkTxId from a spent vtxo)
32
- if (paymentArkTxIds.has(vtxo.txid)) {
33
- continue;
34
- }
35
21
  const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
36
22
  vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
37
23
  const settleAmount = reduceVtxosAmount(settleVtxos);
@@ -45,7 +31,7 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
45
31
  continue; // settlement or change, ignore
46
32
  }
47
33
  const txKey = {
48
- commitmentTxid: vtxo.spentBy || "",
34
+ commitmentTxid: "",
49
35
  boardingTxid: "",
50
36
  arkTxid: "",
51
37
  };
@@ -56,6 +42,10 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
56
42
  settled = true;
57
43
  }
58
44
  }
45
+ else {
46
+ txKey.commitmentTxid =
47
+ vtxo.virtualStatus.commitmentTxIds?.[0] || "";
48
+ }
59
49
  txs.push({
60
50
  key: txKey,
61
51
  amount: vtxo.value - settleAmount - spentAmount,
@@ -67,17 +57,21 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
67
57
  // vtxos by settled by or ark txid
68
58
  const vtxosByTxid = new Map();
69
59
  for (const v of spent) {
70
- // Prefer arkTxId over settledBy to avoid duplicates
71
- // A vtxo should only be grouped once
72
- const groupKey = v.arkTxId || v.settledBy;
73
- if (!groupKey) {
60
+ if (v.settledBy) {
61
+ if (!vtxosByTxid.has(v.settledBy)) {
62
+ vtxosByTxid.set(v.settledBy, []);
63
+ }
64
+ const currentVtxos = vtxosByTxid.get(v.settledBy);
65
+ vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
66
+ }
67
+ if (!v.arkTxId) {
74
68
  continue;
75
69
  }
76
- if (!vtxosByTxid.has(groupKey)) {
77
- vtxosByTxid.set(groupKey, []);
70
+ if (!vtxosByTxid.has(v.arkTxId)) {
71
+ vtxosByTxid.set(v.arkTxId, []);
78
72
  }
79
- const currentVtxos = vtxosByTxid.get(groupKey);
80
- vtxosByTxid.set(groupKey, [...currentVtxos, v]);
73
+ const currentVtxos = vtxosByTxid.get(v.arkTxId);
74
+ vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
81
75
  }
82
76
  for (const [sb, vtxos] of vtxosByTxid) {
83
77
  const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
@@ -88,18 +82,16 @@ export function vtxosToTxs(spendable, spent, boardingBatchTxids) {
88
82
  }
89
83
  const vtxo = getVtxo(resultedVtxos, vtxos);
90
84
  const txKey = {
91
- commitmentTxid: vtxo.virtualStatus.commitmentTxIds?.[0] || "",
85
+ commitmentTxid: "",
92
86
  boardingTxid: "",
93
87
  arkTxid: "",
94
88
  };
95
- // Use the grouping key (sb) as arkTxid if it looks like an arkTxId
96
- // (i.e., if the spent vtxos had arkTxId set, use that instead of result vtxo's txid)
97
- const isArkTxId = vtxos.some((v) => v.arkTxId === sb);
98
- if (isArkTxId) {
99
- txKey.arkTxid = sb;
89
+ if (vtxo.virtualStatus.state === "preconfirmed") {
90
+ txKey.arkTxid = resultedAmount === 0 ? vtxo.arkTxId : vtxo.txid;
100
91
  }
101
- else if (vtxo.virtualStatus.state === "preconfirmed") {
102
- txKey.arkTxid = vtxo.txid;
92
+ else {
93
+ txKey.commitmentTxid =
94
+ vtxo.virtualStatus.commitmentTxIds?.[0] || "";
103
95
  }
104
96
  txs.push({
105
97
  key: txKey,
@@ -1,9 +1,10 @@
1
1
  import { isRecoverable, isSubdust } from './index.js';
2
+ export const DEFAULT_THRESHOLD_MS = 3 * 24 * 60 * 60 * 1000; // 3 days
2
3
  /**
3
4
  * Default renewal configuration values
4
5
  */
5
6
  export const DEFAULT_RENEWAL_CONFIG = {
6
- thresholdPercentage: 10,
7
+ thresholdMs: DEFAULT_THRESHOLD_MS, // 3 days
7
8
  };
8
9
  function getDustAmount(wallet) {
9
10
  return "dustAmount" in wallet ? wallet.dustAmount : 330n;
@@ -77,22 +78,16 @@ function getRecoverableWithSubdust(vtxos, dustAmount) {
77
78
  * @param thresholdMs - Threshold in milliseconds from now
78
79
  * @returns true if VTXO expires within threshold, false otherwise
79
80
  */
80
- export function isVtxoExpiringSoon(vtxo, percentage) {
81
+ export function isVtxoExpiringSoon(vtxo, thresholdMs // in milliseconds
82
+ ) {
83
+ const realThresholdMs = thresholdMs <= 100 ? DEFAULT_THRESHOLD_MS : thresholdMs;
81
84
  const { batchExpiry } = vtxo.virtualStatus;
82
- if (!batchExpiry) {
85
+ if (!batchExpiry)
83
86
  return false; // it doesn't expire
84
- }
85
87
  const now = Date.now();
86
- if (batchExpiry <= now) {
88
+ if (batchExpiry <= now)
87
89
  return false; // already expired
88
- }
89
- // It shouldn't happen, but let's be safe
90
- if (!vtxo.createdAt) {
91
- return false;
92
- }
93
- const duration = batchExpiry - vtxo.createdAt.getTime();
94
- const softExpiry = batchExpiry - (duration * percentage) / 100;
95
- return softExpiry > 0 && softExpiry <= now;
90
+ return batchExpiry - now <= realThresholdMs;
96
91
  }
97
92
  /**
98
93
  * Filter VTXOs that are expiring soon or are recoverable/subdust
@@ -102,8 +97,8 @@ export function isVtxoExpiringSoon(vtxo, percentage) {
102
97
  * @param dustAmount - Dust threshold amount in satoshis
103
98
  * @returns Array of VTXOs expiring within threshold
104
99
  */
105
- export function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
106
- return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, percentage) ||
100
+ export function getExpiringAndRecoverableVtxos(vtxos, thresholdMs, dustAmount) {
101
+ return vtxos.filter((vtxo) => isVtxoExpiringSoon(vtxo, thresholdMs) ||
107
102
  isRecoverable(vtxo) ||
108
103
  isSubdust(vtxo, dustAmount));
109
104
  }
@@ -126,7 +121,7 @@ export function getExpiringAndRecoverableVtxos(vtxos, percentage, dustAmount) {
126
121
  * // Initialize with renewal config
127
122
  * const manager = new VtxoManager(wallet, {
128
123
  * enabled: true,
129
- * thresholdPercentage: 10
124
+ * thresholdMs: 86400000
130
125
  * });
131
126
  *
132
127
  * // Check recoverable balance
@@ -246,24 +241,24 @@ export class VtxoManager {
246
241
  /**
247
242
  * Get VTXOs that are expiring soon based on renewal configuration
248
243
  *
249
- * @param thresholdPercentage - Optional override for threshold percentage (0-100)
244
+ * @param thresholdMs - Optional override for threshold in milliseconds
250
245
  * @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
251
246
  *
252
247
  * @example
253
248
  * ```typescript
254
- * const manager = new VtxoManager(wallet, { enabled: true, thresholdPercentage: 10 });
249
+ * const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
255
250
  * const expiringVtxos = await manager.getExpiringVtxos();
256
251
  * if (expiringVtxos.length > 0) {
257
252
  * console.log(`${expiringVtxos.length} VTXOs expiring soon`);
258
253
  * }
259
254
  * ```
260
255
  */
261
- async getExpiringVtxos(thresholdPercentage) {
256
+ async getExpiringVtxos(thresholdMs) {
262
257
  const vtxos = await this.wallet.getVtxos({ withRecoverable: true });
263
- const percentage = thresholdPercentage ??
264
- this.renewalConfig?.thresholdPercentage ??
265
- DEFAULT_RENEWAL_CONFIG.thresholdPercentage;
266
- return getExpiringAndRecoverableVtxos(vtxos, percentage, getDustAmount(this.wallet));
258
+ const threshold = thresholdMs ??
259
+ this.renewalConfig?.thresholdMs ??
260
+ DEFAULT_RENEWAL_CONFIG.thresholdMs;
261
+ return getExpiringAndRecoverableVtxos(vtxos, threshold, getDustAmount(this.wallet));
267
262
  }
268
263
  /**
269
264
  * Renew expiring VTXOs by settling them back to the wallet's address
@@ -25,6 +25,7 @@ import { InMemoryStorageAdapter } from '../storage/inMemory.js';
25
25
  import { WalletRepositoryImpl, } from '../repositories/walletRepository.js';
26
26
  import { ContractRepositoryImpl, } from '../repositories/contractRepository.js';
27
27
  import { extendCoin, extendVirtualCoin } from './utils.js';
28
+ import { ArkError } from '../providers/errors.js';
28
29
  /**
29
30
  * Main wallet implementation for Bitcoin transactions with Ark protocol support.
30
31
  * The wallet does not store any data locally and relies on Ark and onchain
@@ -552,7 +553,7 @@ export class Wallet {
552
553
  this.makeRegisterIntentSignature(params.inputs, outputs, onchainOutputIndexes, signingPublicKeys),
553
554
  this.makeDeleteIntentSignature(params.inputs),
554
555
  ]);
555
- const intentId = await this.arkProvider.registerIntent(intent);
556
+ const intentId = await this.safeRegisterIntent(intent);
556
557
  const abortController = new AbortController();
557
558
  // listen to settlement events
558
559
  try {
@@ -704,7 +705,9 @@ export class Wallet {
704
705
  // delete the intent to not be stuck in the queue
705
706
  await this.arkProvider.deleteIntent(deleteIntent);
706
707
  }
707
- catch { }
708
+ catch (error) {
709
+ console.error("failed to delete intent: ", error);
710
+ }
708
711
  throw error;
709
712
  }
710
713
  throw new Error("Settlement failed");
@@ -919,6 +922,27 @@ export class Wallet {
919
922
  : undefined);
920
923
  }
921
924
  }
925
+ async safeRegisterIntent(intent) {
926
+ try {
927
+ return this.arkProvider.registerIntent(intent);
928
+ }
929
+ catch (error) {
930
+ // catch the "already registered by another intent" error
931
+ if (error instanceof ArkError &&
932
+ error.code === 0 &&
933
+ error.message.includes("duplicated input")) {
934
+ // delete all intents spending one of the wallet coins
935
+ const allSpendableCoins = await this.getVtxos({
936
+ withRecoverable: true,
937
+ });
938
+ const deleteIntent = await this.makeDeleteIntentSignature(allSpendableCoins);
939
+ await this.arkProvider.deleteIntent(deleteIntent);
940
+ // try again
941
+ return this.arkProvider.registerIntent(intent);
942
+ }
943
+ throw error;
944
+ }
945
+ }
922
946
  async makeRegisterIntentSignature(coins, outputs, onchainOutputsIndexes, cosignerPubKeys) {
923
947
  const inputs = this.prepareIntentProofInputs(coins);
924
948
  const message = {
@@ -10,7 +10,7 @@ import { Wallet, waitForIncomingFunds, IncomingFunds } from "./wallet/wallet";
10
10
  import { TxTree, TxTreeNode } from "./tree/txTree";
11
11
  import { SignerSession, TreeNonces, TreePartialSigs } from "./tree/signingSession";
12
12
  import { Ramps } from "./wallet/ramps";
13
- import { VtxoManager } from "./wallet/vtxo-manager";
13
+ import { isVtxoExpiringSoon, VtxoManager } from "./wallet/vtxo-manager";
14
14
  import { ServiceWorkerWallet } from "./wallet/serviceWorker/wallet";
15
15
  import { OnchainWallet } from "./wallet/onchain";
16
16
  import { setupServiceWorker } from "./wallet/serviceWorker/utils";
@@ -33,5 +33,5 @@ import { Unroll } from "./wallet/unroll";
33
33
  import { WalletRepositoryImpl } from "./repositories/walletRepository";
34
34
  import { ContractRepositoryImpl } from "./repositories/contractRepository";
35
35
  import { ArkError, maybeArkError } from "./providers/errors";
36
- export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
36
+ export { Wallet, SingleKey, OnchainWallet, Ramps, VtxoManager, ESPLORA_URL, EsploraProvider, RestArkProvider, RestIndexerProvider, ArkAddress, DefaultVtxo, VtxoScript, VHTLC, TxType, IndexerTxType, ChainTxType, SettlementEventType, setupServiceWorker, Worker, ServiceWorkerWallet, Request, Response, decodeTapscript, MultisigTapscript, CSVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CLTVMultisigTapscript, TapTreeCoder, ArkPsbtFieldKey, ArkPsbtFieldKeyType, setArkPsbtField, getArkPsbtFields, CosignerPublicKey, VtxoTreeExpiry, VtxoTaprootTree, ConditionWitness, buildOffchainTx, verifyTapscriptSignatures, waitForIncomingFunds, hasBoardingTxExpired, combineTapscriptSigs, isVtxoExpiringSoon, ArkNote, networks, WalletRepositoryImpl, ContractRepositoryImpl, Intent, TxTree, P2A, Unroll, Transaction, ArkError, maybeArkError, };
37
37
  export type { Identity, IWallet, WalletConfig, ProviderClass, ArkTransaction, Coin, ExtendedCoin, ExtendedVirtualCoin, WalletBalance, SendBitcoinParams, Recipient, SettleParams, Status, VirtualStatus, Outpoint, VirtualCoin, TxKey, TapscriptType, ArkTxInput, OffchainTx, TapLeaves, IncomingFunds, IndexerProvider, PageResponse, Batch, ChainTx, CommitmentTx, TxHistoryRecord, Vtxo, VtxoChain, Tx, OnchainProvider, ArkProvider, SettlementEvent, FeeInfo, ArkInfo, SignedIntent, Output, TxNotification, ExplorerTransaction, BatchFinalizationEvent, BatchFinalizedEvent, BatchFailedEvent, TreeSigningStartedEvent, TreeNoncesEvent, BatchStartedEvent, TreeTxEvent, TreeSignatureEvent, ScheduledSession, PaginationOptions, SubscriptionResponse, SubscriptionHeartbeat, SubscriptionEvent, Network, NetworkName, ArkTapscript, RelativeTimelock, EncodedVtxoScript, TapLeafScript, SignerSession, TreeNonces, TreePartialSigs, GetVtxosFilter, Nonces, PartialSig, ArkPsbtFieldCoder, TxTreeNode, AnchorBumper, };
@@ -1,5 +1,6 @@
1
1
  import { ExtendedVirtualCoin, IWallet } from ".";
2
2
  import { SettlementEvent } from "../providers/ark";
3
+ export declare const DEFAULT_THRESHOLD_MS: number;
3
4
  /**
4
5
  * Configuration options for automatic VTXO renewal
5
6
  */
@@ -10,11 +11,11 @@ export interface RenewalConfig {
10
11
  */
11
12
  enabled?: boolean;
12
13
  /**
13
- * Percentage of expiry time to use as threshold (0-100)
14
- * E.g., 10 means renew when 10% of time until expiry remains
15
- * @default 10
14
+ * Threshold in milliseconds to use as threshold for renewal
15
+ * E.g., 86400000 means renew when 24 hours until expiry remains
16
+ * @default 86400000 (24 hours)
16
17
  */
17
- thresholdPercentage?: number;
18
+ thresholdMs?: number;
18
19
  }
19
20
  /**
20
21
  * Default renewal configuration values
@@ -27,7 +28,7 @@ export declare const DEFAULT_RENEWAL_CONFIG: Required<Omit<RenewalConfig, "enabl
27
28
  * @param thresholdMs - Threshold in milliseconds from now
28
29
  * @returns true if VTXO expires within threshold, false otherwise
29
30
  */
30
- export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, percentage: number): boolean;
31
+ export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, thresholdMs: number): boolean;
31
32
  /**
32
33
  * Filter VTXOs that are expiring soon or are recoverable/subdust
33
34
  *
@@ -36,7 +37,7 @@ export declare function isVtxoExpiringSoon(vtxo: ExtendedVirtualCoin, percentage
36
37
  * @param dustAmount - Dust threshold amount in satoshis
37
38
  * @returns Array of VTXOs expiring within threshold
38
39
  */
39
- export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoin[], percentage: number, dustAmount: bigint): ExtendedVirtualCoin[];
40
+ export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoin[], thresholdMs: number, dustAmount: bigint): ExtendedVirtualCoin[];
40
41
  /**
41
42
  * VtxoManager is a unified class for managing VTXO lifecycle operations including
42
43
  * recovery of swept/expired VTXOs and renewal to prevent expiration.
@@ -56,7 +57,7 @@ export declare function getExpiringAndRecoverableVtxos(vtxos: ExtendedVirtualCoi
56
57
  * // Initialize with renewal config
57
58
  * const manager = new VtxoManager(wallet, {
58
59
  * enabled: true,
59
- * thresholdPercentage: 10
60
+ * thresholdMs: 86400000
60
61
  * });
61
62
  *
62
63
  * // Check recoverable balance
@@ -137,19 +138,19 @@ export declare class VtxoManager {
137
138
  /**
138
139
  * Get VTXOs that are expiring soon based on renewal configuration
139
140
  *
140
- * @param thresholdPercentage - Optional override for threshold percentage (0-100)
141
+ * @param thresholdMs - Optional override for threshold in milliseconds
141
142
  * @returns Array of expiring VTXOs, empty array if renewal is disabled or no VTXOs expiring
142
143
  *
143
144
  * @example
144
145
  * ```typescript
145
- * const manager = new VtxoManager(wallet, { enabled: true, thresholdPercentage: 10 });
146
+ * const manager = new VtxoManager(wallet, { enabled: true, thresholdMs: 86400000 });
146
147
  * const expiringVtxos = await manager.getExpiringVtxos();
147
148
  * if (expiringVtxos.length > 0) {
148
149
  * console.log(`${expiringVtxos.length} VTXOs expiring soon`);
149
150
  * }
150
151
  * ```
151
152
  */
152
- getExpiringVtxos(thresholdPercentage?: number): Promise<ExtendedVirtualCoin[]>;
153
+ getExpiringVtxos(thresholdMs?: number): Promise<ExtendedVirtualCoin[]>;
153
154
  /**
154
155
  * Renew expiring VTXOs by settling them back to the wallet's address
155
156
  *
@@ -71,7 +71,7 @@ export declare class Wallet implements IWallet {
71
71
  readonly contractRepository: ContractRepository;
72
72
  readonly renewalConfig: Required<Omit<WalletConfig["renewalConfig"], "enabled">> & {
73
73
  enabled: boolean;
74
- thresholdPercentage: number;
74
+ thresholdMs: number;
75
75
  };
76
76
  private constructor();
77
77
  static create(config: WalletConfig): Promise<Wallet>;
@@ -94,6 +94,7 @@ export declare class Wallet implements IWallet {
94
94
  private handleSettlementSigningEvent;
95
95
  private handleSettlementTreeNoncesEvent;
96
96
  private handleSettlementFinalizationEvent;
97
+ safeRegisterIntent(intent: SignedIntent): Promise<string>;
97
98
  makeRegisterIntentSignature(coins: ExtendedCoin[], outputs: TransactionOutput[], onchainOutputsIndexes: number[], cosignerPubKeys: string[]): Promise<SignedIntent>;
98
99
  makeDeleteIntentSignature(coins: ExtendedCoin[]): Promise<SignedIntent>;
99
100
  private prepareIntentProofInputs;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkade-os/sdk",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Bitcoin wallet SDK with Taproot and Ark integration",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -63,8 +63,8 @@
63
63
  "@types/node": "24.3.1",
64
64
  "@vitest/coverage-v8": "3.2.4",
65
65
  "esbuild": "^0.25.9",
66
- "eventsource": "4.0.0",
67
66
  "expo": "~52.0.47",
67
+ "eventsource": "4.0.0",
68
68
  "glob": "11.0.3",
69
69
  "husky": "9.1.7",
70
70
  "prettier": "3.6.2",