@arkade-os/sdk 0.3.3 → 0.3.5

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
@@ -171,7 +171,13 @@ Collaborative exit or "offboarding" allows you to withdraw your virtual funds to
171
171
  ```typescript
172
172
  import { Ramps } from '@arkade-os/sdk'
173
173
 
174
- const exitTxid = await new Ramps(wallet).offboard(onchainAddress);
174
+ // Get fee information from the server
175
+ const info = await wallet.arkProvider.getInfo();
176
+
177
+ const exitTxid = await new Ramps(wallet).offboard(
178
+ onchainAddress,
179
+ info.fees
180
+ );
175
181
  ```
176
182
 
177
183
  ### Unilateral Exit
@@ -83,8 +83,8 @@ class SingleKey {
83
83
  }
84
84
  async signMessage(message, signatureType = "schnorr") {
85
85
  if (signatureType === "ecdsa")
86
- return (0, secp256k1_1.sign)(message, this.key, { prehash: false });
87
- return secp256k1_1.schnorr.sign(message, this.key);
86
+ return (0, secp256k1_1.signAsync)(message, this.key, { prehash: false });
87
+ return secp256k1_1.schnorr.signAsync(message, this.key);
88
88
  }
89
89
  }
90
90
  exports.SingleKey = SingleKey;
package/dist/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Transaction = exports.Unroll = exports.P2A = exports.TxTree = exports.Intent = exports.ContractRepositoryImpl = exports.WalletRepositoryImpl = exports.networks = exports.ArkNote = 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 = void 0;
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;
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");
@@ -55,6 +55,7 @@ const arkTransaction_1 = require("./utils/arkTransaction");
55
55
  Object.defineProperty(exports, "hasBoardingTxExpired", { enumerable: true, get: function () { return arkTransaction_1.hasBoardingTxExpired; } });
56
56
  Object.defineProperty(exports, "buildOffchainTx", { enumerable: true, get: function () { return arkTransaction_1.buildOffchainTx; } });
57
57
  Object.defineProperty(exports, "verifyTapscriptSignatures", { enumerable: true, get: function () { return arkTransaction_1.verifyTapscriptSignatures; } });
58
+ Object.defineProperty(exports, "combineTapscriptSigs", { enumerable: true, get: function () { return arkTransaction_1.combineTapscriptSigs; } });
58
59
  const unknownFields_1 = require("./utils/unknownFields");
59
60
  Object.defineProperty(exports, "VtxoTaprootTree", { enumerable: true, get: function () { return unknownFields_1.VtxoTaprootTree; } });
60
61
  Object.defineProperty(exports, "ConditionWitness", { enumerable: true, get: function () { return unknownFields_1.ConditionWitness; } });
@@ -46,7 +46,14 @@ class RestArkProvider {
46
46
  })) ?? [],
47
47
  digest: fromServer.digest ?? "",
48
48
  dust: BigInt(fromServer.dust ?? 0),
49
- fees: fromServer.fees,
49
+ fees: {
50
+ intentFee: {
51
+ ...fromServer.fees?.intentFee,
52
+ onchainInput: BigInt(fromServer.fees?.intentFee?.onchainInput ?? 0),
53
+ onchainOutput: BigInt(fromServer.fees?.intentFee?.onchainOutput ?? 0),
54
+ },
55
+ txFeeRate: fromServer?.fees?.txFeeRate ?? "",
56
+ },
50
57
  forfeitAddress: fromServer.forfeitAddress ?? "",
51
58
  forfeitPubkey: fromServer.forfeitPubkey ?? "",
52
59
  network: fromServer.network ?? "",
@@ -139,7 +146,7 @@ class RestArkProvider {
139
146
  "Content-Type": "application/json",
140
147
  },
141
148
  body: JSON.stringify({
142
- proof: {
149
+ intent: {
143
150
  proof: intent.proof,
144
151
  message: intent.message,
145
152
  },
@@ -1,22 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ContractRepositoryImpl = void 0;
4
+ const getContractStorageKey = (id, key) => `contract:${id}:${key}`;
5
+ const getCollectionStorageKey = (type) => `collection:${type}`;
4
6
  class ContractRepositoryImpl {
5
7
  constructor(storage) {
6
- this.cache = new Map();
7
8
  this.storage = storage;
8
9
  }
9
10
  async getContractData(contractId, key) {
10
- const storageKey = `contract:${contractId}:${key}`;
11
- const cached = this.cache.get(storageKey);
12
- if (cached !== undefined)
13
- return cached;
14
- const stored = await this.storage.getItem(storageKey);
11
+ const stored = await this.storage.getItem(getContractStorageKey(contractId, key));
15
12
  if (!stored)
16
13
  return null;
17
14
  try {
18
15
  const data = JSON.parse(stored);
19
- this.cache.set(storageKey, data);
20
16
  return data;
21
17
  }
22
18
  catch (error) {
@@ -25,49 +21,33 @@ class ContractRepositoryImpl {
25
21
  }
26
22
  }
27
23
  async setContractData(contractId, key, data) {
28
- const storageKey = `contract:${contractId}:${key}`;
29
24
  try {
30
- // First persist to storage, only update cache if successful
31
- await this.storage.setItem(storageKey, JSON.stringify(data));
32
- this.cache.set(storageKey, data);
25
+ await this.storage.setItem(getContractStorageKey(contractId, key), JSON.stringify(data));
33
26
  }
34
27
  catch (error) {
35
- // Storage operation failed, cache remains unchanged
36
28
  console.error(`Failed to persist contract data for ${contractId}:${key}:`, error);
37
29
  throw error; // Rethrow to notify caller of failure
38
30
  }
39
31
  }
40
32
  async deleteContractData(contractId, key) {
41
- const storageKey = `contract:${contractId}:${key}`;
42
33
  try {
43
- // First remove from persistent storage, only delete from cache if successful
44
- await this.storage.removeItem(storageKey);
45
- this.cache.delete(storageKey);
34
+ await this.storage.removeItem(getContractStorageKey(contractId, key));
46
35
  }
47
36
  catch (error) {
48
- // Storage operation failed, cache remains unchanged
49
37
  console.error(`Failed to remove contract data for ${contractId}:${key}:`, error);
50
38
  throw error; // Rethrow to notify caller of failure
51
39
  }
52
40
  }
53
41
  async getContractCollection(contractType) {
54
- const storageKey = `collection:${contractType}`;
55
- const cached = this.cache.get(storageKey);
56
- if (cached !== undefined)
57
- return cached;
58
- const stored = await this.storage.getItem(storageKey);
59
- if (!stored) {
60
- this.cache.set(storageKey, []);
42
+ const stored = await this.storage.getItem(getCollectionStorageKey(contractType));
43
+ if (!stored)
61
44
  return [];
62
- }
63
45
  try {
64
46
  const collection = JSON.parse(stored);
65
- this.cache.set(storageKey, collection);
66
47
  return collection;
67
48
  }
68
49
  catch (error) {
69
50
  console.error(`Failed to parse contract collection ${contractType}:`, error);
70
- this.cache.set(storageKey, []);
71
51
  return [];
72
52
  }
73
53
  }
@@ -94,14 +74,10 @@ class ContractRepositoryImpl {
94
74
  // Add new item
95
75
  newCollection = [...collection, item];
96
76
  }
97
- const storageKey = `collection:${contractType}`;
98
77
  try {
99
- // First persist to storage, only update cache if successful
100
- await this.storage.setItem(storageKey, JSON.stringify(newCollection));
101
- this.cache.set(storageKey, newCollection);
78
+ await this.storage.setItem(getCollectionStorageKey(contractType), JSON.stringify(newCollection));
102
79
  }
103
80
  catch (error) {
104
- // Storage operation failed, cache remains unchanged
105
81
  console.error(`Failed to persist contract collection ${contractType}:`, error);
106
82
  throw error; // Rethrow to notify caller of failure
107
83
  }
@@ -114,21 +90,16 @@ class ContractRepositoryImpl {
114
90
  const collection = await this.getContractCollection(contractType);
115
91
  // Build new collection without the specified item
116
92
  const filtered = collection.filter((item) => item[idField] !== id);
117
- const storageKey = `collection:${contractType}`;
118
93
  try {
119
- // First persist to storage, only update cache if successful
120
- await this.storage.setItem(storageKey, JSON.stringify(filtered));
121
- this.cache.set(storageKey, filtered);
94
+ await this.storage.setItem(getCollectionStorageKey(contractType), JSON.stringify(filtered));
122
95
  }
123
96
  catch (error) {
124
- // Storage operation failed, cache remains unchanged
125
97
  console.error(`Failed to persist contract collection removal for ${contractType}:`, error);
126
98
  throw error; // Rethrow to notify caller of failure
127
99
  }
128
100
  }
129
101
  async clearContractData() {
130
102
  await this.storage.clear();
131
- this.cache.clear();
132
103
  }
133
104
  }
134
105
  exports.ContractRepositoryImpl = ContractRepositoryImpl;
@@ -3,6 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WalletRepositoryImpl = void 0;
4
4
  const base_1 = require("@scure/base");
5
5
  const btc_signer_1 = require("@scure/btc-signer");
6
+ const getVtxosStorageKey = (address) => `vtxos:${address}`;
7
+ const getUtxosStorageKey = (address) => `utxos:${address}`;
8
+ const getTransactionsStorageKey = (address) => `tx:${address}`;
9
+ const walletStateStorageKey = "wallet:state";
6
10
  // Utility functions for (de)serializing complex structures
7
11
  const toHex = (b) => (b ? base_1.hex.encode(b) : undefined);
8
12
  const fromHex = (h) => h ? base_1.hex.decode(h) : undefined;
@@ -47,33 +51,17 @@ const deserializeUtxo = (o) => ({
47
51
  class WalletRepositoryImpl {
48
52
  constructor(storage) {
49
53
  this.storage = storage;
50
- this.cache = {
51
- vtxos: new Map(),
52
- utxos: new Map(),
53
- transactions: new Map(),
54
- walletState: null,
55
- initialized: new Set(),
56
- };
57
54
  }
58
55
  async getVtxos(address) {
59
- const cacheKey = `vtxos:${address}`;
60
- if (this.cache.vtxos.has(address)) {
61
- return this.cache.vtxos.get(address);
62
- }
63
- const stored = await this.storage.getItem(cacheKey);
64
- if (!stored) {
65
- this.cache.vtxos.set(address, []);
56
+ const stored = await this.storage.getItem(getVtxosStorageKey(address));
57
+ if (!stored)
66
58
  return [];
67
- }
68
59
  try {
69
60
  const parsed = JSON.parse(stored);
70
- const vtxos = parsed.map(deserializeVtxo);
71
- this.cache.vtxos.set(address, vtxos.slice());
72
- return vtxos.slice();
61
+ return parsed.map(deserializeVtxo);
73
62
  }
74
63
  catch (error) {
75
64
  console.error(`Failed to parse VTXOs for address ${address}:`, error);
76
- this.cache.vtxos.set(address, []);
77
65
  return [];
78
66
  }
79
67
  }
@@ -88,39 +76,27 @@ class WalletRepositoryImpl {
88
76
  storedVtxos.push(vtxo);
89
77
  }
90
78
  }
91
- this.cache.vtxos.set(address, storedVtxos.slice());
92
- await this.storage.setItem(`vtxos:${address}`, JSON.stringify(storedVtxos.map(serializeVtxo)));
79
+ await this.storage.setItem(getVtxosStorageKey(address), JSON.stringify(storedVtxos.map(serializeVtxo)));
93
80
  }
94
81
  async removeVtxo(address, vtxoId) {
95
82
  const vtxos = await this.getVtxos(address);
96
83
  const [txid, vout] = vtxoId.split(":");
97
84
  const filtered = vtxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
98
- this.cache.vtxos.set(address, filtered.slice());
99
- await this.storage.setItem(`vtxos:${address}`, JSON.stringify(filtered.map(serializeVtxo)));
85
+ await this.storage.setItem(getVtxosStorageKey(address), JSON.stringify(filtered.map(serializeVtxo)));
100
86
  }
101
87
  async clearVtxos(address) {
102
- this.cache.vtxos.set(address, []);
103
- await this.storage.removeItem(`vtxos:${address}`);
88
+ await this.storage.removeItem(getVtxosStorageKey(address));
104
89
  }
105
90
  async getUtxos(address) {
106
- const cacheKey = `utxos:${address}`;
107
- if (this.cache.utxos.has(address)) {
108
- return this.cache.utxos.get(address);
109
- }
110
- const stored = await this.storage.getItem(cacheKey);
111
- if (!stored) {
112
- this.cache.utxos.set(address, []);
91
+ const stored = await this.storage.getItem(getUtxosStorageKey(address));
92
+ if (!stored)
113
93
  return [];
114
- }
115
94
  try {
116
95
  const parsed = JSON.parse(stored);
117
- const utxos = parsed.map(deserializeUtxo);
118
- this.cache.utxos.set(address, utxos.slice());
119
- return utxos.slice();
96
+ return parsed.map(deserializeUtxo);
120
97
  }
121
98
  catch (error) {
122
99
  console.error(`Failed to parse UTXOs for address ${address}:`, error);
123
- this.cache.utxos.set(address, []);
124
100
  return [];
125
101
  }
126
102
  }
@@ -135,38 +111,27 @@ class WalletRepositoryImpl {
135
111
  storedUtxos.push(utxo);
136
112
  }
137
113
  });
138
- this.cache.utxos.set(address, storedUtxos.slice());
139
- await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
114
+ await this.storage.setItem(getUtxosStorageKey(address), JSON.stringify(storedUtxos.map(serializeUtxo)));
140
115
  }
141
116
  async removeUtxo(address, utxoId) {
142
117
  const utxos = await this.getUtxos(address);
143
118
  const [txid, vout] = utxoId.split(":");
144
119
  const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
145
- this.cache.utxos.set(address, filtered.slice());
146
- await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
120
+ await this.storage.setItem(getUtxosStorageKey(address), JSON.stringify(filtered.map(serializeUtxo)));
147
121
  }
148
122
  async clearUtxos(address) {
149
- this.cache.utxos.set(address, []);
150
- await this.storage.removeItem(`utxos:${address}`);
123
+ await this.storage.removeItem(getUtxosStorageKey(address));
151
124
  }
152
125
  async getTransactionHistory(address) {
153
- const cacheKey = `tx:${address}`;
154
- if (this.cache.transactions.has(address)) {
155
- return this.cache.transactions.get(address);
156
- }
157
- const stored = await this.storage.getItem(cacheKey);
158
- if (!stored) {
159
- this.cache.transactions.set(address, []);
126
+ const storageKey = getTransactionsStorageKey(address);
127
+ const stored = await this.storage.getItem(storageKey);
128
+ if (!stored)
160
129
  return [];
161
- }
162
130
  try {
163
- const transactions = JSON.parse(stored);
164
- this.cache.transactions.set(address, transactions);
165
- return transactions.slice();
131
+ return JSON.parse(stored);
166
132
  }
167
133
  catch (error) {
168
134
  console.error(`Failed to parse transactions for address ${address}:`, error);
169
- this.cache.transactions.set(address, []);
170
135
  return [];
171
136
  }
172
137
  }
@@ -181,40 +146,26 @@ class WalletRepositoryImpl {
181
146
  storedTransactions.push(tx);
182
147
  }
183
148
  }
184
- this.cache.transactions.set(address, storedTransactions);
185
- await this.storage.setItem(`tx:${address}`, JSON.stringify(storedTransactions));
149
+ await this.storage.setItem(getTransactionsStorageKey(address), JSON.stringify(storedTransactions));
186
150
  }
187
151
  async clearTransactions(address) {
188
- this.cache.transactions.set(address, []);
189
- await this.storage.removeItem(`tx:${address}`);
152
+ await this.storage.removeItem(getTransactionsStorageKey(address));
190
153
  }
191
154
  async getWalletState() {
192
- if (this.cache.walletState !== null ||
193
- this.cache.initialized.has("walletState")) {
194
- return this.cache.walletState;
195
- }
196
- const stored = await this.storage.getItem("wallet:state");
197
- if (!stored) {
198
- this.cache.walletState = null;
199
- this.cache.initialized.add("walletState");
155
+ const stored = await this.storage.getItem(walletStateStorageKey);
156
+ if (!stored)
200
157
  return null;
201
- }
202
158
  try {
203
159
  const state = JSON.parse(stored);
204
- this.cache.walletState = state;
205
- this.cache.initialized.add("walletState");
206
160
  return state;
207
161
  }
208
162
  catch (error) {
209
163
  console.error("Failed to parse wallet state:", error);
210
- this.cache.walletState = null;
211
- this.cache.initialized.add("walletState");
212
164
  return null;
213
165
  }
214
166
  }
215
167
  async saveWalletState(state) {
216
- this.cache.walletState = state;
217
- await this.storage.setItem("wallet:state", JSON.stringify(state));
168
+ await this.storage.setItem(walletStateStorageKey, JSON.stringify(state));
218
169
  }
219
170
  }
220
171
  exports.WalletRepositoryImpl = WalletRepositoryImpl;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildOffchainTx = buildOffchainTx;
4
4
  exports.hasBoardingTxExpired = hasBoardingTxExpired;
5
5
  exports.verifyTapscriptSignatures = verifyTapscriptSignatures;
6
+ exports.combineTapscriptSigs = combineTapscriptSigs;
6
7
  const secp256k1_js_1 = require("@noble/curves/secp256k1.js");
7
8
  const base_1 = require("@scure/base");
8
9
  const btc_signer_1 = require("@scure/btc-signer");
@@ -25,6 +26,17 @@ const transaction_1 = require("./transaction");
25
26
  * @returns Object containing the virtual transaction and checkpoint transactions
26
27
  */
27
28
  function buildOffchainTx(inputs, outputs, serverUnrollScript) {
29
+ let hasOpReturn = false;
30
+ for (const [index, output] of outputs.entries()) {
31
+ if (!output.script)
32
+ throw new Error(`missing output script ${index}`);
33
+ const isOpReturn = btc_signer_1.Script.decode(output.script)[0] === "RETURN";
34
+ if (!isOpReturn)
35
+ continue;
36
+ if (hasOpReturn)
37
+ throw new Error("multiple OP_RETURN outputs");
38
+ hasOpReturn = true;
39
+ }
28
40
  const checkpoints = inputs.map((input) => buildCheckpointTx(input, serverUnrollScript));
29
41
  const arkTx = buildVirtualTx(checkpoints.map((c) => c.input), outputs);
30
42
  return {
@@ -209,3 +221,20 @@ function verifyTapscriptSignatures(tx, inputIndex, requiredSigners, excludePubke
209
221
  throw new Error(`Missing signatures from: ${missingSigners.map((pk) => pk.slice(0, 16)).join(", ")}...`);
210
222
  }
211
223
  }
224
+ /**
225
+ * Merges the signed transaction with the original transaction
226
+ * @param signedTx signed transaction
227
+ * @param originalTx original transaction
228
+ */
229
+ function combineTapscriptSigs(signedTx, originalTx) {
230
+ for (let i = 0; i < signedTx.inputsLength; i++) {
231
+ const input = originalTx.getInput(i);
232
+ const signedInput = signedTx.getInput(i);
233
+ if (!input.tapScriptSig)
234
+ throw new Error("No tapScriptSig");
235
+ originalTx.updateInput(i, {
236
+ tapScriptSig: input.tapScriptSig?.concat(signedInput.tapScriptSig),
237
+ });
238
+ }
239
+ return originalTx;
240
+ }
@@ -14,6 +14,10 @@ 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));
17
21
  let vtxosLeftToCheck = [...spent];
18
22
  for (const vtxo of [...spendable, ...spent]) {
19
23
  if (vtxo.virtualStatus.state !== "preconfirmed" &&
@@ -21,6 +25,16 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
21
25
  vtxo.virtualStatus.commitmentTxIds.some((txid) => boardingBatchTxids.has(txid))) {
22
26
  continue;
23
27
  }
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
+ }
24
38
  const settleVtxos = findVtxosSpentInSettlement(vtxosLeftToCheck, vtxo);
25
39
  vtxosLeftToCheck = removeVtxosFromList(vtxosLeftToCheck, settleVtxos);
26
40
  const settleAmount = reduceVtxosAmount(settleVtxos);
@@ -56,21 +70,17 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
56
70
  // vtxos by settled by or ark txid
57
71
  const vtxosByTxid = new Map();
58
72
  for (const v of spent) {
59
- if (v.settledBy) {
60
- if (!vtxosByTxid.has(v.settledBy)) {
61
- vtxosByTxid.set(v.settledBy, []);
62
- }
63
- const currentVtxos = vtxosByTxid.get(v.settledBy);
64
- vtxosByTxid.set(v.settledBy, [...currentVtxos, v]);
65
- }
66
- if (!v.arkTxId) {
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) {
67
77
  continue;
68
78
  }
69
- if (!vtxosByTxid.has(v.arkTxId)) {
70
- vtxosByTxid.set(v.arkTxId, []);
79
+ if (!vtxosByTxid.has(groupKey)) {
80
+ vtxosByTxid.set(groupKey, []);
71
81
  }
72
- const currentVtxos = vtxosByTxid.get(v.arkTxId);
73
- vtxosByTxid.set(v.arkTxId, [...currentVtxos, v]);
82
+ const currentVtxos = vtxosByTxid.get(groupKey);
83
+ vtxosByTxid.set(groupKey, [...currentVtxos, v]);
74
84
  }
75
85
  for (const [sb, vtxos] of vtxosByTxid) {
76
86
  const resultedVtxos = findVtxosResultedFromTxid([...spendable, ...spent], sb);
@@ -85,7 +95,13 @@ function vtxosToTxs(spendable, spent, boardingBatchTxids) {
85
95
  boardingTxid: "",
86
96
  arkTxid: "",
87
97
  };
88
- if (vtxo.virtualStatus.state === "preconfirmed") {
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;
103
+ }
104
+ else if (vtxo.virtualStatus.state === "preconfirmed") {
89
105
  txKey.arkTxid = vtxo.txid;
90
106
  }
91
107
  txs.push({
@@ -56,10 +56,11 @@ class Ramps {
56
56
  * Offboard vtxos, or "collaborative exit" vtxos to onchain address.
57
57
  *
58
58
  * @param destinationAddress - The destination address to offboard to.
59
+ * @param feeInfo - The fee info to deduct from the offboard amount.
59
60
  * @param amount - The amount to offboard. If not provided, the total amount of vtxos will be offboarded.
60
61
  * @param eventCallback - The callback to receive settlement events. optional.
61
62
  */
62
- async offboard(destinationAddress, amount, eventCallback) {
63
+ async offboard(destinationAddress, feeInfo, amount, eventCallback) {
63
64
  const vtxos = await this.wallet.getVtxos({
64
65
  withRecoverable: true,
65
66
  withUnrolled: false,
@@ -73,6 +74,11 @@ class Ramps {
73
74
  change = totalAmount - amount;
74
75
  }
75
76
  amount = amount ?? totalAmount;
77
+ const fees = feeInfo.intentFee.onchainOutput;
78
+ if (fees > amount) {
79
+ throw new Error(`can't deduct fees from offboard amount (${fees} > ${amount})`);
80
+ }
81
+ amount -= fees;
76
82
  const outputs = [
77
83
  {
78
84
  address: destinationAddress,
@@ -77,7 +77,6 @@ class Worker {
77
77
  const { boardingTxs, commitmentsToIgnore: roundsToIgnore } = await this.wallet.getBoardingTxs();
78
78
  const { spendable, spent } = await this.getAllVtxos();
79
79
  // convert VTXOs to offchain transactions
80
- console.log("getTransactionHistory - vtxosToTxs:", spendable);
81
80
  const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendable, spent, roundsToIgnore);
82
81
  txs = [...boardingTxs, ...offchainTxs];
83
82
  // sort transactions by creation time in descending order (newest first)