@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.
- package/README.md +51 -0
- package/dist/cjs/adapters/expo.js +8 -0
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/providers/expoArk.js +237 -0
- package/dist/cjs/providers/expoIndexer.js +194 -0
- package/dist/cjs/providers/indexer.js +3 -1
- package/dist/cjs/script/base.js +16 -95
- package/dist/cjs/utils/arkTransaction.js +13 -0
- package/dist/cjs/wallet/index.js +1 -1
- package/dist/cjs/wallet/serviceWorker/utils.js +0 -9
- package/dist/cjs/wallet/serviceWorker/worker.js +14 -17
- package/dist/cjs/wallet/utils.js +11 -0
- package/dist/cjs/wallet/wallet.js +69 -51
- package/dist/esm/adapters/expo.js +3 -0
- package/dist/esm/index.js +2 -2
- package/dist/esm/providers/expoArk.js +200 -0
- package/dist/esm/providers/expoIndexer.js +157 -0
- package/dist/esm/providers/indexer.js +3 -1
- package/dist/esm/script/base.js +13 -92
- package/dist/esm/utils/arkTransaction.js +13 -1
- package/dist/esm/wallet/index.js +1 -1
- package/dist/esm/wallet/serviceWorker/utils.js +0 -8
- package/dist/esm/wallet/serviceWorker/worker.js +15 -18
- package/dist/esm/wallet/utils.js +8 -0
- package/dist/esm/wallet/wallet.js +70 -52
- package/dist/types/adapters/expo.d.ts +4 -0
- package/dist/types/index.d.ts +5 -5
- package/dist/types/providers/ark.d.ts +136 -2
- package/dist/types/providers/expoArk.d.ts +22 -0
- package/dist/types/providers/expoIndexer.d.ts +26 -0
- package/dist/types/providers/indexer.d.ts +8 -0
- package/dist/types/utils/arkTransaction.d.ts +3 -1
- package/dist/types/wallet/index.d.ts +44 -6
- package/dist/types/wallet/serviceWorker/utils.d.ts +0 -2
- package/dist/types/wallet/utils.d.ts +2 -0
- package/dist/types/wallet/wallet.d.ts +9 -1
- 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
|
-
|
|
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
|
package/dist/esm/script/base.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Address, p2tr,
|
|
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 =
|
|
21
|
-
|
|
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 =
|
|
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
|
+
}
|
package/dist/esm/wallet/index.js
CHANGED
|
@@ -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.
|
|
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 '
|
|
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"
|
|
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
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
dustAmount == null
|
|
362
|
-
?
|
|
363
|
-
:
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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"));
|