@arkade-os/sdk 0.3.0-alpha.7 → 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.
- package/README.md +99 -14
- package/dist/cjs/adapters/expo.js +8 -0
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +2 -2
- package/dist/cjs/identity/singleKey.js +8 -8
- package/dist/cjs/index.js +14 -5
- package/dist/cjs/{bip322 → intent}/index.js +38 -61
- package/dist/cjs/musig2/index.js +2 -1
- package/dist/cjs/musig2/nonces.js +4 -0
- package/dist/cjs/providers/ark.js +76 -45
- package/dist/cjs/providers/errors.js +59 -0
- package/dist/cjs/providers/expoArk.js +82 -0
- package/dist/cjs/providers/expoIndexer.js +105 -0
- package/dist/cjs/providers/expoUtils.js +124 -0
- package/dist/cjs/providers/indexer.js +3 -1
- package/dist/cjs/providers/onchain.js +19 -20
- package/dist/cjs/repositories/walletRepository.js +64 -28
- package/dist/cjs/script/base.js +15 -7
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +44 -11
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +118 -15
- package/dist/cjs/utils/transaction.js +28 -0
- package/dist/cjs/utils/unknownFields.js +7 -7
- package/dist/cjs/wallet/index.js +1 -1
- package/dist/cjs/wallet/onchain.js +6 -7
- package/dist/cjs/wallet/serviceWorker/response.js +32 -0
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -9
- package/dist/cjs/wallet/serviceWorker/wallet.js +7 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +48 -32
- package/dist/cjs/wallet/unroll.js +7 -9
- package/dist/cjs/wallet/utils.js +20 -0
- package/dist/cjs/wallet/vtxo-manager.js +323 -0
- package/dist/cjs/wallet/wallet.js +165 -174
- package/dist/esm/adapters/expo.js +3 -0
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +1 -1
- package/dist/esm/identity/singleKey.js +9 -9
- package/dist/esm/index.js +14 -10
- package/dist/esm/{bip322 → intent}/index.js +32 -54
- package/dist/esm/musig2/index.js +1 -1
- package/dist/esm/musig2/nonces.js +3 -0
- package/dist/esm/providers/ark.js +76 -45
- package/dist/esm/providers/errors.js +54 -0
- package/dist/esm/providers/expoArk.js +78 -0
- package/dist/esm/providers/expoIndexer.js +101 -0
- package/dist/esm/providers/expoUtils.js +87 -0
- package/dist/esm/providers/indexer.js +3 -1
- package/dist/esm/providers/onchain.js +19 -20
- package/dist/esm/repositories/walletRepository.js +64 -28
- package/dist/esm/script/base.js +12 -4
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +45 -12
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +110 -9
- package/dist/esm/utils/transaction.js +24 -0
- package/dist/esm/utils/unknownFields.js +3 -3
- package/dist/esm/wallet/index.js +1 -1
- package/dist/esm/wallet/onchain.js +3 -4
- package/dist/esm/wallet/serviceWorker/response.js +32 -0
- package/dist/esm/wallet/serviceWorker/utils.js +1 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +8 -9
- package/dist/esm/wallet/serviceWorker/worker.js +49 -33
- package/dist/esm/wallet/unroll.js +5 -7
- package/dist/esm/wallet/utils.js +16 -0
- package/dist/esm/wallet/vtxo-manager.js +317 -0
- package/dist/esm/wallet/wallet.js +159 -168
- package/dist/types/adapters/expo.d.ts +4 -0
- package/dist/types/arknote/index.d.ts +1 -1
- package/dist/types/forfeit.d.ts +2 -2
- package/dist/types/identity/index.d.ts +2 -2
- package/dist/types/identity/singleKey.d.ts +2 -2
- package/dist/types/index.d.ts +11 -9
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/musig2/index.d.ts +1 -1
- package/dist/types/musig2/nonces.d.ts +1 -0
- package/dist/types/providers/ark.d.ts +197 -27
- package/dist/types/providers/errors.d.ts +13 -0
- package/dist/types/providers/expoArk.d.ts +22 -0
- package/dist/types/providers/expoIndexer.d.ts +18 -0
- package/dist/types/providers/expoUtils.d.ts +18 -0
- package/dist/types/providers/indexer.d.ts +8 -8
- package/dist/types/providers/onchain.d.ts +6 -2
- package/dist/types/repositories/walletRepository.d.ts +9 -5
- package/dist/types/script/base.d.ts +5 -2
- package/dist/types/tree/signingSession.d.ts +16 -11
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +15 -5
- package/dist/types/utils/transaction.d.ts +13 -0
- package/dist/types/utils/unknownFields.d.ts +4 -4
- package/dist/types/wallet/index.d.ts +47 -7
- package/dist/types/wallet/onchain.d.ts +1 -1
- package/dist/types/wallet/serviceWorker/response.d.ts +16 -2
- package/dist/types/wallet/serviceWorker/utils.d.ts +1 -2
- package/dist/types/wallet/serviceWorker/wallet.d.ts +2 -2
- package/dist/types/wallet/serviceWorker/worker.d.ts +7 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +3 -0
- package/dist/types/wallet/vtxo-manager.d.ts +179 -0
- package/dist/types/wallet/wallet.d.ts +17 -5
- package/package.json +11 -3
- package/dist/cjs/bip322/errors.js +0 -13
- package/dist/esm/bip322/errors.js +0 -9
- package/dist/types/bip322/errors.d.ts +0 -6
- package/dist/types/bip322/index.d.ts +0 -57
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RestArkProvider, isFetchTimeoutError, } from './ark.js';
|
|
2
|
+
import { getExpoFetch, sseStreamIterator } from './expoUtils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Expo-compatible Ark provider implementation using expo/fetch for SSE support.
|
|
5
|
+
* This provider works specifically in React Native/Expo environments where
|
|
6
|
+
* standard EventSource is not available but expo/fetch provides SSE capabilities.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* import { ExpoArkProvider } from '@arkade-os/sdk/providers/expo';
|
|
11
|
+
*
|
|
12
|
+
* const provider = new ExpoArkProvider('https://ark.example.com');
|
|
13
|
+
* const info = await provider.getInfo();
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export class ExpoArkProvider extends RestArkProvider {
|
|
17
|
+
constructor(serverUrl) {
|
|
18
|
+
super(serverUrl);
|
|
19
|
+
}
|
|
20
|
+
async *getEventStream(signal, topics) {
|
|
21
|
+
const expoFetch = await getExpoFetch();
|
|
22
|
+
const url = `${this.serverUrl}/v1/batch/events`;
|
|
23
|
+
const queryParams = topics.length > 0
|
|
24
|
+
? `?${topics.map((topic) => `topics=${encodeURIComponent(topic)}`).join("&")}`
|
|
25
|
+
: "";
|
|
26
|
+
while (!signal?.aborted) {
|
|
27
|
+
try {
|
|
28
|
+
yield* sseStreamIterator(url + queryParams, signal, expoFetch, {}, (data) => {
|
|
29
|
+
// Handle different response structures
|
|
30
|
+
// v8 mesh API might wrap in {result: ...} or send directly
|
|
31
|
+
const eventData = data.result || data;
|
|
32
|
+
// Skip heartbeat messages
|
|
33
|
+
if (eventData.heartbeat !== undefined) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return this.parseSettlementEvent(eventData);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
// ignore timeout errors, they're expected when the server is not sending anything for 5 min
|
|
44
|
+
// these timeouts are set by expo/fetch function
|
|
45
|
+
if (isFetchTimeoutError(error)) {
|
|
46
|
+
console.debug("Timeout error ignored");
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
console.error("Event stream error:", error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async *getTransactionsStream(signal) {
|
|
55
|
+
const expoFetch = await getExpoFetch();
|
|
56
|
+
const url = `${this.serverUrl}/v1/txs`;
|
|
57
|
+
while (!signal?.aborted) {
|
|
58
|
+
try {
|
|
59
|
+
yield* sseStreamIterator(url, signal, expoFetch, {}, (data) => {
|
|
60
|
+
return this.parseTransactionNotification(data.result);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// ignore timeout errors, they're expected when the server is not sending anything for 5 min
|
|
68
|
+
// these timeouts are set by expo/fetch function
|
|
69
|
+
if (isFetchTimeoutError(error)) {
|
|
70
|
+
console.debug("Timeout error ignored");
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
console.error("Transaction stream error:", error);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { RestIndexerProvider } from './indexer.js';
|
|
2
|
+
import { isFetchTimeoutError } from './ark.js';
|
|
3
|
+
import { getExpoFetch, sseStreamIterator } from './expoUtils.js';
|
|
4
|
+
// Helper function to convert Vtxo to VirtualCoin (same as in indexer.ts)
|
|
5
|
+
function convertVtxo(vtxo) {
|
|
6
|
+
return {
|
|
7
|
+
txid: vtxo.outpoint.txid,
|
|
8
|
+
vout: vtxo.outpoint.vout,
|
|
9
|
+
value: Number(vtxo.amount),
|
|
10
|
+
status: {
|
|
11
|
+
confirmed: !vtxo.isSwept && !vtxo.isPreconfirmed,
|
|
12
|
+
},
|
|
13
|
+
virtualStatus: {
|
|
14
|
+
state: vtxo.isSwept
|
|
15
|
+
? "swept"
|
|
16
|
+
: vtxo.isPreconfirmed
|
|
17
|
+
? "preconfirmed"
|
|
18
|
+
: "settled",
|
|
19
|
+
commitmentTxIds: vtxo.commitmentTxids,
|
|
20
|
+
batchExpiry: vtxo.expiresAt
|
|
21
|
+
? Number(vtxo.expiresAt) * 1000
|
|
22
|
+
: undefined,
|
|
23
|
+
},
|
|
24
|
+
spentBy: vtxo.spentBy ?? "",
|
|
25
|
+
settledBy: vtxo.settledBy,
|
|
26
|
+
arkTxId: vtxo.arkTxid,
|
|
27
|
+
createdAt: new Date(Number(vtxo.createdAt) * 1000),
|
|
28
|
+
isUnrolled: vtxo.isUnrolled,
|
|
29
|
+
isSpent: vtxo.isSpent,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Expo-compatible Indexer provider implementation using expo/fetch for streaming support.
|
|
34
|
+
* This provider works specifically in React Native/Expo environments where
|
|
35
|
+
* standard fetch streaming may not work properly but expo/fetch provides streaming capabilities.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { ExpoIndexerProvider } from '@arkade-os/sdk/adapters/expo';
|
|
40
|
+
*
|
|
41
|
+
* const provider = new ExpoIndexerProvider('https://indexer.example.com');
|
|
42
|
+
* const vtxos = await provider.getVtxos({ scripts: ['script1'] });
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export class ExpoIndexerProvider extends RestIndexerProvider {
|
|
46
|
+
constructor(serverUrl) {
|
|
47
|
+
super(serverUrl);
|
|
48
|
+
}
|
|
49
|
+
async *getSubscription(subscriptionId, abortSignal) {
|
|
50
|
+
// Detect if we're running in React Native/Expo environment
|
|
51
|
+
const isReactNative = typeof navigator !== "undefined" &&
|
|
52
|
+
navigator.product === "ReactNative";
|
|
53
|
+
const expoFetch = await getExpoFetch().catch((error) => {
|
|
54
|
+
// In React Native/Expo, expo/fetch is required for proper streaming support
|
|
55
|
+
if (isReactNative) {
|
|
56
|
+
throw new Error("expo/fetch is unavailable in React Native environment. " +
|
|
57
|
+
"Please ensure expo/fetch is installed and properly configured. " +
|
|
58
|
+
"Streaming support may not work with standard fetch in React Native.");
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
});
|
|
62
|
+
const url = `${this.serverUrl}/v1/indexer/script/subscription/${subscriptionId}`;
|
|
63
|
+
while (!abortSignal.aborted) {
|
|
64
|
+
try {
|
|
65
|
+
yield* sseStreamIterator(url, abortSignal, expoFetch, { "Content-Type": "application/json" }, (data) => {
|
|
66
|
+
// Handle new v8 proto format with heartbeat or event
|
|
67
|
+
if (data.heartbeat !== undefined) {
|
|
68
|
+
// Skip heartbeat messages
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
// Process event messages
|
|
72
|
+
if (data.event) {
|
|
73
|
+
return {
|
|
74
|
+
txid: data.event.txid,
|
|
75
|
+
scripts: data.event.scripts || [],
|
|
76
|
+
newVtxos: (data.event.newVtxos || []).map(convertVtxo),
|
|
77
|
+
spentVtxos: (data.event.spentVtxos || []).map(convertVtxo),
|
|
78
|
+
sweptVtxos: (data.event.sweptVtxos || []).map(convertVtxo),
|
|
79
|
+
tx: data.event.tx,
|
|
80
|
+
checkpointTxs: data.event.checkpointTxs,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
// ignore timeout errors, they're expected when the server is not sending anything for 5 min
|
|
91
|
+
// these timeouts are set by expo/fetch function
|
|
92
|
+
if (isFetchTimeoutError(error)) {
|
|
93
|
+
console.debug("Timeout error ignored");
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
console.error("Subscription error:", error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamically imports expo/fetch with fallback to standard fetch.
|
|
3
|
+
* @returns A fetch function suitable for SSE streaming
|
|
4
|
+
*/
|
|
5
|
+
export async function getExpoFetch(options) {
|
|
6
|
+
const requireExpo = options?.requireExpo ?? false;
|
|
7
|
+
try {
|
|
8
|
+
const expoFetchModule = await import("expo/fetch");
|
|
9
|
+
console.debug("Using expo/fetch for streaming");
|
|
10
|
+
return expoFetchModule.fetch;
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (requireExpo) {
|
|
14
|
+
throw new Error("expo/fetch is unavailable in this environment. " +
|
|
15
|
+
"Please ensure expo/fetch is installed and properly configured.");
|
|
16
|
+
}
|
|
17
|
+
console.warn("Using standard fetch instead of expo/fetch. " +
|
|
18
|
+
"Streaming may not be fully supported in some environments.", error);
|
|
19
|
+
return fetch;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generic SSE stream processor using fetch API with ReadableStream.
|
|
24
|
+
* Handles SSE format parsing, buffer management, and abort signals.
|
|
25
|
+
*
|
|
26
|
+
* @param url - The SSE endpoint URL
|
|
27
|
+
* @param abortSignal - Signal to abort the stream
|
|
28
|
+
* @param fetchFn - Fetch function to use (defaults to standard fetch)
|
|
29
|
+
* @param headers - Additional headers to send
|
|
30
|
+
* @param parseData - Function to parse and yield data from SSE events
|
|
31
|
+
*/
|
|
32
|
+
export async function* sseStreamIterator(url, abortSignal, fetchFn, headers, parseData) {
|
|
33
|
+
const fetchController = new AbortController();
|
|
34
|
+
const cleanup = () => fetchController.abort();
|
|
35
|
+
abortSignal?.addEventListener("abort", cleanup, { once: true });
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetchFn(url, {
|
|
38
|
+
headers: {
|
|
39
|
+
Accept: "text/event-stream",
|
|
40
|
+
...headers,
|
|
41
|
+
},
|
|
42
|
+
signal: fetchController.signal,
|
|
43
|
+
});
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
throw new Error(`Unexpected status ${response.status} when fetching SSE stream`);
|
|
46
|
+
}
|
|
47
|
+
if (!response.body) {
|
|
48
|
+
throw new Error("Response body is null");
|
|
49
|
+
}
|
|
50
|
+
const reader = response.body.getReader();
|
|
51
|
+
const decoder = new TextDecoder();
|
|
52
|
+
let buffer = "";
|
|
53
|
+
while (!abortSignal?.aborted) {
|
|
54
|
+
const { done, value } = await reader.read();
|
|
55
|
+
if (done) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
buffer += decoder.decode(value, { stream: true });
|
|
59
|
+
const lines = buffer.split("\n");
|
|
60
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
61
|
+
const line = lines[i].trim();
|
|
62
|
+
if (!line)
|
|
63
|
+
continue;
|
|
64
|
+
if (line.startsWith("data:")) {
|
|
65
|
+
const jsonStr = line.substring(5).trim();
|
|
66
|
+
if (!jsonStr)
|
|
67
|
+
continue;
|
|
68
|
+
try {
|
|
69
|
+
const data = JSON.parse(jsonStr);
|
|
70
|
+
const parsed = parseData(data);
|
|
71
|
+
if (parsed !== null) {
|
|
72
|
+
yield parsed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (parseError) {
|
|
76
|
+
console.error("Failed to parse SSE data:", parseError);
|
|
77
|
+
throw parseError;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
buffer = lines[lines.length - 1];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
abortSignal?.removeEventListener("abort", cleanup);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -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
|
|
@@ -18,9 +18,10 @@ export const ESPLORA_URL = {
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
export class EsploraProvider {
|
|
21
|
-
constructor(baseUrl) {
|
|
21
|
+
constructor(baseUrl, opts) {
|
|
22
22
|
this.baseUrl = baseUrl;
|
|
23
|
-
this.
|
|
23
|
+
this.pollingInterval = opts?.pollingInterval ?? 15000;
|
|
24
|
+
this.forcePolling = opts?.forcePolling ?? false;
|
|
24
25
|
}
|
|
25
26
|
async getCoins(address) {
|
|
26
27
|
const response = await fetch(`${this.baseUrl}/address/${address}/utxo`);
|
|
@@ -91,13 +92,9 @@ export class EsploraProvider {
|
|
|
91
92
|
let intervalId = null;
|
|
92
93
|
const wsUrl = this.baseUrl.replace(/^http(s)?:/, "ws$1:") + "/v1/ws";
|
|
93
94
|
const poll = async () => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// websocket is not reliable, so we will fallback to polling
|
|
98
|
-
const pollingInterval = 5000; // 5 seconds
|
|
99
|
-
const getAllTxs = () => {
|
|
100
|
-
return Promise.all(addresses.map((address) => this.getTransactions(address))).then((txArrays) => txArrays.flat());
|
|
95
|
+
const getAllTxs = async () => {
|
|
96
|
+
const txArrays = await Promise.all(addresses.map((address) => this.getTransactions(address)));
|
|
97
|
+
return txArrays.flat();
|
|
101
98
|
};
|
|
102
99
|
// initial fetch to get existing transactions
|
|
103
100
|
const initialTxs = await getAllTxs();
|
|
@@ -122,9 +119,19 @@ export class EsploraProvider {
|
|
|
122
119
|
catch (error) {
|
|
123
120
|
console.error("Error in polling mechanism:", error);
|
|
124
121
|
}
|
|
125
|
-
}, pollingInterval);
|
|
122
|
+
}, this.pollingInterval);
|
|
126
123
|
};
|
|
127
124
|
let ws = null;
|
|
125
|
+
const stopFunc = () => {
|
|
126
|
+
if (ws)
|
|
127
|
+
ws.close();
|
|
128
|
+
if (intervalId)
|
|
129
|
+
clearInterval(intervalId);
|
|
130
|
+
};
|
|
131
|
+
if (this.forcePolling) {
|
|
132
|
+
await poll();
|
|
133
|
+
return stopFunc;
|
|
134
|
+
}
|
|
128
135
|
try {
|
|
129
136
|
ws = new WebSocket(wsUrl);
|
|
130
137
|
ws.addEventListener("open", () => {
|
|
@@ -171,13 +178,6 @@ export class EsploraProvider {
|
|
|
171
178
|
// if websocket is not available, fallback to polling
|
|
172
179
|
await poll();
|
|
173
180
|
}
|
|
174
|
-
const stopFunc = () => {
|
|
175
|
-
if (ws && ws.readyState === WebSocket.OPEN)
|
|
176
|
-
ws.close();
|
|
177
|
-
if (intervalId)
|
|
178
|
-
clearInterval(intervalId);
|
|
179
|
-
this.polling = false;
|
|
180
|
-
};
|
|
181
181
|
return stopFunc;
|
|
182
182
|
}
|
|
183
183
|
async getChainTip() {
|
|
@@ -245,8 +245,7 @@ const isExplorerTransaction = (tx) => {
|
|
|
245
245
|
return (typeof tx.txid === "string" &&
|
|
246
246
|
Array.isArray(tx.vout) &&
|
|
247
247
|
tx.vout.every((vout) => typeof vout.scriptpubkey_address === "string" &&
|
|
248
|
-
typeof vout.value === "
|
|
248
|
+
typeof vout.value === "number") &&
|
|
249
249
|
typeof tx.status === "object" &&
|
|
250
|
-
typeof tx.status.confirmed === "boolean"
|
|
251
|
-
typeof tx.status.block_time === "number");
|
|
250
|
+
typeof tx.status.confirmed === "boolean");
|
|
252
251
|
};
|
|
@@ -12,7 +12,14 @@ const serializeVtxo = (v) => ({
|
|
|
12
12
|
tapTree: toHex(v.tapTree),
|
|
13
13
|
forfeitTapLeafScript: serializeTapLeaf(v.forfeitTapLeafScript),
|
|
14
14
|
intentTapLeafScript: serializeTapLeaf(v.intentTapLeafScript),
|
|
15
|
-
extraWitness: v.extraWitness?.map(
|
|
15
|
+
extraWitness: v.extraWitness?.map(toHex),
|
|
16
|
+
});
|
|
17
|
+
const serializeUtxo = (u) => ({
|
|
18
|
+
...u,
|
|
19
|
+
tapTree: toHex(u.tapTree),
|
|
20
|
+
forfeitTapLeafScript: serializeTapLeaf(u.forfeitTapLeafScript),
|
|
21
|
+
intentTapLeafScript: serializeTapLeaf(u.intentTapLeafScript),
|
|
22
|
+
extraWitness: u.extraWitness?.map(toHex),
|
|
16
23
|
});
|
|
17
24
|
const deserializeTapLeaf = (t) => {
|
|
18
25
|
const cb = TaprootControlBlock.decode(fromHex(t.cb));
|
|
@@ -24,13 +31,21 @@ const deserializeVtxo = (o) => ({
|
|
|
24
31
|
tapTree: fromHex(o.tapTree),
|
|
25
32
|
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
26
33
|
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
27
|
-
extraWitness: o.extraWitness?.map(
|
|
34
|
+
extraWitness: o.extraWitness?.map(fromHex),
|
|
35
|
+
});
|
|
36
|
+
const deserializeUtxo = (o) => ({
|
|
37
|
+
...o,
|
|
38
|
+
tapTree: fromHex(o.tapTree),
|
|
39
|
+
forfeitTapLeafScript: deserializeTapLeaf(o.forfeitTapLeafScript),
|
|
40
|
+
intentTapLeafScript: deserializeTapLeaf(o.intentTapLeafScript),
|
|
41
|
+
extraWitness: o.extraWitness?.map(fromHex),
|
|
28
42
|
});
|
|
29
43
|
export class WalletRepositoryImpl {
|
|
30
44
|
constructor(storage) {
|
|
31
45
|
this.storage = storage;
|
|
32
46
|
this.cache = {
|
|
33
47
|
vtxos: new Map(),
|
|
48
|
+
utxos: new Map(),
|
|
34
49
|
transactions: new Map(),
|
|
35
50
|
walletState: null,
|
|
36
51
|
initialized: new Set(),
|
|
@@ -58,18 +73,6 @@ export class WalletRepositoryImpl {
|
|
|
58
73
|
return [];
|
|
59
74
|
}
|
|
60
75
|
}
|
|
61
|
-
async saveVtxo(address, vtxo) {
|
|
62
|
-
const vtxos = await this.getVtxos(address);
|
|
63
|
-
const existing = vtxos.findIndex((v) => v.txid === vtxo.txid && v.vout === vtxo.vout);
|
|
64
|
-
if (existing !== -1) {
|
|
65
|
-
vtxos[existing] = vtxo;
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
vtxos.push(vtxo);
|
|
69
|
-
}
|
|
70
|
-
this.cache.vtxos.set(address, vtxos.slice());
|
|
71
|
-
await this.storage.setItem(`vtxos:${address}`, JSON.stringify(vtxos.map(serializeVtxo)));
|
|
72
|
-
}
|
|
73
76
|
async saveVtxos(address, vtxos) {
|
|
74
77
|
const storedVtxos = await this.getVtxos(address);
|
|
75
78
|
for (const vtxo of vtxos) {
|
|
@@ -95,6 +98,53 @@ export class WalletRepositoryImpl {
|
|
|
95
98
|
this.cache.vtxos.set(address, []);
|
|
96
99
|
await this.storage.removeItem(`vtxos:${address}`);
|
|
97
100
|
}
|
|
101
|
+
async getUtxos(address) {
|
|
102
|
+
const cacheKey = `utxos:${address}`;
|
|
103
|
+
if (this.cache.utxos.has(address)) {
|
|
104
|
+
return this.cache.utxos.get(address);
|
|
105
|
+
}
|
|
106
|
+
const stored = await this.storage.getItem(cacheKey);
|
|
107
|
+
if (!stored) {
|
|
108
|
+
this.cache.utxos.set(address, []);
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(stored);
|
|
113
|
+
const utxos = parsed.map(deserializeUtxo);
|
|
114
|
+
this.cache.utxos.set(address, utxos.slice());
|
|
115
|
+
return utxos.slice();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`Failed to parse UTXOs for address ${address}:`, error);
|
|
119
|
+
this.cache.utxos.set(address, []);
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async saveUtxos(address, utxos) {
|
|
124
|
+
const storedUtxos = await this.getUtxos(address);
|
|
125
|
+
utxos.forEach((utxo) => {
|
|
126
|
+
const existing = storedUtxos.findIndex((u) => u.txid === utxo.txid && u.vout === utxo.vout);
|
|
127
|
+
if (existing !== -1) {
|
|
128
|
+
storedUtxos[existing] = utxo;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
storedUtxos.push(utxo);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.cache.utxos.set(address, storedUtxos.slice());
|
|
135
|
+
await this.storage.setItem(`utxos:${address}`, JSON.stringify(storedUtxos.map(serializeUtxo)));
|
|
136
|
+
}
|
|
137
|
+
async removeUtxo(address, utxoId) {
|
|
138
|
+
const utxos = await this.getUtxos(address);
|
|
139
|
+
const [txid, vout] = utxoId.split(":");
|
|
140
|
+
const filtered = utxos.filter((v) => !(v.txid === txid && v.vout === parseInt(vout, 10)));
|
|
141
|
+
this.cache.utxos.set(address, filtered.slice());
|
|
142
|
+
await this.storage.setItem(`utxos:${address}`, JSON.stringify(filtered.map(serializeUtxo)));
|
|
143
|
+
}
|
|
144
|
+
async clearUtxos(address) {
|
|
145
|
+
this.cache.utxos.set(address, []);
|
|
146
|
+
await this.storage.removeItem(`utxos:${address}`);
|
|
147
|
+
}
|
|
98
148
|
async getTransactionHistory(address) {
|
|
99
149
|
const cacheKey = `tx:${address}`;
|
|
100
150
|
if (this.cache.transactions.has(address)) {
|
|
@@ -116,20 +166,6 @@ export class WalletRepositoryImpl {
|
|
|
116
166
|
return [];
|
|
117
167
|
}
|
|
118
168
|
}
|
|
119
|
-
async saveTransaction(address, tx) {
|
|
120
|
-
const transactions = await this.getTransactionHistory(address);
|
|
121
|
-
const existing = transactions.findIndex((t) => t.key === tx.key);
|
|
122
|
-
if (existing !== -1) {
|
|
123
|
-
transactions[existing] = tx;
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
transactions.push(tx);
|
|
127
|
-
}
|
|
128
|
-
// Sort by createdAt descending
|
|
129
|
-
transactions.sort((a, b) => b.createdAt - a.createdAt);
|
|
130
|
-
this.cache.transactions.set(address, transactions);
|
|
131
|
-
await this.storage.setItem(`tx:${address}`, JSON.stringify(transactions));
|
|
132
|
-
}
|
|
133
169
|
async saveTransactions(address, txs) {
|
|
134
170
|
const storedTransactions = await this.getTransactionHistory(address);
|
|
135
171
|
for (const tx of txs) {
|
package/dist/esm/script/base.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { Script, Address, p2tr, taprootListToTree } from "@scure/btc-signer";
|
|
1
|
+
import { Script, Address, p2tr, taprootListToTree, TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer";
|
|
2
2
|
import { TAP_LEAF_VERSION } from "@scure/btc-signer/payment.js";
|
|
3
3
|
import { PSBTOutput } from "@scure/btc-signer/psbt.js";
|
|
4
|
-
import { TAPROOT_UNSPENDABLE_KEY, } from "@scure/btc-signer/utils.js";
|
|
5
4
|
import { hex } from "@scure/base";
|
|
6
5
|
import { ArkAddress } from './address.js';
|
|
7
6
|
import { ConditionCSVMultisigTapscript, CSVMultisigTapscript, } from './tapscript.js';
|
|
8
|
-
const TapTreeCoder = PSBTOutput.tapTree[2];
|
|
7
|
+
export const TapTreeCoder = PSBTOutput.tapTree[2];
|
|
9
8
|
export function scriptFromTapLeafScript(leaf) {
|
|
10
9
|
return leaf[1].subarray(0, leaf[1].length - 1); // remove the version byte
|
|
11
10
|
}
|
|
@@ -25,7 +24,16 @@ export class VtxoScript {
|
|
|
25
24
|
}
|
|
26
25
|
constructor(scripts) {
|
|
27
26
|
this.scripts = scripts;
|
|
28
|
-
|
|
27
|
+
// reverse the scripts if the number of scripts is odd
|
|
28
|
+
// this is to be compatible with arkd algorithm computing taproot tree from list of tapscripts
|
|
29
|
+
// the scripts must be reversed only HERE while we compute the tweaked public key
|
|
30
|
+
// but the original order should be preserved while encoding as taptree
|
|
31
|
+
// note: .slice().reverse() is used instead of .reverse() to avoid mutating the original array
|
|
32
|
+
const list = scripts.length % 2 !== 0 ? scripts.slice().reverse() : scripts;
|
|
33
|
+
const tapTree = taprootListToTree(list.map((script) => ({
|
|
34
|
+
script,
|
|
35
|
+
leafVersion: TAP_LEAF_VERSION,
|
|
36
|
+
})));
|
|
29
37
|
const payment = p2tr(TAPROOT_UNSPENDABLE_KEY, tapTree, undefined, true);
|
|
30
38
|
if (!payment.tapLeafScript ||
|
|
31
39
|
payment.tapLeafScript.length !== scripts.length) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as bip68 from "bip68";
|
|
2
|
-
import { Script, ScriptNum } from "@scure/btc-signer
|
|
3
|
-
import { p2tr_ms } from "@scure/btc-signer/payment.js";
|
|
2
|
+
import { Script, ScriptNum, p2tr_ms } from "@scure/btc-signer";
|
|
4
3
|
import { hex } from "@scure/base";
|
|
5
4
|
const MinimalScriptNum = ScriptNum(undefined, true);
|
|
6
5
|
export var TapscriptType;
|
package/dist/esm/script/vhtlc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Script } from "@scure/btc-signer
|
|
1
|
+
import { Script } from "@scure/btc-signer";
|
|
2
2
|
import { CLTVMultisigTapscript, ConditionCSVMultisigTapscript, ConditionMultisigTapscript, CSVMultisigTapscript, MultisigTapscript, } from './tapscript.js';
|
|
3
3
|
import { hex } from "@scure/base";
|
|
4
4
|
import { VtxoScript } from './base.js';
|
|
@@ -3,7 +3,7 @@ import { Script } from "@scure/btc-signer/script.js";
|
|
|
3
3
|
import { SigHash } from "@scure/btc-signer/transaction.js";
|
|
4
4
|
import { hex } from "@scure/base";
|
|
5
5
|
import { schnorr, secp256k1 } from "@noble/curves/secp256k1.js";
|
|
6
|
-
import { randomPrivateKeyBytes
|
|
6
|
+
import { randomPrivateKeyBytes } from "@scure/btc-signer/utils.js";
|
|
7
7
|
import { CosignerPublicKey, getArkPsbtFields } from '../utils/unknownFields.js';
|
|
8
8
|
export const ErrMissingVtxoGraph = new Error("missing vtxo graph");
|
|
9
9
|
export const ErrMissingAggregateKey = new Error("missing aggregate key");
|
|
@@ -20,15 +20,15 @@ export class TreeSignerSession {
|
|
|
20
20
|
const secretKey = randomPrivateKeyBytes();
|
|
21
21
|
return new TreeSignerSession(secretKey);
|
|
22
22
|
}
|
|
23
|
-
init(tree, scriptRoot, rootInputAmount) {
|
|
23
|
+
async init(tree, scriptRoot, rootInputAmount) {
|
|
24
24
|
this.graph = tree;
|
|
25
25
|
this.scriptRoot = scriptRoot;
|
|
26
26
|
this.rootSharedOutputAmount = rootInputAmount;
|
|
27
27
|
}
|
|
28
|
-
getPublicKey() {
|
|
28
|
+
async getPublicKey() {
|
|
29
29
|
return secp256k1.getPublicKey(this.secretKey);
|
|
30
30
|
}
|
|
31
|
-
getNonces() {
|
|
31
|
+
async getNonces() {
|
|
32
32
|
if (!this.graph)
|
|
33
33
|
throw ErrMissingVtxoGraph;
|
|
34
34
|
if (!this.myNonces) {
|
|
@@ -40,12 +40,46 @@ export class TreeSignerSession {
|
|
|
40
40
|
}
|
|
41
41
|
return publicNonces;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
if (this.
|
|
45
|
-
throw
|
|
46
|
-
this.aggregateNonces
|
|
43
|
+
async aggregatedNonces(txid, noncesByPubkey) {
|
|
44
|
+
if (!this.graph)
|
|
45
|
+
throw ErrMissingVtxoGraph;
|
|
46
|
+
if (!this.aggregateNonces) {
|
|
47
|
+
this.aggregateNonces = new Map();
|
|
48
|
+
}
|
|
49
|
+
if (!this.myNonces) {
|
|
50
|
+
await this.getNonces(); // generate nonces if not generated yet
|
|
51
|
+
}
|
|
52
|
+
if (this.aggregateNonces.has(txid)) {
|
|
53
|
+
return {
|
|
54
|
+
hasAllNonces: this.aggregateNonces.size === this.myNonces?.size,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const myNonce = this.myNonces.get(txid);
|
|
58
|
+
if (!myNonce)
|
|
59
|
+
throw new Error(`missing nonce for txid ${txid}`);
|
|
60
|
+
const myPublicKey = await this.getPublicKey();
|
|
61
|
+
// set my nonce to not rely on server
|
|
62
|
+
noncesByPubkey.set(hex.encode(myPublicKey.subarray(1)), myNonce);
|
|
63
|
+
const tx = this.graph.find(txid);
|
|
64
|
+
if (!tx)
|
|
65
|
+
throw new Error(`missing tx for txid ${txid}`);
|
|
66
|
+
const cosigners = getArkPsbtFields(tx.root, 0, CosignerPublicKey).map((c) => hex.encode(c.key.subarray(1)) // xonly pubkey
|
|
67
|
+
);
|
|
68
|
+
const pubNonces = [];
|
|
69
|
+
for (const cosigner of cosigners) {
|
|
70
|
+
const nonce = noncesByPubkey.get(cosigner);
|
|
71
|
+
if (!nonce) {
|
|
72
|
+
throw new Error(`missing nonce for cosigner ${cosigner}`);
|
|
73
|
+
}
|
|
74
|
+
pubNonces.push(nonce.pubNonce);
|
|
75
|
+
}
|
|
76
|
+
const aggregateNonce = musig2.aggregateNonces(pubNonces);
|
|
77
|
+
this.aggregateNonces.set(txid, { pubNonce: aggregateNonce });
|
|
78
|
+
return {
|
|
79
|
+
hasAllNonces: this.aggregateNonces.size === this.myNonces?.size,
|
|
80
|
+
};
|
|
47
81
|
}
|
|
48
|
-
sign() {
|
|
82
|
+
async sign() {
|
|
49
83
|
if (!this.graph)
|
|
50
84
|
throw ErrMissingVtxoGraph;
|
|
51
85
|
if (!this.aggregateNonces)
|
|
@@ -128,9 +162,8 @@ export async function validateTreeSigs(finalAggregatedKey, sharedOutputAmount, v
|
|
|
128
162
|
function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
129
163
|
// generate P2TR script from musig2 final key
|
|
130
164
|
const pkScript = Script.encode(["OP_1", finalKey.slice(1)]);
|
|
131
|
-
const txid = hex.encode(sha256x2(tx.toBytes(true)).reverse());
|
|
132
165
|
// if the input is the root input, return the shared output amount
|
|
133
|
-
if (
|
|
166
|
+
if (tx.id === graph.txid) {
|
|
134
167
|
return {
|
|
135
168
|
amount: sharedOutputAmount,
|
|
136
169
|
script: pkScript,
|
|
@@ -140,7 +173,7 @@ function getPrevOutput(finalKey, graph, sharedOutputAmount, tx) {
|
|
|
140
173
|
const parentInput = tx.getInput(0);
|
|
141
174
|
if (!parentInput.txid)
|
|
142
175
|
throw new Error("missing parent input txid");
|
|
143
|
-
const parentTxid = hex.encode(
|
|
176
|
+
const parentTxid = hex.encode(parentInput.txid);
|
|
144
177
|
const parent = graph.find(parentTxid);
|
|
145
178
|
if (!parent)
|
|
146
179
|
throw new Error("parent tx not found");
|