@1sat/wallet-toolbox 0.0.6 → 0.0.7
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/dist/OneSatWallet.d.ts +40 -17
- package/dist/OneSatWallet.js +956 -0
- package/dist/errors.js +11 -0
- package/dist/index.d.ts +0 -2
- package/dist/index.js +12 -93764
- package/dist/indexers/Bsv21Indexer.js +232 -0
- package/dist/indexers/CosignIndexer.js +25 -0
- package/dist/indexers/FundIndexer.js +64 -0
- package/dist/indexers/InscriptionIndexer.js +115 -0
- package/dist/indexers/LockIndexer.js +42 -0
- package/dist/indexers/MapIndexer.js +62 -0
- package/dist/indexers/OpNSIndexer.js +38 -0
- package/dist/indexers/OrdLockIndexer.js +63 -0
- package/dist/indexers/OriginIndexer.js +240 -0
- package/dist/indexers/Outpoint.js +53 -0
- package/dist/indexers/SigmaIndexer.js +133 -0
- package/dist/indexers/index.js +13 -0
- package/dist/indexers/parseAddress.js +24 -0
- package/dist/indexers/types.js +18 -0
- package/dist/services/OneSatServices.d.ts +12 -4
- package/dist/services/OneSatServices.js +231 -0
- package/dist/services/client/ArcadeClient.js +107 -0
- package/dist/services/client/BaseClient.js +125 -0
- package/dist/services/client/BeefClient.js +33 -0
- package/dist/services/client/Bsv21Client.js +65 -0
- package/dist/services/client/ChaintracksClient.js +175 -0
- package/dist/services/client/OrdfsClient.js +122 -0
- package/dist/services/client/OwnerClient.js +123 -0
- package/dist/services/client/TxoClient.js +85 -0
- package/dist/services/client/index.js +8 -0
- package/dist/services/types.js +5 -0
- package/dist/signers/ReadOnlySigner.js +47 -0
- package/dist/sync/IndexedDbSyncQueue.js +355 -0
- package/dist/sync/SqliteSyncQueue.js +197 -0
- package/dist/sync/index.js +3 -0
- package/dist/sync/types.js +4 -0
- package/package.json +5 -5
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
const QUEUE_STORE = "queue";
|
|
2
|
+
const STATE_STORE = "state";
|
|
3
|
+
const STATE_KEY = "syncState";
|
|
4
|
+
const DB_VERSION = 1;
|
|
5
|
+
/**
|
|
6
|
+
* IndexedDB implementation of SyncQueueStorage for browser environments.
|
|
7
|
+
*/
|
|
8
|
+
export class IndexedDbSyncQueue {
|
|
9
|
+
dbName;
|
|
10
|
+
db = null;
|
|
11
|
+
dbPromise = null;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new IndexedDB sync queue.
|
|
14
|
+
* @param accountId - Unique identifier for the account (e.g., address, pubkey hash)
|
|
15
|
+
*/
|
|
16
|
+
constructor(accountId) {
|
|
17
|
+
this.dbName = `sync-queue-${accountId}`;
|
|
18
|
+
}
|
|
19
|
+
async getDb() {
|
|
20
|
+
if (this.db)
|
|
21
|
+
return this.db;
|
|
22
|
+
if (this.dbPromise)
|
|
23
|
+
return this.dbPromise;
|
|
24
|
+
this.dbPromise = new Promise((resolve, reject) => {
|
|
25
|
+
const request = indexedDB.open(this.dbName, DB_VERSION);
|
|
26
|
+
request.onerror = () => reject(request.error);
|
|
27
|
+
request.onblocked = () => {
|
|
28
|
+
reject(new Error("Database blocked - close other tabs"));
|
|
29
|
+
};
|
|
30
|
+
request.onsuccess = () => {
|
|
31
|
+
this.db = request.result;
|
|
32
|
+
resolve(this.db);
|
|
33
|
+
};
|
|
34
|
+
request.onupgradeneeded = (event) => {
|
|
35
|
+
const db = event.target.result;
|
|
36
|
+
// Queue store with indexes
|
|
37
|
+
if (!db.objectStoreNames.contains(QUEUE_STORE)) {
|
|
38
|
+
const queueStore = db.createObjectStore(QUEUE_STORE, {
|
|
39
|
+
keyPath: "id",
|
|
40
|
+
});
|
|
41
|
+
queueStore.createIndex("status", "status", { unique: false });
|
|
42
|
+
queueStore.createIndex("outpoint", "outpoint", { unique: false });
|
|
43
|
+
}
|
|
44
|
+
// State store (simple key-value)
|
|
45
|
+
if (!db.objectStoreNames.contains(STATE_STORE)) {
|
|
46
|
+
db.createObjectStore(STATE_STORE, { keyPath: "key" });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return this.dbPromise;
|
|
51
|
+
}
|
|
52
|
+
async enqueue(items) {
|
|
53
|
+
if (items.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const db = await this.getDb();
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
59
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
60
|
+
tx.oncomplete = () => resolve();
|
|
61
|
+
tx.onerror = () => reject(tx.error);
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const id = `${item.outpoint}:${item.score}`;
|
|
64
|
+
// Check if item already exists
|
|
65
|
+
const getRequest = store.get(id);
|
|
66
|
+
getRequest.onsuccess = () => {
|
|
67
|
+
const existing = getRequest.result;
|
|
68
|
+
// Skip if already done
|
|
69
|
+
if (existing?.status === "done") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const queueItem = {
|
|
73
|
+
id,
|
|
74
|
+
outpoint: item.outpoint,
|
|
75
|
+
score: item.score,
|
|
76
|
+
spendTxid: item.spendTxid,
|
|
77
|
+
status: "pending",
|
|
78
|
+
attempts: existing?.attempts ?? 0,
|
|
79
|
+
createdAt: existing?.createdAt ?? now,
|
|
80
|
+
updatedAt: now,
|
|
81
|
+
};
|
|
82
|
+
store.put(queueItem);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
async claim(count = 1) {
|
|
88
|
+
const db = await this.getDb();
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
// Step 1: Find up to `count` pending items as seeds
|
|
91
|
+
const seedItems = await new Promise((resolve, reject) => {
|
|
92
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
93
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
94
|
+
const index = store.index("status");
|
|
95
|
+
const seeds = [];
|
|
96
|
+
const request = index.openCursor(IDBKeyRange.only("pending"));
|
|
97
|
+
request.onsuccess = () => {
|
|
98
|
+
const cursor = request.result;
|
|
99
|
+
if (cursor && seeds.length < count) {
|
|
100
|
+
seeds.push(cursor.value);
|
|
101
|
+
cursor.continue();
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
tx.oncomplete = () => resolve(seeds);
|
|
105
|
+
tx.onerror = () => reject(tx.error);
|
|
106
|
+
});
|
|
107
|
+
if (seedItems.length === 0) {
|
|
108
|
+
return new Map();
|
|
109
|
+
}
|
|
110
|
+
// Step 2: Get unique txids from seeds
|
|
111
|
+
const txids = new Set();
|
|
112
|
+
for (const item of seedItems) {
|
|
113
|
+
txids.add(item.outpoint.substring(0, 64));
|
|
114
|
+
}
|
|
115
|
+
// Step 3: For each txid, get ALL pending items (not just seeds)
|
|
116
|
+
const byTxid = new Map();
|
|
117
|
+
for (const txid of txids) {
|
|
118
|
+
const items = await this.getPendingByTxid(txid);
|
|
119
|
+
if (items.length > 0) {
|
|
120
|
+
byTxid.set(txid, items);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Step 4: Mark all gathered items as processing
|
|
124
|
+
const allItems = Array.from(byTxid.values()).flat();
|
|
125
|
+
await this.markProcessing(allItems, now);
|
|
126
|
+
return byTxid;
|
|
127
|
+
}
|
|
128
|
+
async getPendingByTxid(txid) {
|
|
129
|
+
const db = await this.getDb();
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
132
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
133
|
+
const index = store.index("outpoint");
|
|
134
|
+
const results = [];
|
|
135
|
+
const request = index.openCursor();
|
|
136
|
+
request.onsuccess = () => {
|
|
137
|
+
const cursor = request.result;
|
|
138
|
+
if (cursor) {
|
|
139
|
+
const item = cursor.value;
|
|
140
|
+
if (item.outpoint.startsWith(txid) && item.status === "pending") {
|
|
141
|
+
results.push(item);
|
|
142
|
+
}
|
|
143
|
+
cursor.continue();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
tx.oncomplete = () => resolve(results);
|
|
147
|
+
tx.onerror = () => reject(tx.error);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async markProcessing(items, now) {
|
|
151
|
+
if (items.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const db = await this.getDb();
|
|
154
|
+
return new Promise((resolve, reject) => {
|
|
155
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
156
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
157
|
+
tx.oncomplete = () => resolve();
|
|
158
|
+
tx.onerror = () => reject(tx.error);
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
item.status = "processing";
|
|
161
|
+
item.attempts += 1;
|
|
162
|
+
item.updatedAt = now;
|
|
163
|
+
store.put(item);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async complete(id) {
|
|
168
|
+
return this.completeMany([id]);
|
|
169
|
+
}
|
|
170
|
+
async completeMany(ids) {
|
|
171
|
+
if (ids.length === 0)
|
|
172
|
+
return;
|
|
173
|
+
const db = await this.getDb();
|
|
174
|
+
const now = Date.now();
|
|
175
|
+
return new Promise((resolve, reject) => {
|
|
176
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
177
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
178
|
+
tx.oncomplete = () => resolve();
|
|
179
|
+
tx.onerror = () => reject(tx.error);
|
|
180
|
+
for (const id of ids) {
|
|
181
|
+
const request = store.get(id);
|
|
182
|
+
request.onsuccess = () => {
|
|
183
|
+
const item = request.result;
|
|
184
|
+
if (item) {
|
|
185
|
+
item.status = "done";
|
|
186
|
+
item.updatedAt = now;
|
|
187
|
+
store.put(item);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async fail(id, error) {
|
|
194
|
+
const db = await this.getDb();
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
198
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
199
|
+
const request = store.get(id);
|
|
200
|
+
request.onsuccess = () => {
|
|
201
|
+
const item = request.result;
|
|
202
|
+
if (item) {
|
|
203
|
+
item.status = "failed";
|
|
204
|
+
item.lastError = error;
|
|
205
|
+
item.updatedAt = now;
|
|
206
|
+
store.put(item);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
tx.oncomplete = () => resolve();
|
|
210
|
+
tx.onerror = () => reject(tx.error);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async getByTxid(txid) {
|
|
214
|
+
const db = await this.getDb();
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
217
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
218
|
+
const index = store.index("outpoint");
|
|
219
|
+
const results = [];
|
|
220
|
+
// Use a cursor to find all outpoints starting with this txid
|
|
221
|
+
const request = index.openCursor();
|
|
222
|
+
request.onsuccess = () => {
|
|
223
|
+
const cursor = request.result;
|
|
224
|
+
if (cursor) {
|
|
225
|
+
const item = cursor.value;
|
|
226
|
+
if (item.outpoint.startsWith(txid)) {
|
|
227
|
+
results.push(item);
|
|
228
|
+
}
|
|
229
|
+
cursor.continue();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
tx.oncomplete = () => resolve(results);
|
|
233
|
+
tx.onerror = () => reject(tx.error);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
async getByStatus(status, limit = 100) {
|
|
237
|
+
const db = await this.getDb();
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
240
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
241
|
+
const index = store.index("status");
|
|
242
|
+
const results = [];
|
|
243
|
+
const request = index.openCursor(IDBKeyRange.only(status));
|
|
244
|
+
request.onsuccess = () => {
|
|
245
|
+
const cursor = request.result;
|
|
246
|
+
if (cursor && results.length < limit) {
|
|
247
|
+
results.push(cursor.value);
|
|
248
|
+
cursor.continue();
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
tx.oncomplete = () => resolve(results);
|
|
252
|
+
tx.onerror = () => reject(tx.error);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
async getStats() {
|
|
256
|
+
const db = await this.getDb();
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const tx = db.transaction(QUEUE_STORE, "readonly");
|
|
259
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
260
|
+
const request = store.openCursor();
|
|
261
|
+
const txidsByStatus = {
|
|
262
|
+
pending: new Set(),
|
|
263
|
+
processing: new Set(),
|
|
264
|
+
done: new Set(),
|
|
265
|
+
failed: new Set(),
|
|
266
|
+
};
|
|
267
|
+
request.onsuccess = () => {
|
|
268
|
+
const cursor = request.result;
|
|
269
|
+
if (cursor) {
|
|
270
|
+
const item = cursor.value;
|
|
271
|
+
const txid = item.outpoint.substring(0, 64);
|
|
272
|
+
txidsByStatus[item.status]?.add(txid);
|
|
273
|
+
cursor.continue();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
resolve({
|
|
277
|
+
pending: txidsByStatus.pending.size,
|
|
278
|
+
processing: txidsByStatus.processing.size,
|
|
279
|
+
done: txidsByStatus.done.size,
|
|
280
|
+
failed: txidsByStatus.failed.size,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
request.onerror = () => reject(request.error);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async getState() {
|
|
288
|
+
const db = await this.getDb();
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
const tx = db.transaction(STATE_STORE, "readonly");
|
|
291
|
+
const store = tx.objectStore(STATE_STORE);
|
|
292
|
+
const request = store.get(STATE_KEY);
|
|
293
|
+
request.onsuccess = () => {
|
|
294
|
+
const result = request.result;
|
|
295
|
+
resolve(result?.value ?? { lastQueuedScore: 0 });
|
|
296
|
+
};
|
|
297
|
+
request.onerror = () => reject(request.error);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async setState(state) {
|
|
301
|
+
const db = await this.getDb();
|
|
302
|
+
const current = await this.getState();
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
const tx = db.transaction(STATE_STORE, "readwrite");
|
|
305
|
+
const store = tx.objectStore(STATE_STORE);
|
|
306
|
+
store.put({
|
|
307
|
+
key: STATE_KEY,
|
|
308
|
+
value: { ...current, ...state },
|
|
309
|
+
});
|
|
310
|
+
tx.oncomplete = () => resolve();
|
|
311
|
+
tx.onerror = () => reject(tx.error);
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async resetProcessing() {
|
|
315
|
+
const db = await this.getDb();
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
return new Promise((resolve, reject) => {
|
|
318
|
+
const tx = db.transaction(QUEUE_STORE, "readwrite");
|
|
319
|
+
const store = tx.objectStore(QUEUE_STORE);
|
|
320
|
+
const index = store.index("status");
|
|
321
|
+
let count = 0;
|
|
322
|
+
const request = index.openCursor(IDBKeyRange.only("processing"));
|
|
323
|
+
request.onsuccess = () => {
|
|
324
|
+
const cursor = request.result;
|
|
325
|
+
if (cursor) {
|
|
326
|
+
const item = cursor.value;
|
|
327
|
+
item.status = "pending";
|
|
328
|
+
item.updatedAt = now;
|
|
329
|
+
cursor.update(item);
|
|
330
|
+
count++;
|
|
331
|
+
cursor.continue();
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
tx.oncomplete = () => resolve(count);
|
|
335
|
+
tx.onerror = () => reject(tx.error);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
async clear() {
|
|
339
|
+
const db = await this.getDb();
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const tx = db.transaction([QUEUE_STORE, STATE_STORE], "readwrite");
|
|
342
|
+
tx.objectStore(QUEUE_STORE).clear();
|
|
343
|
+
tx.objectStore(STATE_STORE).clear();
|
|
344
|
+
tx.oncomplete = () => resolve();
|
|
345
|
+
tx.onerror = () => reject(tx.error);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
async close() {
|
|
349
|
+
if (this.db) {
|
|
350
|
+
this.db.close();
|
|
351
|
+
this.db = null;
|
|
352
|
+
this.dbPromise = null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite implementation of SyncQueueStorage for Node/Bun environments.
|
|
3
|
+
*
|
|
4
|
+
* Works with both `better-sqlite3` (Node) and `bun:sqlite` (Bun).
|
|
5
|
+
*/
|
|
6
|
+
export class SqliteSyncQueue {
|
|
7
|
+
db;
|
|
8
|
+
/**
|
|
9
|
+
* Create a new SQLite sync queue.
|
|
10
|
+
* @param db - SQLite database instance (from better-sqlite3 or bun:sqlite)
|
|
11
|
+
*/
|
|
12
|
+
constructor(db) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.initialize();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create and open a SQLite database for an account.
|
|
18
|
+
* Helper for common use case.
|
|
19
|
+
*
|
|
20
|
+
* @param accountId - Unique identifier for the account (e.g., address, pubkey hash)
|
|
21
|
+
* @param dataDir - Directory for database files
|
|
22
|
+
* @param Database - SQLite Database constructor (e.g., from better-sqlite3 or bun:sqlite)
|
|
23
|
+
*/
|
|
24
|
+
static create(accountId, dataDir,
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: accepts any SQLite constructor
|
|
26
|
+
Database) {
|
|
27
|
+
const dbPath = `${dataDir}/sync-queue-${accountId}.db`;
|
|
28
|
+
const db = new Database(dbPath);
|
|
29
|
+
return new SqliteSyncQueue(db);
|
|
30
|
+
}
|
|
31
|
+
initialize() {
|
|
32
|
+
this.db.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS queue (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
outpoint TEXT NOT NULL,
|
|
36
|
+
score INTEGER NOT NULL,
|
|
37
|
+
spend_txid TEXT,
|
|
38
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
39
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
last_error TEXT,
|
|
41
|
+
created_at INTEGER NOT NULL,
|
|
42
|
+
updated_at INTEGER NOT NULL
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_queue_status ON queue(status);
|
|
46
|
+
CREATE INDEX IF NOT EXISTS idx_queue_outpoint ON queue(outpoint);
|
|
47
|
+
|
|
48
|
+
CREATE TABLE IF NOT EXISTS state (
|
|
49
|
+
key TEXT PRIMARY KEY,
|
|
50
|
+
value TEXT NOT NULL
|
|
51
|
+
);
|
|
52
|
+
`);
|
|
53
|
+
}
|
|
54
|
+
async enqueue(items) {
|
|
55
|
+
if (items.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const checkStmt = this.db.prepare("SELECT status FROM queue WHERE id = ?");
|
|
59
|
+
const insertStmt = this.db.prepare(`
|
|
60
|
+
INSERT OR REPLACE INTO queue (id, outpoint, score, spend_txid, status, attempts, created_at, updated_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, 'pending', COALESCE((SELECT attempts FROM queue WHERE id = ?), 0), COALESCE((SELECT created_at FROM queue WHERE id = ?), ?), ?)
|
|
62
|
+
`);
|
|
63
|
+
for (const item of items) {
|
|
64
|
+
const id = `${item.outpoint}:${item.score}`;
|
|
65
|
+
// Skip if already done
|
|
66
|
+
const existing = checkStmt.get(id);
|
|
67
|
+
if (existing?.status === "done") {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
insertStmt.run(id, item.outpoint, item.score, item.spendTxid ?? null, id, id, now, now);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async claim(count = 1) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
// Step 1: Get up to `count` pending items as seeds
|
|
76
|
+
const seedRows = this.db
|
|
77
|
+
.prepare("SELECT * FROM queue WHERE status = 'pending' LIMIT ?")
|
|
78
|
+
.all(count);
|
|
79
|
+
if (seedRows.length === 0)
|
|
80
|
+
return new Map();
|
|
81
|
+
// Step 2: Get unique txids from seeds
|
|
82
|
+
const txids = new Set();
|
|
83
|
+
for (const row of seedRows) {
|
|
84
|
+
txids.add(row.outpoint.substring(0, 64));
|
|
85
|
+
}
|
|
86
|
+
// Step 3: For each txid, get ALL pending items
|
|
87
|
+
const byTxid = new Map();
|
|
88
|
+
const allIds = [];
|
|
89
|
+
for (const txid of txids) {
|
|
90
|
+
const rows = this.db
|
|
91
|
+
.prepare("SELECT * FROM queue WHERE outpoint LIKE ? AND status = 'pending'")
|
|
92
|
+
.all(`${txid}%`);
|
|
93
|
+
if (rows.length > 0) {
|
|
94
|
+
const items = rows.map((row) => this.rowToItem(row, "processing", now));
|
|
95
|
+
byTxid.set(txid, items);
|
|
96
|
+
allIds.push(...rows.map((r) => r.id));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Step 4: Mark all gathered items as processing
|
|
100
|
+
if (allIds.length > 0) {
|
|
101
|
+
const placeholders = allIds.map(() => "?").join(",");
|
|
102
|
+
this.db
|
|
103
|
+
.prepare(`UPDATE queue SET status = 'processing', attempts = attempts + 1, updated_at = ?
|
|
104
|
+
WHERE id IN (${placeholders})`)
|
|
105
|
+
.run(now, ...allIds);
|
|
106
|
+
}
|
|
107
|
+
return byTxid;
|
|
108
|
+
}
|
|
109
|
+
async complete(id) {
|
|
110
|
+
return this.completeMany([id]);
|
|
111
|
+
}
|
|
112
|
+
async completeMany(ids) {
|
|
113
|
+
if (ids.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
const now = Date.now();
|
|
116
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
117
|
+
this.db
|
|
118
|
+
.prepare(`UPDATE queue SET status = 'done', updated_at = ? WHERE id IN (${placeholders})`)
|
|
119
|
+
.run(now, ...ids);
|
|
120
|
+
}
|
|
121
|
+
async fail(id, error) {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
this.db
|
|
124
|
+
.prepare("UPDATE queue SET status = 'failed', last_error = ?, updated_at = ? WHERE id = ?")
|
|
125
|
+
.run(error, now, id);
|
|
126
|
+
}
|
|
127
|
+
async getByTxid(txid) {
|
|
128
|
+
const rows = this.db
|
|
129
|
+
.prepare("SELECT * FROM queue WHERE outpoint LIKE ?")
|
|
130
|
+
.all(`${txid}%`);
|
|
131
|
+
return rows.map((row) => this.rowToItem(row));
|
|
132
|
+
}
|
|
133
|
+
async getByStatus(status, limit = 100) {
|
|
134
|
+
const rows = this.db
|
|
135
|
+
.prepare("SELECT * FROM queue WHERE status = ? LIMIT ?")
|
|
136
|
+
.all(status, limit);
|
|
137
|
+
return rows.map((row) => this.rowToItem(row));
|
|
138
|
+
}
|
|
139
|
+
async getStats() {
|
|
140
|
+
const row = this.db
|
|
141
|
+
.prepare(`SELECT
|
|
142
|
+
COUNT(DISTINCT CASE WHEN status = 'pending' THEN substr(outpoint, 1, 64) END) as pending,
|
|
143
|
+
COUNT(DISTINCT CASE WHEN status = 'processing' THEN substr(outpoint, 1, 64) END) as processing,
|
|
144
|
+
COUNT(DISTINCT CASE WHEN status = 'done' THEN substr(outpoint, 1, 64) END) as done,
|
|
145
|
+
COUNT(DISTINCT CASE WHEN status = 'failed' THEN substr(outpoint, 1, 64) END) as failed
|
|
146
|
+
FROM queue`)
|
|
147
|
+
.get();
|
|
148
|
+
return {
|
|
149
|
+
pending: row.pending ?? 0,
|
|
150
|
+
processing: row.processing ?? 0,
|
|
151
|
+
done: row.done ?? 0,
|
|
152
|
+
failed: row.failed ?? 0,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
async getState() {
|
|
156
|
+
const row = this.db
|
|
157
|
+
.prepare("SELECT value FROM state WHERE key = 'syncState'")
|
|
158
|
+
.get();
|
|
159
|
+
if (!row) {
|
|
160
|
+
return { lastQueuedScore: 0 };
|
|
161
|
+
}
|
|
162
|
+
return JSON.parse(row.value);
|
|
163
|
+
}
|
|
164
|
+
async setState(state) {
|
|
165
|
+
const current = await this.getState();
|
|
166
|
+
const updated = { ...current, ...state };
|
|
167
|
+
this.db
|
|
168
|
+
.prepare("INSERT OR REPLACE INTO state (key, value) VALUES ('syncState', ?)")
|
|
169
|
+
.run(JSON.stringify(updated));
|
|
170
|
+
}
|
|
171
|
+
async resetProcessing() {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const result = this.db
|
|
174
|
+
.prepare("UPDATE queue SET status = 'pending', updated_at = ? WHERE status = 'processing'")
|
|
175
|
+
.run(now);
|
|
176
|
+
return result.changes;
|
|
177
|
+
}
|
|
178
|
+
async clear() {
|
|
179
|
+
this.db.exec("DELETE FROM queue; DELETE FROM state;");
|
|
180
|
+
}
|
|
181
|
+
async close() {
|
|
182
|
+
this.db.close();
|
|
183
|
+
}
|
|
184
|
+
rowToItem(row, statusOverride, updatedAtOverride) {
|
|
185
|
+
return {
|
|
186
|
+
id: row.id,
|
|
187
|
+
outpoint: row.outpoint,
|
|
188
|
+
score: row.score,
|
|
189
|
+
spendTxid: row.spend_txid ?? undefined,
|
|
190
|
+
status: statusOverride ?? row.status,
|
|
191
|
+
attempts: statusOverride ? row.attempts + 1 : row.attempts,
|
|
192
|
+
lastError: row.last_error ?? undefined,
|
|
193
|
+
createdAt: row.created_at,
|
|
194
|
+
updatedAt: updatedAtOverride ?? row.updated_at,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/wallet-toolbox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "BSV wallet library extending @bsv/wallet-toolbox with 1Sat Ordinals protocol support",
|
|
5
5
|
"author": "1Sat Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "
|
|
31
|
+
"build": "tsc",
|
|
32
32
|
"dev": "tsc --watch",
|
|
33
33
|
"lint": "biome check src",
|
|
34
34
|
"lint:fix": "biome check --write src",
|
|
@@ -37,13 +37,13 @@
|
|
|
37
37
|
"tester": "bun run build && bun run tester:build && bun ./tester/server.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@bopen-io/
|
|
41
|
-
"@bsv/sdk": "1.9.
|
|
42
|
-
"@bsv/wallet-toolbox": "^1.7.14",
|
|
40
|
+
"@bopen-io/templates": "^1.1.2",
|
|
41
|
+
"@bsv/sdk": "^1.9.29",
|
|
43
42
|
"buffer": "^6.0.3"
|
|
44
43
|
},
|
|
45
44
|
"devDependencies": {
|
|
46
45
|
"@biomejs/biome": "^1.9.4",
|
|
46
|
+
"@bsv/wallet-toolbox": "^1.7.18",
|
|
47
47
|
"@types/bun": "^1.3.4",
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
}
|