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

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 (37) hide show
  1. package/README.md +51 -0
  2. package/dist/cjs/adapters/expo.js +8 -0
  3. package/dist/cjs/index.js +2 -1
  4. package/dist/cjs/providers/expoArk.js +237 -0
  5. package/dist/cjs/providers/expoIndexer.js +194 -0
  6. package/dist/cjs/providers/indexer.js +3 -1
  7. package/dist/cjs/script/base.js +16 -95
  8. package/dist/cjs/utils/arkTransaction.js +13 -0
  9. package/dist/cjs/wallet/index.js +1 -1
  10. package/dist/cjs/wallet/serviceWorker/utils.js +0 -9
  11. package/dist/cjs/wallet/serviceWorker/worker.js +14 -17
  12. package/dist/cjs/wallet/utils.js +11 -0
  13. package/dist/cjs/wallet/wallet.js +69 -51
  14. package/dist/esm/adapters/expo.js +3 -0
  15. package/dist/esm/index.js +2 -2
  16. package/dist/esm/providers/expoArk.js +200 -0
  17. package/dist/esm/providers/expoIndexer.js +157 -0
  18. package/dist/esm/providers/indexer.js +3 -1
  19. package/dist/esm/script/base.js +13 -92
  20. package/dist/esm/utils/arkTransaction.js +13 -1
  21. package/dist/esm/wallet/index.js +1 -1
  22. package/dist/esm/wallet/serviceWorker/utils.js +0 -8
  23. package/dist/esm/wallet/serviceWorker/worker.js +15 -18
  24. package/dist/esm/wallet/utils.js +8 -0
  25. package/dist/esm/wallet/wallet.js +70 -52
  26. package/dist/types/adapters/expo.d.ts +4 -0
  27. package/dist/types/index.d.ts +5 -5
  28. package/dist/types/providers/ark.d.ts +136 -2
  29. package/dist/types/providers/expoArk.d.ts +22 -0
  30. package/dist/types/providers/expoIndexer.d.ts +26 -0
  31. package/dist/types/providers/indexer.d.ts +8 -0
  32. package/dist/types/utils/arkTransaction.d.ts +3 -1
  33. package/dist/types/wallet/index.d.ts +44 -6
  34. package/dist/types/wallet/serviceWorker/utils.d.ts +0 -2
  35. package/dist/types/wallet/utils.d.ts +2 -0
  36. package/dist/types/wallet/wallet.d.ts +9 -1
  37. package/package.json +11 -2
@@ -0,0 +1,200 @@
1
+ import { RestArkProvider, isFetchTimeoutError, } from './ark.js';
2
+ /**
3
+ * Expo-compatible Ark provider implementation using expo/fetch for SSE support.
4
+ * This provider works specifically in React Native/Expo environments where
5
+ * standard EventSource is not available but expo/fetch provides SSE capabilities.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { ExpoArkProvider } from '@arkade-os/sdk/providers/expo';
10
+ *
11
+ * const provider = new ExpoArkProvider('https://ark.example.com');
12
+ * const info = await provider.getInfo();
13
+ * ```
14
+ */
15
+ export class ExpoArkProvider extends RestArkProvider {
16
+ constructor(serverUrl) {
17
+ super(serverUrl);
18
+ }
19
+ 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
+ }
33
+ const url = `${this.serverUrl}/v1/batch/events`;
34
+ const queryParams = topics.length > 0
35
+ ? `?${topics.map((topic) => `topics=${encodeURIComponent(topic)}`).join("&")}`
36
+ : "";
37
+ 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
+ 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;
63
+ }
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
+ }
101
+ }
102
+ catch (error) {
103
+ if (error instanceof Error && error.name === "AbortError") {
104
+ break;
105
+ }
106
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
107
+ // these timeouts are set by expo/fetch function
108
+ if (isFetchTimeoutError(error)) {
109
+ console.debug("Timeout error ignored");
110
+ continue;
111
+ }
112
+ console.error("Event stream error:", error);
113
+ throw error;
114
+ }
115
+ finally {
116
+ // Clean up the abort listener
117
+ signal?.removeEventListener("abort", cleanup);
118
+ }
119
+ }
120
+ }
121
+ 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
+ }
135
+ const url = `${this.serverUrl}/v1/txs`;
136
+ 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
+ try {
143
+ const response = await expoFetch(url, {
144
+ headers: {
145
+ Accept: "text/event-stream",
146
+ },
147
+ signal: fetchController.signal,
148
+ });
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
+ }
181
+ catch (error) {
182
+ if (error instanceof Error && error.name === "AbortError") {
183
+ break;
184
+ }
185
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
186
+ // these timeouts are set by expo/fetch function
187
+ if (isFetchTimeoutError(error)) {
188
+ console.debug("Timeout error ignored");
189
+ continue;
190
+ }
191
+ console.error("Address subscription error:", error);
192
+ throw error;
193
+ }
194
+ finally {
195
+ // Clean up the abort listener
196
+ signal?.removeEventListener("abort", cleanup);
197
+ }
198
+ }
199
+ }
200
+ }
@@ -0,0 +1,157 @@
1
+ import { RestIndexerProvider } from './indexer.js';
2
+ import { isFetchTimeoutError } from './ark.js';
3
+ // Helper function to convert Vtxo to VirtualCoin (same as in indexer.ts)
4
+ function convertVtxo(vtxo) {
5
+ return {
6
+ txid: vtxo.outpoint.txid,
7
+ vout: vtxo.outpoint.vout,
8
+ value: Number(vtxo.amount),
9
+ status: {
10
+ confirmed: !vtxo.isSwept && !vtxo.isPreconfirmed,
11
+ },
12
+ virtualStatus: {
13
+ state: vtxo.isSwept
14
+ ? "swept"
15
+ : vtxo.isPreconfirmed
16
+ ? "preconfirmed"
17
+ : "settled",
18
+ commitmentTxIds: vtxo.commitmentTxids,
19
+ batchExpiry: vtxo.expiresAt
20
+ ? Number(vtxo.expiresAt) * 1000
21
+ : undefined,
22
+ },
23
+ spentBy: vtxo.spentBy ?? "",
24
+ settledBy: vtxo.settledBy,
25
+ arkTxId: vtxo.arkTxid,
26
+ createdAt: new Date(Number(vtxo.createdAt) * 1000),
27
+ isUnrolled: vtxo.isUnrolled,
28
+ isSpent: vtxo.isSpent,
29
+ };
30
+ }
31
+ /**
32
+ * Expo-compatible Indexer provider implementation using expo/fetch for streaming support.
33
+ * This provider works specifically in React Native/Expo environments where
34
+ * standard fetch streaming may not work properly but expo/fetch provides streaming capabilities.
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { ExpoIndexerProvider } from '@arkade-os/sdk/adapters/expo';
39
+ *
40
+ * const provider = new ExpoIndexerProvider('https://indexer.example.com');
41
+ * const vtxos = await provider.getVtxos({ scripts: ['script1'] });
42
+ * ```
43
+ */
44
+ export class ExpoIndexerProvider extends RestIndexerProvider {
45
+ constructor(serverUrl) {
46
+ super(serverUrl);
47
+ }
48
+ async *getSubscription(subscriptionId, abortSignal) {
49
+ // Detect if we're running in React Native/Expo environment
50
+ const isReactNative = typeof navigator !== "undefined" &&
51
+ 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) {
61
+ // In React Native/Expo, expo/fetch is required for proper streaming support
62
+ if (isReactNative) {
63
+ throw new Error("expo/fetch is unavailable in React Native environment. " +
64
+ "Please ensure expo/fetch is installed and properly configured. " +
65
+ "Streaming support may not work with standard fetch in React Native.");
66
+ }
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
+ }
71
+ const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
72
+ while (!abortSignal.aborted) {
73
+ 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;
101
+ }
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
+ }
138
+ }
139
+ buffer = lines[lines.length - 1];
140
+ }
141
+ }
142
+ catch (error) {
143
+ if (error instanceof Error && error.name === "AbortError") {
144
+ break;
145
+ }
146
+ // ignore timeout errors, they're expected when the server is not sending anything for 5 min
147
+ // these timeouts are set by expo/fetch function
148
+ if (isFetchTimeoutError(error)) {
149
+ console.debug("Timeout error ignored");
150
+ continue;
151
+ }
152
+ console.error("Subscription error:", error);
153
+ throw error;
154
+ }
155
+ }
156
+ }
157
+ }
@@ -173,6 +173,7 @@ export class RestIndexerProvider {
173
173
  scripts: data.event.scripts || [],
174
174
  newVtxos: (data.event.newVtxos || []).map(convertVtxo),
175
175
  spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
176
+ sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
176
177
  tx: data.event.tx,
177
178
  checkpointTxs: data.event.checkpointTxs,
178
179
  };
@@ -326,7 +327,7 @@ export class RestIndexerProvider {
326
327
  });
327
328
  if (!res.ok) {
328
329
  const errorText = await res.text();
329
- throw new Error(`Failed to unsubscribe to scripts: ${errorText}`);
330
+ console.warn(`Failed to unsubscribe to scripts: ${errorText}`);
330
331
  }
331
332
  }
332
333
  }
@@ -354,6 +355,7 @@ function convertVtxo(vtxo) {
354
355
  arkTxId: vtxo.arkTxid,
355
356
  createdAt: new Date(Number(vtxo.createdAt) * 1000),
356
357
  isUnrolled: vtxo.isUnrolled,
358
+ isSpent: vtxo.isSpent,
357
359
  };
358
360
  }
359
361
  // Unexported namespace for type guards only
@@ -1,9 +1,11 @@
1
- import { Address, p2tr, TAP_LEAF_VERSION, taprootListToTree, } from "@scure/btc-signer/payment.js";
1
+ import { Script, Address, p2tr, taprootListToTree } from "@scure/btc-signer";
2
+ import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
3
+ import { PSBTOutput } from "@scure/btc-signer/psbt.js";
2
4
  import { TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer/utils.js";
3
- import { ArkAddress } from './address.js';
4
- import { Script } from "@scure/btc-signer/script.js";
5
5
  import { hex } from "@scure/base";
6
+ import { ArkAddress } from './address.js';
6
7
  import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
8
+ const TapTreeCoder = PSBTOutput.tapTree[2];
7
9
  export function scriptFromTapLeafScript(leaf) {
8
10
  return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
9
11
  }
@@ -17,8 +19,9 @@ export function scriptFromTapLeafScript(leaf) {
17
19
  */
18
20
  export class VtxoScript {
19
21
  static decode(tapTree) {
20
- const leaves = decodeTaprootTree(tapTree);
21
- return new VtxoScript(leaves);
22
+ const leaves = TapTreeCoder.decode(tapTree);
23
+ const scripts = leaves.map((leaf) => leaf.script);
24
+ return new VtxoScript(scripts);
22
25
  }
23
26
  constructor(scripts) {
24
27
  this.scripts = scripts;
@@ -32,7 +35,11 @@ export class VtxoScript {
32
35
  this.tweakedPublicKey = payment.tweakedPubkey;
33
36
  }
34
37
  encode() {
35
- const tapTree = encodeTaprootTree(this.scripts);
38
+ const tapTree = TapTreeCoder.encode(this.scripts.map((script) => ({
39
+ depth: 1,
40
+ version: TAP_LEAF_VERSION,
41
+ script,
42
+ })));
36
43
  return tapTree;
37
44
  }
38
45
  address(prefix, serverPubKey) {
@@ -75,89 +82,3 @@ export class VtxoScript {
75
82
  return paths;
76
83
  }
77
84
  }
78
- function decodeTaprootTree(tapTree) {
79
- let offset = 0;
80
- const scripts = [];
81
- // Read number of leaves
82
- const [numLeaves, numLeavesSize] = decodeCompactSizeUint(tapTree, offset);
83
- offset += numLeavesSize;
84
- // Read each leaf
85
- for (let i = 0; i < numLeaves; i++) {
86
- // Skip depth (1 byte)
87
- offset += 1;
88
- // Skip leaf version (1 byte)
89
- offset += 1;
90
- // Read script length
91
- const [scriptLength, scriptLengthSize] = decodeCompactSizeUint(tapTree, offset);
92
- offset += scriptLengthSize;
93
- // Read script content
94
- const script = tapTree.slice(offset, offset + scriptLength);
95
- scripts.push(script);
96
- offset += scriptLength;
97
- }
98
- return scripts;
99
- }
100
- function decodeCompactSizeUint(data, offset) {
101
- const firstByte = data[offset];
102
- if (firstByte < 0xfd) {
103
- return [firstByte, 1];
104
- }
105
- else if (firstByte === 0xfd) {
106
- const value = new DataView(data.buffer).getUint16(offset + 1, true);
107
- return [value, 3];
108
- }
109
- else if (firstByte === 0xfe) {
110
- const value = new DataView(data.buffer).getUint32(offset + 1, true);
111
- return [value, 5];
112
- }
113
- else {
114
- const value = Number(new DataView(data.buffer).getBigUint64(offset + 1, true));
115
- return [value, 9];
116
- }
117
- }
118
- function encodeTaprootTree(leaves) {
119
- const chunks = [];
120
- // Write number of leaves as compact size uint
121
- chunks.push(encodeCompactSizeUint(leaves.length));
122
- for (const tapscript of leaves) {
123
- // Write depth (always 1 for now)
124
- chunks.push(new Uint8Array([1]));
125
- // Write leaf version (0xc0 for tapscript)
126
- chunks.push(new Uint8Array([0xc0]));
127
- // Write script length and script
128
- chunks.push(encodeCompactSizeUint(tapscript.length));
129
- chunks.push(tapscript);
130
- }
131
- // Concatenate all chunks
132
- const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
133
- const result = new Uint8Array(totalLength);
134
- let offset = 0;
135
- for (const chunk of chunks) {
136
- result.set(chunk, offset);
137
- offset += chunk.length;
138
- }
139
- return result;
140
- }
141
- function encodeCompactSizeUint(value) {
142
- if (value < 0xfd) {
143
- return new Uint8Array([value]);
144
- }
145
- else if (value <= 0xffff) {
146
- const buffer = new Uint8Array(3);
147
- buffer[0] = 0xfd;
148
- new DataView(buffer.buffer).setUint16(1, value, true);
149
- return buffer;
150
- }
151
- else if (value <= 0xffffffff) {
152
- const buffer = new Uint8Array(5);
153
- buffer[0] = 0xfe;
154
- new DataView(buffer.buffer).setUint32(1, value, true);
155
- return buffer;
156
- }
157
- else {
158
- const buffer = new Uint8Array(9);
159
- buffer[0] = 0xff;
160
- new DataView(buffer.buffer).setBigUint64(1, BigInt(value), true);
161
- return buffer;
162
- }
163
- }
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_SEQUENCE, Transaction, } from "@scure/btc-signer/transaction.js";
2
- import { CLTVMultisigTapscript, decodeTapscript } from '../script/tapscript.js';
2
+ import { CLTVMultisigTapscript, decodeTapscript, } from '../script/tapscript.js';
3
3
  import { scriptFromTapLeafScript, VtxoScript, } from '../script/base.js';
4
4
  import { P2A } from './anchor.js';
5
5
  import { hex } from "@scure/base";
@@ -103,3 +103,15 @@ const nLocktimeMinSeconds = 500000000n;
103
103
  function isSeconds(locktime) {
104
104
  return locktime >= nLocktimeMinSeconds;
105
105
  }
106
+ export function hasBoardingTxExpired(coin, boardingTimelock) {
107
+ if (!coin.status.block_time)
108
+ return false;
109
+ if (boardingTimelock.value === 0n)
110
+ return true;
111
+ if (boardingTimelock.type !== "blocks")
112
+ return false; // TODO: handle get chain tip
113
+ // validate expiry in terms of seconds
114
+ const now = BigInt(Math.floor(Date.now() / 1000));
115
+ const blockTime = BigInt(Math.floor(coin.status.block_time));
116
+ return blockTime + boardingTimelock.value <= now;
117
+ }
@@ -4,7 +4,7 @@ export var TxType;
4
4
  TxType["TxReceived"] = "RECEIVED";
5
5
  })(TxType || (TxType = {}));
6
6
  export function isSpendable(vtxo) {
7
- return vtxo.spentBy === undefined || vtxo.spentBy === "";
7
+ return !vtxo.isSpent;
8
8
  }
9
9
  export function isRecoverable(vtxo) {
10
10
  return vtxo.virtualStatus.state === "swept" && isSpendable(vtxo);
@@ -44,11 +44,3 @@ export async function setupServiceWorker(path) {
44
44
  navigator.serviceWorker.addEventListener("error", onError);
45
45
  });
46
46
  }
47
- export function extendVirtualCoin(wallet, vtxo) {
48
- return {
49
- ...vtxo,
50
- forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
51
- intentTapLeafScript: wallet.offchainTapscript.exit(),
52
- tapTree: wallet.offchainTapscript.encode(),
53
- };
54
- }
@@ -1,6 +1,6 @@
1
1
  /// <reference lib="webworker" />
2
2
  import { SingleKey } from '../../identity/singleKey.js';
3
- import { isSpendable, isSubdust } from '../index.js';
3
+ import { isRecoverable, isSpendable, isSubdust } from '../index.js';
4
4
  import { Wallet } from '../wallet.js';
5
5
  import { Request } from './request.js';
6
6
  import { Response } from './response.js';
@@ -10,7 +10,7 @@ import { RestIndexerProvider } from '../../providers/indexer.js';
10
10
  import { hex } from "@scure/base";
11
11
  import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js';
12
12
  import { WalletRepositoryImpl, } from '../../repositories/walletRepository.js';
13
- import { extendVirtualCoin } from './utils.js';
13
+ import { extendVirtualCoin } from '../utils.js';
14
14
  /**
15
15
  * Worker is a class letting to interact with ServiceWorkerWallet from the client
16
16
  * it aims to be run in a service worker context
@@ -74,6 +74,8 @@ export class Worker {
74
74
  this.incomingFundsSubscription();
75
75
  // Clear storage - this replaces vtxoRepository.close()
76
76
  await this.storage.clear();
77
+ // Reset in-memory caches by recreating the repository
78
+ this.walletRepository = new WalletRepositoryImpl(this.storage);
77
79
  this.wallet = undefined;
78
80
  this.arkProvider = undefined;
79
81
  this.indexerProvider = undefined;
@@ -102,9 +104,6 @@ export class Worker {
102
104
  const txs = await this.wallet.getTransactionHistory();
103
105
  if (txs)
104
106
  await this.walletRepository.saveTransactions(address, txs);
105
- // stop previous subscriptions if any
106
- if (this.incomingFundsSubscription)
107
- this.incomingFundsSubscription();
108
107
  // subscribe for incoming funds and notify all clients when new funds arrive
109
108
  this.incomingFundsSubscription = await this.wallet.notifyIncomingFunds(async (funds) => {
110
109
  if (funds.type === "vtxo") {
@@ -124,7 +123,7 @@ export class Worker {
124
123
  // notify all clients about the vtxo update
125
124
  this.sendMessageToAllClients("VTXO_UPDATE", JSON.stringify({ newVtxos, spentVtxos }));
126
125
  }
127
- if (funds.type === "utxo" && funds.coins.length > 0) {
126
+ if (funds.type === "utxo") {
128
127
  // notify all clients about the utxo update
129
128
  this.sendMessageToAllClients("UTXO_UPDATE", JSON.stringify(funds.coins));
130
129
  }
@@ -355,17 +354,16 @@ export class Worker {
355
354
  if (!message.filter?.withRecoverable) {
356
355
  if (!this.wallet)
357
356
  throw new Error("Wallet not initialized");
358
- // exclude subdust is we don't want recoverable
359
- const dustAmount = this.wallet?.dustAmount;
360
- vtxos =
361
- dustAmount == null
362
- ? vtxos
363
- : vtxos.filter((v) => !isSubdust(v, dustAmount));
364
- }
365
- if (message.filter?.withRecoverable) {
366
- // get also swept and spendable vtxos
367
- const sweptVtxos = await this.getSweptVtxos();
368
- vtxos.push(...sweptVtxos.filter(isSpendable));
357
+ // exclude subdust and recoverable if we don't want recoverable
358
+ const notSubdust = (v) => {
359
+ const dustAmount = this.wallet?.dustAmount;
360
+ return dustAmount == null
361
+ ? true
362
+ : !isSubdust(v, dustAmount);
363
+ };
364
+ vtxos = vtxos
365
+ .filter(notSubdust)
366
+ .filter((v) => !isRecoverable(v));
369
367
  }
370
368
  event.source?.postMessage(Response.vtxos(message.id, vtxos));
371
369
  }
@@ -526,7 +524,6 @@ export class Worker {
526
524
  }
527
525
  async handleReloadWallet(event) {
528
526
  const message = event.data;
529
- console.log("RELOAD_WALLET message received", message);
530
527
  if (!Request.isReloadWallet(message)) {
531
528
  console.error("Invalid RELOAD_WALLET message format", message);
532
529
  event.source?.postMessage(Response.error(message.id, "Invalid RELOAD_WALLET message format"));
@@ -0,0 +1,8 @@
1
+ export function extendVirtualCoin(wallet, vtxo) {
2
+ return {
3
+ ...vtxo,
4
+ forfeitTapLeafScript: wallet.offchainTapscript.forfeit(),
5
+ intentTapLeafScript: wallet.offchainTapscript.exit(),
6
+ tapTree: wallet.offchainTapscript.encode(),
7
+ };
8
+ }