@arkade-os/sdk 0.4.20 → 0.4.22

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.
@@ -153,62 +153,75 @@ export class RestIndexerProvider {
153
153
  }
154
154
  return data;
155
155
  }
156
- async *getSubscription(subscriptionId, abortSignal) {
156
+ getSubscription(subscriptionId, abortSignal) {
157
157
  const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
158
- while (!abortSignal?.aborted) {
158
+ let iterator = null;
159
+ const closeIterator = () => iterator?.close();
160
+ const gen = (async function* () {
161
+ const abortHandler = closeIterator;
162
+ abortSignal?.addEventListener("abort", abortHandler);
159
163
  try {
160
- const eventSource = new EventSource(url);
161
- // Set up abort handling
162
- const abortHandler = () => {
163
- eventSource.close();
164
- };
165
- abortSignal?.addEventListener("abort", abortHandler);
166
- try {
167
- for await (const event of eventSourceIterator(eventSource)) {
168
- if (abortSignal?.aborted)
169
- break;
170
- try {
171
- const data = JSON.parse(event.data);
172
- if (data.event) {
173
- yield {
174
- txid: data.event.txid,
175
- scripts: data.event.scripts || [],
176
- newVtxos: (data.event.newVtxos || []).map(convertVtxo),
177
- spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
178
- sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
179
- tx: data.event.tx,
180
- checkpointTxs: data.event.checkpointTxs,
181
- };
164
+ while (!abortSignal?.aborted) {
165
+ try {
166
+ const currentIterator = eventSourceIterator(new EventSource(url));
167
+ iterator = currentIterator;
168
+ for await (const event of currentIterator) {
169
+ if (abortSignal?.aborted)
170
+ break;
171
+ try {
172
+ const data = JSON.parse(event.data);
173
+ if (data.event) {
174
+ yield {
175
+ txid: data.event.txid,
176
+ scripts: data.event.scripts || [],
177
+ newVtxos: (data.event.newVtxos || []).map(convertVtxo),
178
+ spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
179
+ sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
180
+ tx: data.event.tx,
181
+ checkpointTxs: data.event.checkpointTxs,
182
+ };
183
+ }
184
+ }
185
+ catch (err) {
186
+ console.error("Failed to parse subscription event:", err);
187
+ throw err;
182
188
  }
183
189
  }
184
- catch (err) {
185
- console.error("Failed to parse subscription event:", err);
186
- throw err;
190
+ }
191
+ catch (error) {
192
+ if (abortSignal?.aborted ||
193
+ (error instanceof Error &&
194
+ error.name === "AbortError")) {
195
+ break;
187
196
  }
197
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
198
+ if (isFetchTimeoutError(error)) {
199
+ console.debug("Timeout error ignored");
200
+ continue;
201
+ }
202
+ if (isEventSourceError(error)) {
203
+ throw error;
204
+ }
205
+ console.error("Subscription error:", error);
206
+ throw error;
207
+ }
208
+ finally {
209
+ closeIterator();
210
+ iterator = null;
188
211
  }
189
- }
190
- finally {
191
- abortSignal?.removeEventListener("abort", abortHandler);
192
- eventSource.close();
193
212
  }
194
213
  }
195
- catch (error) {
196
- if (abortSignal?.aborted ||
197
- (error instanceof Error && error.name === "AbortError")) {
198
- break;
199
- }
200
- // ignore timeout errors, they're expected when the server is not sending anything for 5 min
201
- if (isFetchTimeoutError(error)) {
202
- console.debug("Timeout error ignored");
203
- continue;
204
- }
205
- if (isEventSourceError(error)) {
206
- throw error;
207
- }
208
- console.error("Subscription error:", error);
209
- throw error;
214
+ finally {
215
+ abortSignal?.removeEventListener("abort", abortHandler);
216
+ closeIterator();
210
217
  }
211
- }
218
+ })();
219
+ const origReturn = gen.return.bind(gen);
220
+ gen.return = (value) => {
221
+ closeIterator();
222
+ return origReturn(value);
223
+ };
224
+ return gen;
212
225
  }
213
226
  async getVirtualTxs(txids, opts) {
214
227
  let url = `${this.serverUrl}/v1/indexer/virtualTx/${txids.join(",")}`;
@@ -1,29 +1,67 @@
1
+ function createAbortError() {
2
+ const error = new Error("EventSource closed");
3
+ error.name = "AbortError";
4
+ return error;
5
+ }
1
6
  /**
2
- * Creates an async iterator over EventSource messages that attaches listeners
3
- * eagerly (at call time) rather than lazily (at first .next() call).
4
- * This ensures events are buffered immediately, preventing race conditions
5
- * where events arrive before iteration begins.
7
+ * Creates a close-aware EventSource async iterator.
8
+ *
9
+ * Listeners attach eagerly so events are buffered before the first next() call.
10
+ * close() closes the EventSource, removes listeners, and wakes any pending
11
+ * next() even when the browser does not emit an error from EventSource.close().
6
12
  */
7
13
  export function eventSourceIterator(eventSource) {
8
14
  const messageQueue = [];
9
15
  const errorQueue = [];
10
16
  let messageResolve = null;
11
17
  let errorResolve = null;
18
+ let closed = false;
19
+ let cleanedUp = false;
20
+ const cleanup = () => {
21
+ if (cleanedUp)
22
+ return;
23
+ cleanedUp = true;
24
+ eventSource.removeEventListener("message", messageHandler);
25
+ eventSource.removeEventListener("error", errorHandler);
26
+ };
27
+ const close = () => {
28
+ if (closed)
29
+ return;
30
+ closed = true;
31
+ messageQueue.length = 0;
32
+ errorQueue.length = 0;
33
+ eventSource.close();
34
+ cleanup();
35
+ if (errorResolve) {
36
+ const reject = errorResolve;
37
+ messageResolve = null;
38
+ errorResolve = null;
39
+ reject(createAbortError());
40
+ }
41
+ };
12
42
  const messageHandler = (event) => {
43
+ if (closed)
44
+ return;
13
45
  if (messageResolve) {
14
- messageResolve(event);
46
+ const resolve = messageResolve;
15
47
  messageResolve = null;
48
+ errorResolve = null;
49
+ resolve(event);
16
50
  }
17
51
  else {
18
52
  messageQueue.push(event);
19
53
  }
20
54
  };
21
55
  const errorHandler = () => {
56
+ if (closed)
57
+ return;
22
58
  const error = new Error("EventSource error");
23
59
  error.name = "EventSourceError";
24
60
  if (errorResolve) {
25
- errorResolve(error);
61
+ const reject = errorResolve;
62
+ messageResolve = null;
26
63
  errorResolve = null;
64
+ reject(error);
27
65
  }
28
66
  else {
29
67
  errorQueue.push(error);
@@ -33,9 +71,9 @@ export function eventSourceIterator(eventSource) {
33
71
  // even before the caller starts iterating
34
72
  eventSource.addEventListener("message", messageHandler);
35
73
  eventSource.addEventListener("error", errorHandler);
36
- return (async function* () {
74
+ const gen = (async function* () {
37
75
  try {
38
- while (true) {
76
+ while (!closed) {
39
77
  // if we have queued messages, yield the first one, remove it from the queue
40
78
  if (messageQueue.length > 0) {
41
79
  yield messageQueue.shift();
@@ -54,17 +92,25 @@ export function eventSourceIterator(eventSource) {
54
92
  messageResolve = null;
55
93
  errorResolve = null;
56
94
  });
57
- if (result) {
95
+ if (!closed && result) {
58
96
  yield result;
59
97
  }
60
98
  }
61
99
  }
62
100
  finally {
63
- // clean up
64
- eventSource.removeEventListener("message", messageHandler);
65
- eventSource.removeEventListener("error", errorHandler);
101
+ closed = true;
102
+ cleanup();
103
+ eventSource.close();
66
104
  }
67
105
  })();
106
+ const origReturn = gen.return.bind(gen);
107
+ const managed = gen;
108
+ managed.close = close;
109
+ managed.return = (value) => {
110
+ close();
111
+ return origReturn(value);
112
+ };
113
+ return managed;
68
114
  }
69
115
  export function isEventSourceError(error) {
70
116
  return error instanceof Error && error.name === "EventSourceError";
@@ -25,20 +25,30 @@ export class DelegatorManagerImpl {
25
25
  // fetch server and delegator info once, shared across all groups
26
26
  const arkInfo = await this.arkInfoProvider.getInfo();
27
27
  const delegateInfo = await this.delegatorProvider.getDelegateInfo();
28
+ // keep only vtxos that can be signed by the delegate
29
+ const eligible = vtxos
30
+ .filter((v) => findDelegateTapLeaf(v, delegateInfo.pubkey) !== undefined)
31
+ .map((v) => v);
32
+ if (eligible.length === 0) {
33
+ return { delegated: [], failed: [] };
34
+ }
28
35
  // if explicit delegateAt is provided, delegate all virtual outputs at once without sorting
29
36
  if (delegateAt) {
30
37
  try {
31
- await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, vtxos, destinationScript, delegateAt);
38
+ await delegate(this.identity, this.delegatorProvider, arkInfo, delegateInfo, eligible, destinationScript, delegateAt);
32
39
  }
33
40
  catch (error) {
34
- return { delegated: [], failed: [{ outpoints: vtxos, error }] };
41
+ return {
42
+ delegated: [],
43
+ failed: [{ outpoints: eligible, error }],
44
+ };
35
45
  }
36
- return { delegated: vtxos, failed: [] };
46
+ return { delegated: eligible, failed: [] };
37
47
  }
38
48
  // if no explicit delegateAt is provided, sort virtual outputs by expiry and delegate in groups of the same expiry day
39
49
  const groupByExpiry = new Map();
40
50
  let recoverableVtxos = [];
41
- for (const vtxo of vtxos) {
51
+ for (const vtxo of eligible) {
42
52
  if (isRecoverable(vtxo)) {
43
53
  recoverableVtxos.push(vtxo);
44
54
  continue;
@@ -185,20 +195,7 @@ async function delegate(identity, delegatorProvider, arkInfo, delegateInfo, vtxo
185
195
  await delegatorProvider.delegate(registerIntent, forfeits);
186
196
  }
187
197
  async function makeDelegateForfeitTx(input, connectorAmount, delegatePubkey, forfeitOutputScript, identity) {
188
- if (delegatePubkey.length === 66) {
189
- delegatePubkey = delegatePubkey.slice(2);
190
- }
191
- const vtxoScript = VtxoScript.decode(input.tapTree);
192
- const delegateTapLeaf = vtxoScript.leaves.find((tapLeaf) => {
193
- const arkTapscript = decodeTapscript(scriptFromTapLeafScript(tapLeaf));
194
- if (!MultisigTapscript.is(arkTapscript))
195
- return false;
196
- if (!arkTapscript.params.pubkeys
197
- .map(hex.encode)
198
- .includes(delegatePubkey))
199
- return false;
200
- return true;
201
- });
198
+ const delegateTapLeaf = findDelegateTapLeaf(input, delegatePubkey);
202
199
  if (!delegateTapLeaf) {
203
200
  throw new Error(`delegate tap leaf not found for input: ${input.txid}:${input.vout}`);
204
201
  }
@@ -286,3 +283,15 @@ function getDayTimestamp(timestamp) {
286
283
  date.setUTCHours(0, 0, 0, 0);
287
284
  return date.getTime();
288
285
  }
286
+ function findDelegateTapLeaf(vtxo, delegatePubkey) {
287
+ if (!vtxo.tapTree)
288
+ return undefined;
289
+ const pk = delegatePubkey.length === 66 ? delegatePubkey.slice(2) : delegatePubkey;
290
+ const vtxoScript = VtxoScript.decode(vtxo.tapTree);
291
+ return vtxoScript.leaves.find((tapLeaf) => {
292
+ const arkTapscript = decodeTapscript(scriptFromTapLeafScript(tapLeaf));
293
+ if (!MultisigTapscript.is(arkTapscript))
294
+ return false;
295
+ return arkTapscript.params.pubkeys.map(hex.encode).includes(pk);
296
+ });
297
+ }
@@ -700,7 +700,9 @@ export class WalletMessageHandler {
700
700
  const { vtxoOutpoints, destination, delegateAt } = message.payload;
701
701
  const allVtxos = await wallet.getVtxos();
702
702
  const outpointSet = new Set(vtxoOutpoints.map((o) => `${o.txid}:${o.vout}`));
703
- const filtered = allVtxos.filter((v) => outpointSet.has(`${v.txid}:${v.vout}`));
703
+ const filtered = allVtxos
704
+ .filter((v) => outpointSet.has(`${v.txid}:${v.vout}`))
705
+ .map((v) => ({ ...v, contractScript: v.script }));
704
706
  const result = await delegatorManager.delegate(filtered, destination, delegateAt !== undefined ? new Date(delegateAt) : undefined);
705
707
  return {
706
708
  tag: this.messageTag,
@@ -693,11 +693,13 @@ export class VtxoManager {
693
693
  console.error("Error renewing VTXOs:", e);
694
694
  });
695
695
  }
696
- delegatorManager
697
- ?.delegate(event.vtxos, destination)
698
- .catch((e) => {
699
- console.error("Error delegating VTXOs:", e);
700
- });
696
+ if (delegatorManager) {
697
+ delegatorManager
698
+ .delegate(event.vtxos, destination)
699
+ .catch((e) => {
700
+ console.error("Error delegating VTXOs:", e);
701
+ });
702
+ }
701
703
  });
702
704
  return stopWatching;
703
705
  }