@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,41 +1,9 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  exports.ExpoIndexerProvider = void 0;
37
4
  const indexer_1 = require("./indexer");
38
5
  const ark_1 = require("./ark");
6
+ const expoUtils_1 = require("./expoUtils");
39
7
  // Helper function to convert Vtxo to VirtualCoin (same as in indexer.ts)
40
8
  function convertVtxo(vtxo) {
41
9
  return {
@@ -85,95 +53,38 @@ class ExpoIndexerProvider extends indexer_1.RestIndexerProvider {
85
53
  // Detect if we're running in React Native/Expo environment
86
54
  const isReactNative = typeof navigator !== "undefined" &&
87
55
  navigator.product === "ReactNative";
88
- // Dynamic import to avoid bundling expo/fetch in non-Expo environments
89
- let expoFetch = fetch; // Default to standard fetch
90
- try {
91
- const expoFetchModule = await Promise.resolve().then(() => __importStar(require("expo/fetch")));
92
- // expo/fetch returns a compatible fetch function but with different types
93
- expoFetch = expoFetchModule.fetch;
94
- console.debug("Using expo/fetch for indexer subscription");
95
- }
96
- catch (error) {
56
+ const expoFetch = await (0, expoUtils_1.getExpoFetch)().catch((error) => {
97
57
  // In React Native/Expo, expo/fetch is required for proper streaming support
98
58
  if (isReactNative) {
99
59
  throw new Error("expo/fetch is unavailable in React Native environment. " +
100
60
  "Please ensure expo/fetch is installed and properly configured. " +
101
61
  "Streaming support may not work with standard fetch in React Native.");
102
62
  }
103
- // In non-RN environments, fall back to standard fetch but warn about potential streaming issues
104
- console.warn("Using standard fetch instead of expo/fetch. " +
105
- "Streaming may not be fully supported in some environments.", error);
106
- }
63
+ throw error;
64
+ });
107
65
  const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
108
66
  while (!abortSignal.aborted) {
109
67
  try {
110
- const res = await expoFetch(url, {
111
- headers: {
112
- Accept: "text/event-stream",
113
- "Content-Type": "application/json",
114
- },
115
- signal: abortSignal,
116
- });
117
- if (!res.ok) {
118
- throw new Error(`Unexpected status ${res.status} when subscribing to address updates`);
119
- }
120
- // Check if response is the expected content type
121
- const contentType = res.headers.get("content-type");
122
- if (contentType &&
123
- !contentType.includes("text/event-stream") &&
124
- !contentType.includes("application/json")) {
125
- throw new Error(`Unexpected content-type: ${contentType}. Expected text/event-stream or application/json`);
126
- }
127
- if (!res.body) {
128
- throw new Error("Response body is null");
129
- }
130
- const reader = res.body.getReader();
131
- const decoder = new TextDecoder();
132
- let buffer = "";
133
- while (!abortSignal.aborted) {
134
- const { done, value } = await reader.read();
135
- if (done) {
136
- break;
68
+ yield* (0, expoUtils_1.sseStreamIterator)(url, abortSignal, expoFetch, { "Content-Type": "application/json" }, (data) => {
69
+ // Handle new v8 proto format with heartbeat or event
70
+ if (data.heartbeat !== undefined) {
71
+ // Skip heartbeat messages
72
+ return null;
137
73
  }
138
- buffer += decoder.decode(value, { stream: true });
139
- const lines = buffer.split("\n");
140
- for (let i = 0; i < lines.length - 1; i++) {
141
- const line = lines[i].trim();
142
- if (!line)
143
- continue;
144
- try {
145
- // Parse SSE format: "data: {json}"
146
- if (line.startsWith("data:")) {
147
- const jsonStr = line.substring(5).trim();
148
- if (!jsonStr)
149
- continue;
150
- const data = JSON.parse(jsonStr);
151
- // Handle new v8 proto format with heartbeat or event
152
- if (data.heartbeat !== undefined) {
153
- // Skip heartbeat messages
154
- continue;
155
- }
156
- // Process event messages
157
- if (data.event) {
158
- yield {
159
- txid: data.event.txid,
160
- scripts: data.event.scripts || [],
161
- newVtxos: (data.event.newVtxos || []).map(convertVtxo),
162
- spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
163
- sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
164
- tx: data.event.tx,
165
- checkpointTxs: data.event.checkpointTxs,
166
- };
167
- }
168
- }
169
- }
170
- catch (parseError) {
171
- console.error("Failed to parse subscription response:", parseError);
172
- throw parseError;
173
- }
74
+ // Process event messages
75
+ if (data.event) {
76
+ return {
77
+ txid: data.event.txid,
78
+ scripts: data.event.scripts || [],
79
+ newVtxos: (data.event.newVtxos || []).map(convertVtxo),
80
+ spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
81
+ sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
82
+ tx: data.event.tx,
83
+ checkpointTxs: data.event.checkpointTxs,
84
+ };
174
85
  }
175
- buffer = lines[lines.length - 1];
176
- }
86
+ return null;
87
+ });
177
88
  }
178
89
  catch (error) {
179
90
  if (error instanceof Error && error.name === "AbortError") {
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.getExpoFetch = getExpoFetch;
37
+ exports.sseStreamIterator = sseStreamIterator;
38
+ /**
39
+ * Dynamically imports expo/fetch with fallback to standard fetch.
40
+ * @returns A fetch function suitable for SSE streaming
41
+ */
42
+ async function getExpoFetch(options) {
43
+ const requireExpo = options?.requireExpo ?? false;
44
+ try {
45
+ const expoFetchModule = await Promise.resolve().then(() => __importStar(require("expo/fetch")));
46
+ console.debug("Using expo/fetch for streaming");
47
+ return expoFetchModule.fetch;
48
+ }
49
+ catch (error) {
50
+ if (requireExpo) {
51
+ throw new Error("expo/fetch is unavailable in this environment. " +
52
+ "Please ensure expo/fetch is installed and properly configured.");
53
+ }
54
+ console.warn("Using standard fetch instead of expo/fetch. " +
55
+ "Streaming may not be fully supported in some environments.", error);
56
+ return fetch;
57
+ }
58
+ }
59
+ /**
60
+ * Generic SSE stream processor using fetch API with ReadableStream.
61
+ * Handles SSE format parsing, buffer management, and abort signals.
62
+ *
63
+ * @param url - The SSE endpoint URL
64
+ * @param abortSignal - Signal to abort the stream
65
+ * @param fetchFn - Fetch function to use (defaults to standard fetch)
66
+ * @param headers - Additional headers to send
67
+ * @param parseData - Function to parse and yield data from SSE events
68
+ */
69
+ async function* sseStreamIterator(url, abortSignal, fetchFn, headers, parseData) {
70
+ const fetchController = new AbortController();
71
+ const cleanup = () => fetchController.abort();
72
+ abortSignal?.addEventListener("abort", cleanup, { once: true });
73
+ try {
74
+ const response = await fetchFn(url, {
75
+ headers: {
76
+ Accept: "text/event-stream",
77
+ ...headers,
78
+ },
79
+ signal: fetchController.signal,
80
+ });
81
+ if (!response.ok) {
82
+ throw new Error(`Unexpected status ${response.status} when fetching SSE stream`);
83
+ }
84
+ if (!response.body) {
85
+ throw new Error("Response body is null");
86
+ }
87
+ const reader = response.body.getReader();
88
+ const decoder = new TextDecoder();
89
+ let buffer = "";
90
+ while (!abortSignal?.aborted) {
91
+ const { done, value } = await reader.read();
92
+ if (done) {
93
+ break;
94
+ }
95
+ buffer += decoder.decode(value, { stream: true });
96
+ const lines = buffer.split("\n");
97
+ for (let i = 0; i < lines.length - 1; i++) {
98
+ const line = lines[i].trim();
99
+ if (!line)
100
+ continue;
101
+ if (line.startsWith("data:")) {
102
+ const jsonStr = line.substring(5).trim();
103
+ if (!jsonStr)
104
+ continue;
105
+ try {
106
+ const data = JSON.parse(jsonStr);
107
+ const parsed = parseData(data);
108
+ if (parsed !== null) {
109
+ yield parsed;
110
+ }
111
+ }
112
+ catch (parseError) {
113
+ console.error("Failed to parse SSE data:", parseError);
114
+ throw parseError;
115
+ }
116
+ }
117
+ }
118
+ buffer = lines[lines.length - 1];
119
+ }
120
+ }
121
+ finally {
122
+ abortSignal?.removeEventListener("abort", cleanup);
123
+ }
124
+ }
@@ -21,9 +21,10 @@ exports.ESPLORA_URL = {
21
21
  * ```
22
22
  */
23
23
  class EsploraProvider {
24
- constructor(baseUrl) {
24
+ constructor(baseUrl, opts) {
25
25
  this.baseUrl = baseUrl;
26
- this.polling = false;
26
+ this.pollingInterval = opts?.pollingInterval ?? 15000;
27
+ this.forcePolling = opts?.forcePolling ?? false;
27
28
  }
28
29
  async getCoins(address) {
29
30
  const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
@@ -94,13 +95,9 @@ class EsploraProvider {
94
95
  let intervalId = null;
95
96
  const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
96
97
  const poll = async () => {
97
- if (this.polling)
98
- return;
99
- this.polling = true;
100
- // websocket is not reliable, so we will fallback to polling
101
- const pollingInterval = 5000; // 5 seconds
102
- const getAllTxs = () => {
103
- return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
98
+ const getAllTxs = async () => {
99
+ const txArrays = await Promise.all(addresses.map((address) => this.getTransactions(address)));
100
+ return txArrays.flat();
104
101
  };
105
102
  // initial fetch to get existing transactions
106
103
  const initialTxs = await getAllTxs();
@@ -125,9 +122,19 @@ class EsploraProvider {
125
122
  catch (error) {
126
123
  console.error("Error in polling mechanism:", error);
127
124
  }
128
- }, pollingInterval);
125
+ }, this.pollingInterval);
129
126
  };
130
127
  let ws = null;
128
+ const stopFunc = () => {
129
+ if (ws)
130
+ ws.close();
131
+ if (intervalId)
132
+ clearInterval(intervalId);
133
+ };
134
+ if (this.forcePolling) {
135
+ await poll();
136
+ return stopFunc;
137
+ }
131
138
  try {
132
139
  ws = new WebSocket(wsUrl);
133
140
  ws.addEventListener("open", () => {
@@ -174,13 +181,6 @@ class EsploraProvider {
174
181
  // if websocket is not available, fallback to polling
175
182
  await poll();
176
183
  }
177
- const stopFunc = () => {
178
- if (ws && ws.readyState === WebSocket.OPEN)
179
- ws.close();
180
- if (intervalId)
181
- clearInterval(intervalId);
182
- this.polling = false;
183
- };
184
184
  return stopFunc;
185
185
  }
186
186
  async getChainTip() {
@@ -249,8 +249,7 @@ const isExplorerTransaction = (tx) => {
249
249
  return (typeof tx.txid === "string" &&
250
250
  Array.isArray(tx.vout) &&
251
251
  tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
252
- typeof vout.value === "string") &&
252
+ typeof vout.value === "number") &&
253
253
  typeof tx.status === "object" &&
254
- typeof tx.status.confirmed === "boolean" &&
255
- typeof tx.status.block_time === "number");
254
+ typeof tx.status.confirmed === "boolean");
256
255
  };
@@ -15,7 +15,14 @@ const serializeVtxo = (v) => ({
15
15
  tapTree: toHex(v.tapTree),
16
16
  forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
17
17
  intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
18
- extraWitness: v.extraWitness?.map((w) => toHex(w)),
18
+ extraWitness: v.extraWitness?.map(toHex),
19
+ });
20
+ const serializeUtxo = (u) => ({
21
+ ...u,
22
+ tapTree: toHex(u.tapTree),
23
+ forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
24
+ intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
25
+ extraWitness: u.extraWitness?.map(toHex),
19
26
  });
20
27
  const deserializeTapLeaf = (t) => {
21
28
  const cb = btc_signer_1.TaprootControlBlock.decode(fromHex(t.cb));
@@ -27,13 +34,21 @@ const deserializeVtxo = (o) => ({
27
34
  tapTree: fromHex(o.tapTree),
28
35
  forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
29
36
  intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
30
- extraWitness: o.extraWitness?.map((w) => fromHex(w)),
37
+ extraWitness: o.extraWitness?.map(fromHex),
38
+ });
39
+ const deserializeUtxo = (o) => ({
40
+ ...o,
41
+ tapTree: fromHex(o.tapTree),
42
+ forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
43
+ intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
44
+ extraWitness: o.extraWitness?.map(fromHex),
31
45
  });
32
46
  class WalletRepositoryImpl {
33
47
  constructor(storage) {
34
48
  this.storage = storage;
35
49
  this.cache = {
36
50
  vtxos: new Map(),
51
+ utxos: new Map(),
37
52
  transactions: new Map(),
38
53
  walletState: null,
39
54
  initialized: new Set(),
@@ -61,18 +76,6 @@ class WalletRepositoryImpl {
61
76
  return [];
62
77
  }
63
78
  }
64
- async saveVtxo(address, vtxo) {
65
- const vtxos = await this.getVtxos(address);
66
- const existing = vtxos.findIndex((v) => v.txid === vtxo.txid && v.vout === vtxo.vout);
67
- if (existing !== -1) {
68
- vtxos[existing] = vtxo;
69
- }
70
- else {
71
- vtxos.push(vtxo);
72
- }
73
- this.cache.vtxos.set(address, vtxos.slice());
74
- await this.storage.setItem(`vtxos:${address}`, JSON.stringify(vtxos.map(serializeVtxo)));
75
- }
76
79
  async saveVtxos(address, vtxos) {
77
80
  const storedVtxos = await this.getVtxos(address);
78
81
  for (const vtxo of vtxos) {
@@ -98,6 +101,53 @@ class WalletRepositoryImpl {
98
101
  this.cache.vtxos.set(address, []);
99
102
  await this.storage.removeItem(`vtxos:${address}`);
100
103
  }
104
+ async getUtxos(address) {
105
+ const cacheKey = `utxos:${address}`;
106
+ if (this.cache.utxos.has(address)) {
107
+ return this.cache.utxos.get(address);
108
+ }
109
+ const stored = await this.storage.getItem(cacheKey);
110
+ if (!stored) {
111
+ this.cache.utxos.set(address, []);
112
+ return [];
113
+ }
114
+ try {
115
+ const parsed = JSON.parse(stored);
116
+ const utxos = parsed.map(deserializeUtxo);
117
+ this.cache.utxos.set(address, utxos.slice());
118
+ return utxos.slice();
119
+ }
120
+ catch (error) {
121
+ console.error(`Failed to parse UTXOs for address ${address}:`, error);
122
+ this.cache.utxos.set(address, []);
123
+ return [];
124
+ }
125
+ }
126
+ async saveUtxos(address, utxos) {
127
+ const storedUtxos = await this.getUtxos(address);
128
+ utxos.forEach((utxo) => {
129
+ const existing = storedUtxos.findIndex((u) => u.txid === utxo.txid && u.vout === utxo.vout);
130
+ if (existing !== -1) {
131
+ storedUtxos[existing] = utxo;
132
+ }
133
+ else {
134
+ storedUtxos.push(utxo);
135
+ }
136
+ });
137
+ this.cache.utxos.set(address, storedUtxos.slice());
138
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
139
+ }
140
+ async removeUtxo(address, utxoId) {
141
+ const utxos = await this.getUtxos(address);
142
+ const [txid, vout] = utxoId.split(":");
143
+ const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
144
+ this.cache.utxos.set(address, filtered.slice());
145
+ await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
146
+ }
147
+ async clearUtxos(address) {
148
+ this.cache.utxos.set(address, []);
149
+ await this.storage.removeItem(`utxos:${address}`);
150
+ }
101
151
  async getTransactionHistory(address) {
102
152
  const cacheKey = `tx:${address}`;
103
153
  if (this.cache.transactions.has(address)) {
@@ -119,20 +169,6 @@ class WalletRepositoryImpl {
119
169
  return [];
120
170
  }
121
171
  }
122
- async saveTransaction(address, tx) {
123
- const transactions = await this.getTransactionHistory(address);
124
- const existing = transactions.findIndex((t) => t.key === tx.key);
125
- if (existing !== -1) {
126
- transactions[existing] = tx;
127
- }
128
- else {
129
- transactions.push(tx);
130
- }
131
- // Sort by createdAt descending
132
- transactions.sort((a, b) => b.createdAt - a.createdAt);
133
- this.cache.transactions.set(address, transactions);
134
- await this.storage.setItem(`tx:${address}`, JSON.stringify(transactions));
135
- }
136
172
  async saveTransactions(address, txs) {
137
173
  const storedTransactions = await this.getTransactionHistory(address);
138
174
  for (const tx of txs) {
@@ -1,15 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.VtxoScript = void 0;
3
+ exports.VtxoScript = exports.TapTreeCoder = void 0;
4
4
  exports.scriptFromTapLeafScript = scriptFromTapLeafScript;
5
5
  const btc_signer_1 = require("@scure/btc-signer");
6
6
  const payment_js_1 = require("@scure/btc-signer/payment.js");
7
7
  const psbt_js_1 = require("@scure/btc-signer/psbt.js");
8
- const utils_js_1 = require("@scure/btc-signer/utils.js");
9
8
  const base_1 = require("@scure/base");
10
9
  const address_1 = require("./address");
11
10
  const tapscript_1 = require("./tapscript");
12
- const TapTreeCoder = psbt_js_1.PSBTOutput.tapTree[2];
11
+ exports.TapTreeCoder = psbt_js_1.PSBTOutput.tapTree[2];
13
12
  function scriptFromTapLeafScript(leaf) {
14
13
  return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
15
14
  }
@@ -23,14 +22,23 @@ function scriptFromTapLeafScript(leaf) {
23
22
  */
24
23
  class VtxoScript {
25
24
  static decode(tapTree) {
26
- const leaves = TapTreeCoder.decode(tapTree);
25
+ const leaves = exports.TapTreeCoder.decode(tapTree);
27
26
  const scripts = leaves.map((leaf) => leaf.script);
28
27
  return new VtxoScript(scripts);
29
28
  }
30
29
  constructor(scripts) {
31
30
  this.scripts = scripts;
32
- const tapTree = (0, btc_signer_1.taprootListToTree)(scripts.map((script) => ({ script, leafVersion: payment_js_1.TAP_LEAF_VERSION })));
33
- const payment = (0, btc_signer_1.p2tr)(utils_js_1.TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
31
+ // reverse the scripts if the number of scripts is odd
32
+ // this is to be compatible with arkd algorithm computing taproot tree from list of tapscripts
33
+ // the scripts must be reversed only HERE while we compute the tweaked public key
34
+ // but the original order should be preserved while encoding as taptree
35
+ // note: .slice().reverse() is used instead of .reverse() to avoid mutating the original array
36
+ const list = scripts.length % 2 !== 0 ? scripts.slice().reverse() : scripts;
37
+ const tapTree = (0, btc_signer_1.taprootListToTree)(list.map((script) => ({
38
+ script,
39
+ leafVersion: payment_js_1.TAP_LEAF_VERSION,
40
+ })));
41
+ const payment = (0, btc_signer_1.p2tr)(btc_signer_1.TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
34
42
  if (!payment.tapLeafScript ||
35
43
  payment.tapLeafScript.length !== scripts.length) {
36
44
  throw new Error("invalid scripts");
@@ -39,7 +47,7 @@ class VtxoScript {
39
47
  this.tweakedPublicKey = payment.tweakedPubkey;
40
48
  }
41
49
  encode() {
42
- const tapTree = TapTreeCoder.encode(this.scripts.map((script) => ({
50
+ const tapTree = exports.TapTreeCoder.encode(this.scripts.map((script) => ({
43
51
  depth: 1,
44
52
  version: payment_js_1.TAP_LEAF_VERSION,
45
53
  script,