@arkade-os/sdk 0.3.0-alpha.8 → 0.3.0

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.
Files changed (101) hide show
  1. package/README.md +48 -14
  2. package/dist/cjs/arknote/index.js +3 -3
  3. package/dist/cjs/forfeit.js +2 -2
  4. package/dist/cjs/identity/singleKey.js +8 -8
  5. package/dist/cjs/index.js +13 -5
  6. package/dist/cjs/{bip322 → intent}/index.js +38 -61
  7. package/dist/cjs/musig2/index.js +2 -1
  8. package/dist/cjs/musig2/nonces.js +4 -0
  9. package/dist/cjs/providers/ark.js +76 -45
  10. package/dist/cjs/providers/errors.js +59 -0
  11. package/dist/cjs/providers/expoArk.js +15 -170
  12. package/dist/cjs/providers/expoIndexer.js +22 -111
  13. package/dist/cjs/providers/expoUtils.js +124 -0
  14. package/dist/cjs/providers/onchain.js +19 -20
  15. package/dist/cjs/repositories/walletRepository.js +64 -28
  16. package/dist/cjs/script/base.js +15 -7
  17. package/dist/cjs/script/tapscript.js +20 -21
  18. package/dist/cjs/script/vhtlc.js +2 -2
  19. package/dist/cjs/tree/signingSession.js +44 -11
  20. package/dist/cjs/tree/txTree.js +3 -4
  21. package/dist/cjs/tree/validation.js +2 -3
  22. package/dist/cjs/utils/arkTransaction.js +105 -15
  23. package/dist/cjs/utils/transaction.js +28 -0
  24. package/dist/cjs/utils/unknownFields.js +7 -7
  25. package/dist/cjs/wallet/onchain.js +6 -7
  26. package/dist/cjs/wallet/serviceWorker/response.js +32 -0
  27. package/dist/cjs/wallet/serviceWorker/utils.js +2 -0
  28. package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
  29. package/dist/cjs/wallet/serviceWorker/worker.js +46 -27
  30. package/dist/cjs/wallet/unroll.js +7 -9
  31. package/dist/cjs/wallet/utils.js +9 -0
  32. package/dist/cjs/wallet/vtxo-manager.js +323 -0
  33. package/dist/cjs/wallet/wallet.js +98 -125
  34. package/dist/esm/arknote/index.js +2 -2
  35. package/dist/esm/forfeit.js +1 -1
  36. package/dist/esm/identity/singleKey.js +9 -9
  37. package/dist/esm/index.js +14 -10
  38. package/dist/esm/{bip322 → intent}/index.js +32 -54
  39. package/dist/esm/musig2/index.js +1 -1
  40. package/dist/esm/musig2/nonces.js +3 -0
  41. package/dist/esm/providers/ark.js +76 -45
  42. package/dist/esm/providers/errors.js +54 -0
  43. package/dist/esm/providers/expoArk.js +15 -137
  44. package/dist/esm/providers/expoIndexer.js +22 -78
  45. package/dist/esm/providers/expoUtils.js +87 -0
  46. package/dist/esm/providers/onchain.js +19 -20
  47. package/dist/esm/repositories/walletRepository.js +64 -28
  48. package/dist/esm/script/base.js +12 -4
  49. package/dist/esm/script/tapscript.js +1 -2
  50. package/dist/esm/script/vhtlc.js +1 -1
  51. package/dist/esm/tree/signingSession.js +45 -12
  52. package/dist/esm/tree/txTree.js +3 -4
  53. package/dist/esm/tree/validation.js +2 -3
  54. package/dist/esm/utils/arkTransaction.js +97 -8
  55. package/dist/esm/utils/transaction.js +24 -0
  56. package/dist/esm/utils/unknownFields.js +3 -3
  57. package/dist/esm/wallet/onchain.js +3 -4
  58. package/dist/esm/wallet/serviceWorker/response.js +32 -0
  59. package/dist/esm/wallet/serviceWorker/utils.js +1 -0
  60. package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
  61. package/dist/esm/wallet/serviceWorker/worker.js +48 -29
  62. package/dist/esm/wallet/unroll.js +5 -7
  63. package/dist/esm/wallet/utils.js +8 -0
  64. package/dist/esm/wallet/vtxo-manager.js +317 -0
  65. package/dist/esm/wallet/wallet.js +92 -119
  66. package/dist/types/arknote/index.d.ts +1 -1
  67. package/dist/types/forfeit.d.ts +2 -2
  68. package/dist/types/identity/index.d.ts +2 -2
  69. package/dist/types/identity/singleKey.d.ts +2 -2
  70. package/dist/types/index.d.ts +9 -7
  71. package/dist/types/intent/index.d.ts +41 -0
  72. package/dist/types/musig2/index.d.ts +1 -1
  73. package/dist/types/musig2/nonces.d.ts +1 -0
  74. package/dist/types/providers/ark.d.ts +62 -26
  75. package/dist/types/providers/errors.d.ts +13 -0
  76. package/dist/types/providers/expoIndexer.d.ts +2 -10
  77. package/dist/types/providers/expoUtils.d.ts +18 -0
  78. package/dist/types/providers/indexer.d.ts +1 -9
  79. package/dist/types/providers/onchain.d.ts +6 -2
  80. package/dist/types/repositories/walletRepository.d.ts +9 -5
  81. package/dist/types/script/base.d.ts +5 -2
  82. package/dist/types/tree/signingSession.d.ts +16 -11
  83. package/dist/types/utils/anchor.d.ts +2 -2
  84. package/dist/types/utils/arkTransaction.d.ts +12 -4
  85. package/dist/types/utils/transaction.d.ts +13 -0
  86. package/dist/types/utils/unknownFields.d.ts +4 -4
  87. package/dist/types/wallet/index.d.ts +6 -4
  88. package/dist/types/wallet/onchain.d.ts +1 -1
  89. package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
  90. package/dist/types/wallet/serviceWorker/utils.d.ts +1 -0
  91. package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
  92. package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
  93. package/dist/types/wallet/unroll.d.ts +1 -1
  94. package/dist/types/wallet/utils.d.ts +2 -1
  95. package/dist/types/wallet/vtxo-manager.d.ts +179 -0
  96. package/dist/types/wallet/wallet.d.ts +8 -4
  97. package/package.json +1 -2
  98. package/dist/cjs/bip322/errors.js +0 -13
  99. package/dist/esm/bip322/errors.js +0 -9
  100. package/dist/types/bip322/errors.d.ts +0 -6
  101. package/dist/types/bip322/index.d.ts +0 -57
@@ -1,4 +1,5 @@
1
1
  import { RestArkProvider, isFetchTimeoutError, } from './ark.js';
2
+ import { getExpoFetch, sseStreamIterator } from './expoUtils.js';
2
3
  /**
3
4
  * Expo-compatible Ark provider implementation using expo/fetch for SSE support.
4
5
  * This provider works specifically in React Native/Expo environments where
@@ -17,87 +18,23 @@ export class ExpoArkProvider extends RestArkProvider {
17
18
  super(serverUrl);
18
19
  }
19
20
  async *getEventStream(signal, topics) {
20
- // Dynamic import to avoid bundling expo/fetch in non-Expo environments
21
- let expoFetch = fetch; // Default to standard fetch
22
- try {
23
- const expoFetchModule = await import("expo/fetch");
24
- // expo/fetch returns a compatible fetch function but with different types
25
- expoFetch = expoFetchModule.fetch;
26
- console.debug("Using expo/fetch for SSE");
27
- }
28
- catch (error) {
29
- // Fall back to standard fetch if expo/fetch is not available
30
- console.warn("Using standard fetch instead of expo/fetch. " +
31
- "Streaming may not be fully supported in some environments.", error);
32
- }
21
+ const expoFetch = await getExpoFetch();
33
22
  const url = `${this.serverUrl}/v1/batch/events`;
34
23
  const queryParams = topics.length > 0
35
24
  ? `?${topics.map((topic) => `topics=${encodeURIComponent(topic)}`).join("&")}`
36
25
  : "";
37
26
  while (!signal?.aborted) {
38
- // Create a new AbortController for this specific fetch attempt
39
- // to prevent accumulating listeners on the parent signal
40
- const fetchController = new AbortController();
41
- const cleanup = () => fetchController.abort();
42
- signal?.addEventListener("abort", cleanup, { once: true });
43
27
  try {
44
- const response = await expoFetch(url + queryParams, {
45
- headers: {
46
- Accept: "text/event-stream",
47
- },
48
- signal: fetchController.signal,
49
- });
50
- if (!response.ok) {
51
- throw new Error(`Unexpected status ${response.status} when fetching event stream`);
52
- }
53
- if (!response.body) {
54
- throw new Error("Response body is null");
55
- }
56
- const reader = response.body.getReader();
57
- const decoder = new TextDecoder();
58
- let buffer = "";
59
- while (!signal?.aborted) {
60
- const { done, value } = await reader.read();
61
- if (done) {
62
- break;
28
+ yield* sseStreamIterator(url + queryParams, signal, expoFetch, {}, (data) => {
29
+ // Handle different response structures
30
+ // v8 mesh API might wrap in {result: ...} or send directly
31
+ const eventData = data.result || data;
32
+ // Skip heartbeat messages
33
+ if (eventData.heartbeat !== undefined) {
34
+ return null;
63
35
  }
64
- // Append new data to buffer and split by newlines
65
- buffer += decoder.decode(value, { stream: true });
66
- const lines = buffer.split("\n");
67
- // Process all complete lines
68
- for (let i = 0; i < lines.length - 1; i++) {
69
- const line = lines[i].trim();
70
- if (!line)
71
- continue;
72
- try {
73
- // Parse SSE format: "data: {json}"
74
- if (line.startsWith("data:")) {
75
- const jsonStr = line.substring(5).trim();
76
- if (!jsonStr)
77
- continue;
78
- const data = JSON.parse(jsonStr);
79
- // Handle different response structures
80
- // v8 mesh API might wrap in {result: ...} or send directly
81
- const eventData = data.result || data;
82
- // Skip heartbeat messages
83
- if (eventData.heartbeat !== undefined) {
84
- continue;
85
- }
86
- const event = this.parseSettlementEvent(eventData);
87
- if (event) {
88
- yield event;
89
- }
90
- }
91
- }
92
- catch (err) {
93
- console.error("Failed to parse event:", line);
94
- console.error("Parse error:", err);
95
- throw err;
96
- }
97
- }
98
- // Keep the last partial line in the buffer
99
- buffer = lines[lines.length - 1];
100
- }
36
+ return this.parseSettlementEvent(eventData);
37
+ });
101
38
  }
102
39
  catch (error) {
103
40
  if (error instanceof Error && error.name === "AbortError") {
@@ -112,71 +49,16 @@ export class ExpoArkProvider extends RestArkProvider {
112
49
  console.error("Event stream error:", error);
113
50
  throw error;
114
51
  }
115
- finally {
116
- // Clean up the abort listener
117
- signal?.removeEventListener("abort", cleanup);
118
- }
119
52
  }
120
53
  }
121
54
  async *getTransactionsStream(signal) {
122
- // Dynamic import to avoid bundling expo/fetch in non-Expo environments
123
- let expoFetch = fetch; // Default to standard fetch
124
- try {
125
- const expoFetchModule = await import("expo/fetch");
126
- // expo/fetch returns a compatible fetch function but with different types
127
- expoFetch = expoFetchModule.fetch;
128
- console.debug("Using expo/fetch for transaction stream");
129
- }
130
- catch (error) {
131
- // Fall back to standard fetch if expo/fetch is not available
132
- console.warn("Using standard fetch instead of expo/fetch. " +
133
- "Streaming may not be fully supported in some environments.", error);
134
- }
55
+ const expoFetch = await getExpoFetch();
135
56
  const url = `${this.serverUrl}/v1/txs`;
136
57
  while (!signal?.aborted) {
137
- // Create a new AbortController for this specific fetch attempt
138
- // to prevent accumulating listeners on the parent signal
139
- const fetchController = new AbortController();
140
- const cleanup = () => fetchController.abort();
141
- signal?.addEventListener("abort", cleanup, { once: true });
142
58
  try {
143
- const response = await expoFetch(url, {
144
- headers: {
145
- Accept: "text/event-stream",
146
- },
147
- signal: fetchController.signal,
59
+ yield* sseStreamIterator(url, signal, expoFetch, {}, (data) => {
60
+ return this.parseTransactionNotification(data.result);
148
61
  });
149
- if (!response.ok) {
150
- throw new Error(`Unexpected status ${response.status} when fetching transaction stream`);
151
- }
152
- if (!response.body) {
153
- throw new Error("Response body is null");
154
- }
155
- const reader = response.body.getReader();
156
- const decoder = new TextDecoder();
157
- let buffer = "";
158
- while (!signal?.aborted) {
159
- const { done, value } = await reader.read();
160
- if (done) {
161
- break;
162
- }
163
- // Append new data to buffer and split by newlines
164
- buffer += decoder.decode(value, { stream: true });
165
- const lines = buffer.split("\n");
166
- // Process all complete lines
167
- for (let i = 0; i < lines.length - 1; i++) {
168
- const line = lines[i].trim();
169
- if (!line)
170
- continue;
171
- const data = JSON.parse(line);
172
- const txNotification = this.parseTransactionNotification(data.result);
173
- if (txNotification) {
174
- yield txNotification;
175
- }
176
- }
177
- // Keep the last partial line in the buffer
178
- buffer = lines[lines.length - 1];
179
- }
180
62
  }
181
63
  catch (error) {
182
64
  if (error instanceof Error && error.name === "AbortError") {
@@ -188,13 +70,9 @@ export class ExpoArkProvider extends RestArkProvider {
188
70
  console.debug("Timeout error ignored");
189
71
  continue;
190
72
  }
191
- console.error("Address subscription error:", error);
73
+ console.error("Transaction stream error:", error);
192
74
  throw error;
193
75
  }
194
- finally {
195
- // Clean up the abort listener
196
- signal?.removeEventListener("abort", cleanup);
197
- }
198
76
  }
199
77
  }
200
78
  }
@@ -1,5 +1,6 @@
1
1
  import { RestIndexerProvider } from './indexer.js';
2
2
  import { isFetchTimeoutError } from './ark.js';
3
+ import { getExpoFetch, sseStreamIterator } from './expoUtils.js';
3
4
  // Helper function to convert Vtxo to VirtualCoin (same as in indexer.ts)
4
5
  function convertVtxo(vtxo) {
5
6
  return {
@@ -49,95 +50,38 @@ export class ExpoIndexerProvider extends RestIndexerProvider {
49
50
  // Detect if we're running in React Native/Expo environment
50
51
  const isReactNative = typeof navigator !== "undefined" &&
51
52
  navigator.product === "ReactNative";
52
- // Dynamic import to avoid bundling expo/fetch in non-Expo environments
53
- let expoFetch = fetch; // Default to standard fetch
54
- try {
55
- const expoFetchModule = await import("expo/fetch");
56
- // expo/fetch returns a compatible fetch function but with different types
57
- expoFetch = expoFetchModule.fetch;
58
- console.debug("Using expo/fetch for indexer subscription");
59
- }
60
- catch (error) {
53
+ const expoFetch = await getExpoFetch().catch((error) => {
61
54
  // In React Native/Expo, expo/fetch is required for proper streaming support
62
55
  if (isReactNative) {
63
56
  throw new Error("expo/fetch is unavailable in React Native environment. " +
64
57
  "Please ensure expo/fetch is installed and properly configured. " +
65
58
  "Streaming support may not work with standard fetch in React Native.");
66
59
  }
67
- // In non-RN environments, fall back to standard fetch but warn about potential streaming issues
68
- console.warn("Using standard fetch instead of expo/fetch. " +
69
- "Streaming may not be fully supported in some environments.", error);
70
- }
60
+ throw error;
61
+ });
71
62
  const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
72
63
  while (!abortSignal.aborted) {
73
64
  try {
74
- const res = await expoFetch(url, {
75
- headers: {
76
- Accept: "text/event-stream",
77
- "Content-Type": "application/json",
78
- },
79
- signal: abortSignal,
80
- });
81
- if (!res.ok) {
82
- throw new Error(`Unexpected status ${res.status} when subscribing to address updates`);
83
- }
84
- // Check if response is the expected content type
85
- const contentType = res.headers.get("content-type");
86
- if (contentType &&
87
- !contentType.includes("text/event-stream") &&
88
- !contentType.includes("application/json")) {
89
- throw new Error(`Unexpected content-type: ${contentType}. Expected text/event-stream or application/json`);
90
- }
91
- if (!res.body) {
92
- throw new Error("Response body is null");
93
- }
94
- const reader = res.body.getReader();
95
- const decoder = new TextDecoder();
96
- let buffer = "";
97
- while (!abortSignal.aborted) {
98
- const { done, value } = await reader.read();
99
- if (done) {
100
- break;
65
+ yield* sseStreamIterator(url, abortSignal, expoFetch, { "Content-Type": "application/json" }, (data) => {
66
+ // Handle new v8 proto format with heartbeat or event
67
+ if (data.heartbeat !== undefined) {
68
+ // Skip heartbeat messages
69
+ return null;
101
70
  }
102
- buffer += decoder.decode(value, { stream: true });
103
- const lines = buffer.split("\n");
104
- for (let i = 0; i < lines.length - 1; i++) {
105
- const line = lines[i].trim();
106
- if (!line)
107
- continue;
108
- try {
109
- // Parse SSE format: "data: {json}"
110
- if (line.startsWith("data:")) {
111
- const jsonStr = line.substring(5).trim();
112
- if (!jsonStr)
113
- continue;
114
- const data = JSON.parse(jsonStr);
115
- // Handle new v8 proto format with heartbeat or event
116
- if (data.heartbeat !== undefined) {
117
- // Skip heartbeat messages
118
- continue;
119
- }
120
- // Process event messages
121
- if (data.event) {
122
- yield {
123
- txid: data.event.txid,
124
- scripts: data.event.scripts || [],
125
- newVtxos: (data.event.newVtxos || []).map(convertVtxo),
126
- spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
127
- sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
128
- tx: data.event.tx,
129
- checkpointTxs: data.event.checkpointTxs,
130
- };
131
- }
132
- }
133
- }
134
- catch (parseError) {
135
- console.error("Failed to parse subscription response:", parseError);
136
- throw parseError;
137
- }
71
+ // Process event messages
72
+ if (data.event) {
73
+ return {
74
+ txid: data.event.txid,
75
+ scripts: data.event.scripts || [],
76
+ newVtxos: (data.event.newVtxos || []).map(convertVtxo),
77
+ spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
78
+ sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
79
+ tx: data.event.tx,
80
+ checkpointTxs: data.event.checkpointTxs,
81
+ };
138
82
  }
139
- buffer = lines[lines.length - 1];
140
- }
83
+ return null;
84
+ });
141
85
  }
142
86
  catch (error) {
143
87
  if (error instanceof Error && error.name === "AbortError") {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Dynamically imports expo/fetch with fallback to standard fetch.
3
+ * @returns A fetch function suitable for SSE streaming
4
+ */
5
+ export async function getExpoFetch(options) {
6
+ const requireExpo = options?.requireExpo ?? false;
7
+ try {
8
+ const expoFetchModule = await import("expo/fetch");
9
+ console.debug("Using expo/fetch for streaming");
10
+ return expoFetchModule.fetch;
11
+ }
12
+ catch (error) {
13
+ if (requireExpo) {
14
+ throw new Error("expo/fetch is unavailable in this environment. " +
15
+ "Please ensure expo/fetch is installed and properly configured.");
16
+ }
17
+ console.warn("Using standard fetch instead of expo/fetch. " +
18
+ "Streaming may not be fully supported in some environments.", error);
19
+ return fetch;
20
+ }
21
+ }
22
+ /**
23
+ * Generic SSE stream processor using fetch API with ReadableStream.
24
+ * Handles SSE format parsing, buffer management, and abort signals.
25
+ *
26
+ * @param url - The SSE endpoint URL
27
+ * @param abortSignal - Signal to abort the stream
28
+ * @param fetchFn - Fetch function to use (defaults to standard fetch)
29
+ * @param headers - Additional headers to send
30
+ * @param parseData - Function to parse and yield data from SSE events
31
+ */
32
+ export async function* sseStreamIterator(url, abortSignal, fetchFn, headers, parseData) {
33
+ const fetchController = new AbortController();
34
+ const cleanup = () => fetchController.abort();
35
+ abortSignal?.addEventListener("abort", cleanup, { once: true });
36
+ try {
37
+ const response = await fetchFn(url, {
38
+ headers: {
39
+ Accept: "text/event-stream",
40
+ ...headers,
41
+ },
42
+ signal: fetchController.signal,
43
+ });
44
+ if (!response.ok) {
45
+ throw new Error(`Unexpected status ${response.status} when fetching SSE stream`);
46
+ }
47
+ if (!response.body) {
48
+ throw new Error("Response body is null");
49
+ }
50
+ const reader = response.body.getReader();
51
+ const decoder = new TextDecoder();
52
+ let buffer = "";
53
+ while (!abortSignal?.aborted) {
54
+ const { done, value } = await reader.read();
55
+ if (done) {
56
+ break;
57
+ }
58
+ buffer += decoder.decode(value, { stream: true });
59
+ const lines = buffer.split("\n");
60
+ for (let i = 0; i < lines.length - 1; i++) {
61
+ const line = lines[i].trim();
62
+ if (!line)
63
+ continue;
64
+ if (line.startsWith("data:")) {
65
+ const jsonStr = line.substring(5).trim();
66
+ if (!jsonStr)
67
+ continue;
68
+ try {
69
+ const data = JSON.parse(jsonStr);
70
+ const parsed = parseData(data);
71
+ if (parsed !== null) {
72
+ yield parsed;
73
+ }
74
+ }
75
+ catch (parseError) {
76
+ console.error("Failed to parse SSE data:", parseError);
77
+ throw parseError;
78
+ }
79
+ }
80
+ }
81
+ buffer = lines[lines.length - 1];
82
+ }
83
+ }
84
+ finally {
85
+ abortSignal?.removeEventListener("abort", cleanup);
86
+ }
87
+ }
@@ -18,9 +18,10 @@ export const ESPLORA_URL = {
18
18
  * ```
19
19
  */
20
20
  export class EsploraProvider {
21
- constructor(baseUrl) {
21
+ constructor(baseUrl, opts) {
22
22
  this.baseUrl = baseUrl;
23
- this.polling = false;
23
+ this.pollingInterval = opts?.pollingInterval ?? 15000;
24
+ this.forcePolling = opts?.forcePolling ?? false;
24
25
  }
25
26
  async getCoins(address) {
26
27
  const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
@@ -91,13 +92,9 @@ export class EsploraProvider {
91
92
  let intervalId = null;
92
93
  const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
93
94
  const poll = async () => {
94
- if (this.polling)
95
- return;
96
- this.polling = true;
97
- // websocket is not reliable, so we will fallback to polling
98
- const pollingInterval = 5000; // 5 seconds
99
- const getAllTxs = () => {
100
- return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
95
+ const getAllTxs = async () => {
96
+ const txArrays = await Promise.all(addresses.map((address) => this.getTransactions(address)));
97
+ return txArrays.flat();
101
98
  };
102
99
  // initial fetch to get existing transactions
103
100
  const initialTxs = await getAllTxs();
@@ -122,9 +119,19 @@ export class EsploraProvider {
122
119
  catch (error) {
123
120
  console.error("Error in polling mechanism:", error);
124
121
  }
125
- }, pollingInterval);
122
+ }, this.pollingInterval);
126
123
  };
127
124
  let ws = null;
125
+ const stopFunc = () => {
126
+ if (ws)
127
+ ws.close();
128
+ if (intervalId)
129
+ clearInterval(intervalId);
130
+ };
131
+ if (this.forcePolling) {
132
+ await poll();
133
+ return stopFunc;
134
+ }
128
135
  try {
129
136
  ws = new WebSocket(wsUrl);
130
137
  ws.addEventListener("open", () => {
@@ -171,13 +178,6 @@ export class EsploraProvider {
171
178
  // if websocket is not available, fallback to polling
172
179
  await poll();
173
180
  }
174
- const stopFunc = () => {
175
- if (ws && ws.readyState === WebSocket.OPEN)
176
- ws.close();
177
- if (intervalId)
178
- clearInterval(intervalId);
179
- this.polling = false;
180
- };
181
181
  return stopFunc;
182
182
  }
183
183
  async getChainTip() {
@@ -245,8 +245,7 @@ const isExplorerTransaction = (tx) => {
245
245
  return (typeof tx.txid === "string" &&
246
246
  Array.isArray(tx.vout) &&
247
247
  tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
248
- typeof vout.value === "string") &&
248
+ typeof vout.value === "number") &&
249
249
  typeof tx.status === "object" &&
250
- typeof tx.status.confirmed === "boolean" &&
251
- typeof tx.status.block_time === "number");
250
+ typeof tx.status.confirmed === "boolean");
252
251
  };
@@ -12,7 +12,14 @@ const serializeVtxo = (v) => ({
12
12
  tapTree: toHex(v.tapTree),
13
13
  forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
14
14
  intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
15
- extraWitness: v.extraWitness?.map((w) => toHex(w)),
15
+ extraWitness: v.extraWitness?.map(toHex),
16
+ });
17
+ const serializeUtxo = (u) => ({
18
+ ...u,
19
+ tapTree: toHex(u.tapTree),
20
+ forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
21
+ intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
22
+ extraWitness: u.extraWitness?.map(toHex),
16
23
  });
17
24
  const deserializeTapLeaf = (t) => {
18
25
  const cb = TaprootControlBlock.decode(fromHex(t.cb));
@@ -24,13 +31,21 @@ const deserializeVtxo = (o) => ({
24
31
  tapTree: fromHex(o.tapTree),
25
32
  forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
26
33
  intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
27
- extraWitness: o.extraWitness?.map((w) => fromHex(w)),
34
+ extraWitness: o.extraWitness?.map(fromHex),
35
+ });
36
+ const deserializeUtxo = (o) => ({
37
+ ...o,
38
+ tapTree: fromHex(o.tapTree),
39
+ forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
40
+ intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
41
+ extraWitness: o.extraWitness?.map(fromHex),
28
42
  });
29
43
  export class WalletRepositoryImpl {
30
44
  constructor(storage) {
31
45
  this.storage = storage;
32
46
  this.cache = {
33
47
  vtxos: new Map(),
48
+ utxos: new Map(),
34
49
  transactions: new Map(),
35
50
  walletState: null,
36
51
  initialized: new Set(),
@@ -58,18 +73,6 @@ export class WalletRepositoryImpl {
58
73
  return [];
59
74
  }
60
75
  }
61
- async saveVtxo(address, vtxo) {
62
- const vtxos = await this.getVtxos(address);
63
- const existing = vtxos.findIndex((v) => v.txid === vtxo.txid && v.vout === vtxo.vout);
64
- if (existing !== -1) {
65
- vtxos[existing] = vtxo;
66
- }
67
- else {
68
- vtxos.push(vtxo);
69
- }
70
- this.cache.vtxos.set(address, vtxos.slice());
71
- await this.storage.setItem(`vtxos:${address}`, JSON.stringify(vtxos.map(serializeVtxo)));
72
- }
73
76
  async saveVtxos(address, vtxos) {
74
77
  const storedVtxos = await this.getVtxos(address);
75
78
  for (const vtxo of vtxos) {
@@ -95,6 +98,53 @@ export class WalletRepositoryImpl {
95
98
  this.cache.vtxos.set(address, []);
96
99
  await this.storage.removeItem(`vtxos:${address}`);
97
100
  }
101
+ async getUtxos(address) {
102
+ const cacheKey = `utxos:${address}`;
103
+ if (this.cache.utxos.has(address)) {
104
+ return this.cache.utxos.get(address);
105
+ }
106
+ const stored = await this.storage.getItem(cacheKey);
107
+ if (!stored) {
108
+ this.cache.utxos.set(address, []);
109
+ return [];
110
+ }
111
+ try {
112
+ const parsed = JSON.parse(stored);
113
+ const utxos = parsed.map(deserializeUtxo);
114
+ this.cache.utxos.set(address, utxos.slice());
115
+ return utxos.slice();
116
+ }
117
+ catch (error) {
118
+ console.error(`Failed to parse UTXOs for address ${address}:`, error);
119
+ this.cache.utxos.set(address, []);
120
+ return [];
121
+ }
122
+ }
123
+ async saveUtxos(address, utxos) {
124
+ const storedUtxos = await this.getUtxos(address);
125
+ utxos.forEach((utxo) => {
126
+ const existing = storedUtxos.findIndex((u) => u.txid === utxo.txid && u.vout === utxo.vout);
127
+ if (existing !== -1) {
128
+ storedUtxos[existing] = utxo;
129
+ }
130
+ else {
131
+ storedUtxos.push(utxo);
132
+ }
133
+ });
134
+ this.cache.utxos.set(address, storedUtxos.slice());
135
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
136
+ }
137
+ async removeUtxo(address, utxoId) {
138
+ const utxos = await this.getUtxos(address);
139
+ const [txid, vout] = utxoId.split(":");
140
+ const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
141
+ this.cache.utxos.set(address, filtered.slice());
142
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
143
+ }
144
+ async clearUtxos(address) {
145
+ this.cache.utxos.set(address, []);
146
+ await this.storage.removeItem(`utxos:${address}`);
147
+ }
98
148
  async getTransactionHistory(address) {
99
149
  const cacheKey = `tx:${address}`;
100
150
  if (this.cache.transactions.has(address)) {
@@ -116,20 +166,6 @@ export class WalletRepositoryImpl {
116
166
  return [];
117
167
  }
118
168
  }
119
- async saveTransaction(address, tx) {
120
- const transactions = await this.getTransactionHistory(address);
121
- const existing = transactions.findIndex((t) => t.key === tx.key);
122
- if (existing !== -1) {
123
- transactions[existing] = tx;
124
- }
125
- else {
126
- transactions.push(tx);
127
- }
128
- // Sort by createdAt descending
129
- transactions.sort((a, b) => b.createdAt - a.createdAt);
130
- this.cache.transactions.set(address, transactions);
131
- await this.storage.setItem(`tx:${address}`, JSON.stringify(transactions));
132
- }
133
169
  async saveTransactions(address, txs) {
134
170
  const storedTransactions = await this.getTransactionHistory(address);
135
171
  for (const tx of txs) {
@@ -1,11 +1,10 @@
1
- import { Script, Address, p2tr, taprootListToTree } from "@scure/btc-signer";
1
+ import { Script, Address, p2tr, taprootListToTree, TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer";
2
2
  import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
3
3
  import { PSBTOutput } from "@scure/btc-signer/psbt.js";
4
- import { TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer/utils.js";
5
4
  import { hex } from "@scure/base";
6
5
  import { ArkAddress } from './address.js';
7
6
  import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
8
- const TapTreeCoder = PSBTOutput.tapTree[2];
7
+ export const TapTreeCoder = PSBTOutput.tapTree[2];
9
8
  export function scriptFromTapLeafScript(leaf) {
10
9
  return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
11
10
  }
@@ -25,7 +24,16 @@ export class VtxoScript {
25
24
  }
26
25
  constructor(scripts) {
27
26
  this.scripts = scripts;
28
- const tapTree = taprootListToTree(scripts.map((script) => ({ script, leafVersion: TAP_LEAF_VERSION })));
27
+ // reverse the scripts if the number of scripts is odd
28
+ // this is to be compatible with arkd algorithm computing taproot tree from list of tapscripts
29
+ // the scripts must be reversed only HERE while we compute the tweaked public key
30
+ // but the original order should be preserved while encoding as taptree
31
+ // note: .slice().reverse() is used instead of .reverse() to avoid mutating the original array
32
+ const list = scripts.length % 2 !== 0 ? scripts.slice().reverse() : scripts;
33
+ const tapTree = taprootListToTree(list.map((script) => ({
34
+ script,
35
+ leafVersion: TAP_LEAF_VERSION,
36
+ })));
29
37
  const payment = p2tr(TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
30
38
  if (!payment.tapLeafScript ||
31
39
  payment.tapLeafScript.length !== scripts.length) {
@@ -1,6 +1,5 @@
1
1
  import * as bip68 from "bip68";
2
- import { Script, ScriptNum } from "@scure/btc-signer/script.js";
3
- import { p2tr_ms } from "@scure/btc-signer/payment.js";
2
+ import { Script, ScriptNum, p2tr_ms } from "@scure/btc-signer";
4
3
  import { hex } from "@scure/base";
5
4
  const MinimalScriptNum = ScriptNum(undefined, true);
6
5
  export var TapscriptType;
@@ -1,4 +1,4 @@
1
- import { Script } from "@scure/btc-signer/script.js";
1
+ import { Script } from "@scure/btc-signer";
2
2
  import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, MultisigTapscript, } from './tapscript.js';
3
3
  import { hex } from "@scure/base";
4
4
  import { VtxoScript } from './base.js';