@arkade-os/sdk 0.3.0-alpha.7 → 0.3.1-alpha.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 +115 -14
- package/dist/cjs/adapters/expo.js +8 -0
- package/dist/cjs/arknote/index.js +3 -3
- package/dist/cjs/forfeit.js +5 -2
- package/dist/cjs/identity/singleKey.js +5 -4
- package/dist/cjs/index.js +7 -3
- package/dist/cjs/{bip322 → intent}/index.js +37 -55
- package/dist/cjs/providers/ark.js +62 -23
- package/dist/cjs/providers/expoArk.js +82 -0
- package/dist/cjs/providers/expoIndexer.js +105 -0
- package/dist/cjs/providers/indexer.js +3 -1
- package/dist/cjs/providers/utils.js +122 -0
- package/dist/cjs/script/base.js +1 -2
- package/dist/cjs/script/tapscript.js +20 -21
- package/dist/cjs/script/vhtlc.js +2 -2
- package/dist/cjs/tree/signingSession.js +7 -8
- package/dist/cjs/tree/txTree.js +3 -4
- package/dist/cjs/tree/validation.js +2 -3
- package/dist/cjs/utils/arkTransaction.js +117 -12
- package/dist/cjs/utils/unknownFields.js +5 -5
- package/dist/cjs/wallet/index.js +1 -1
- package/dist/cjs/wallet/onchain.js +4 -5
- package/dist/cjs/wallet/serviceWorker/utils.js +2 -9
- package/dist/cjs/wallet/serviceWorker/wallet.js +4 -8
- package/dist/cjs/wallet/serviceWorker/worker.js +25 -23
- package/dist/cjs/wallet/unroll.js +6 -7
- package/dist/cjs/wallet/utils.js +11 -0
- package/dist/cjs/wallet/vtxo-manager.js +381 -0
- package/dist/cjs/wallet/wallet.js +130 -143
- package/dist/esm/adapters/expo.js +3 -0
- package/dist/esm/arknote/index.js +2 -2
- package/dist/esm/forfeit.js +4 -1
- package/dist/esm/identity/singleKey.js +7 -6
- package/dist/esm/index.js +7 -6
- package/dist/esm/{bip322 → intent}/index.js +31 -48
- package/dist/esm/providers/ark.js +62 -23
- package/dist/esm/providers/expoArk.js +78 -0
- package/dist/esm/providers/expoIndexer.js +101 -0
- package/dist/esm/providers/indexer.js +3 -1
- package/dist/esm/providers/utils.js +87 -0
- package/dist/esm/script/base.js +1 -2
- package/dist/esm/script/tapscript.js +1 -2
- package/dist/esm/script/vhtlc.js +1 -1
- package/dist/esm/tree/signingSession.js +8 -9
- package/dist/esm/tree/txTree.js +3 -4
- package/dist/esm/tree/validation.js +2 -3
- package/dist/esm/utils/arkTransaction.js +108 -5
- package/dist/esm/utils/unknownFields.js +1 -1
- package/dist/esm/wallet/index.js +1 -1
- package/dist/esm/wallet/onchain.js +1 -2
- package/dist/esm/wallet/serviceWorker/utils.js +1 -8
- package/dist/esm/wallet/serviceWorker/wallet.js +5 -9
- package/dist/esm/wallet/serviceWorker/worker.js +26 -24
- package/dist/esm/wallet/unroll.js +2 -3
- package/dist/esm/wallet/utils.js +8 -0
- package/dist/esm/wallet/vtxo-manager.js +372 -0
- package/dist/esm/wallet/wallet.js +124 -137
- 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 +1 -1
- package/dist/types/identity/singleKey.d.ts +1 -1
- package/dist/types/index.d.ts +8 -7
- package/dist/types/intent/index.d.ts +41 -0
- package/dist/types/providers/ark.d.ts +190 -22
- package/dist/types/providers/expoArk.d.ts +22 -0
- package/dist/types/providers/expoIndexer.d.ts +18 -0
- package/dist/types/providers/indexer.d.ts +8 -8
- package/dist/types/providers/utils.d.ts +18 -0
- package/dist/types/script/base.d.ts +3 -2
- package/dist/types/tree/signingSession.d.ts +10 -10
- package/dist/types/utils/anchor.d.ts +2 -2
- package/dist/types/utils/arkTransaction.d.ts +16 -4
- package/dist/types/utils/unknownFields.d.ts +2 -2
- package/dist/types/wallet/index.d.ts +47 -7
- package/dist/types/wallet/onchain.d.ts +1 -1
- 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 +3 -1
- package/dist/types/wallet/unroll.d.ts +1 -1
- package/dist/types/wallet/utils.d.ts +2 -0
- package/dist/types/wallet/vtxo-manager.d.ts +207 -0
- package/dist/types/wallet/wallet.d.ts +16 -4
- 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
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import { OP } from "@scure/btc-signer
|
|
2
|
-
import { Transaction, SigHash } from "@scure/btc-signer/transaction.js";
|
|
3
|
-
import { Script } from "@scure/btc-signer/script.js";
|
|
4
|
-
import { ErrMissingData, ErrMissingInputs, ErrMissingWitnessUtxo, } from './errors.js';
|
|
1
|
+
import { OP, Transaction, Script, SigHash } from "@scure/btc-signer";
|
|
5
2
|
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
6
|
-
import { base64 } from "@scure/base";
|
|
7
3
|
/**
|
|
8
|
-
*
|
|
4
|
+
* Intent proof implementation for Bitcoin message signing.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
6
|
+
* Intent proof defines a standard for signing Bitcoin messages as well as proving
|
|
11
7
|
* ownership of coins. This namespace provides utilities for creating and
|
|
12
|
-
* validating
|
|
8
|
+
* validating Intent proof.
|
|
13
9
|
*
|
|
10
|
+
* it is greatly inspired by BIP322.
|
|
14
11
|
* @see https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
|
|
15
12
|
*
|
|
16
13
|
* @example
|
|
17
14
|
* ```typescript
|
|
18
|
-
* // Create a
|
|
19
|
-
* const proof =
|
|
15
|
+
* // Create a Intent proof
|
|
16
|
+
* const proof = Intent.create(
|
|
20
17
|
* "Hello Bitcoin!",
|
|
21
18
|
* [input],
|
|
22
19
|
* [output]
|
|
@@ -25,65 +22,46 @@ import { base64 } from "@scure/base";
|
|
|
25
22
|
* // Sign the proof
|
|
26
23
|
* const signedProof = await identity.sign(proof);
|
|
27
24
|
*
|
|
28
|
-
* // Extract the signature
|
|
29
|
-
* const signature = BIP322.signature(signedProof);
|
|
30
|
-
* ```
|
|
31
25
|
*/
|
|
32
|
-
export var
|
|
33
|
-
(function (
|
|
26
|
+
export var Intent;
|
|
27
|
+
(function (Intent) {
|
|
34
28
|
/**
|
|
35
|
-
* Creates a new
|
|
29
|
+
* Creates a new Intent proof unsigned transaction.
|
|
36
30
|
*
|
|
37
31
|
* This function constructs a special transaction that can be signed to prove
|
|
38
32
|
* ownership of VTXOs and UTXOs. The proof includes the message to be
|
|
39
33
|
* signed and the inputs/outputs that demonstrate ownership.
|
|
40
34
|
*
|
|
41
|
-
* @param message - The
|
|
35
|
+
* @param message - The Intent message to be signed
|
|
42
36
|
* @param inputs - Array of transaction inputs to prove ownership of
|
|
43
37
|
* @param outputs - Optional array of transaction outputs
|
|
44
|
-
* @returns An unsigned
|
|
38
|
+
* @returns An unsigned Intent proof transaction
|
|
45
39
|
*/
|
|
46
40
|
function create(message, inputs, outputs = []) {
|
|
47
41
|
if (inputs.length == 0)
|
|
48
|
-
throw
|
|
42
|
+
throw new Error("intent proof requires at least one input");
|
|
49
43
|
if (!validateInputs(inputs))
|
|
50
|
-
throw
|
|
44
|
+
throw new Error("invalid inputs");
|
|
51
45
|
if (!validateOutputs(outputs))
|
|
52
|
-
throw
|
|
46
|
+
throw new Error("invalid outputs");
|
|
53
47
|
// create the initial transaction to spend
|
|
54
48
|
const toSpend = craftToSpendTx(message, inputs[0].witnessUtxo.script);
|
|
55
49
|
// create the transaction to sign
|
|
56
50
|
return craftToSignTx(toSpend, inputs, outputs);
|
|
57
51
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* Finalizes and extracts the FullProof transaction into a BIP-322 signature.
|
|
61
|
-
*
|
|
62
|
-
* This function takes a signed proof transaction and converts it into a
|
|
63
|
-
* base64-encoded signature string. If the proof's inputs have special
|
|
64
|
-
* spending conditions, a custom finalizer can be provided.
|
|
65
|
-
*
|
|
66
|
-
* @param signedProof - The signed BIP-322 proof transaction
|
|
67
|
-
* @param finalizer - Optional custom finalizer function
|
|
68
|
-
* @returns Base64-encoded BIP-322 signature
|
|
69
|
-
*/
|
|
70
|
-
function signature(signedProof, finalizer = (tx) => tx.finalize()) {
|
|
71
|
-
finalizer(signedProof);
|
|
72
|
-
return base64.encode(signedProof.extract());
|
|
73
|
-
}
|
|
74
|
-
BIP322.signature = signature;
|
|
75
|
-
})(BIP322 || (BIP322 = {}));
|
|
52
|
+
Intent.create = create;
|
|
53
|
+
})(Intent || (Intent = {}));
|
|
76
54
|
const OP_RETURN_EMPTY_PKSCRIPT = new Uint8Array([OP.RETURN]);
|
|
77
55
|
const ZERO_32 = new Uint8Array(32).fill(0);
|
|
78
56
|
const MAX_INDEX = 0xffffffff;
|
|
79
|
-
const
|
|
57
|
+
const TAG_INTENT_PROOF = "ark-intent-proof-message";
|
|
80
58
|
function validateInput(input) {
|
|
81
59
|
if (input.index === undefined)
|
|
82
|
-
throw
|
|
60
|
+
throw new Error("intent proof input requires index");
|
|
83
61
|
if (input.txid === undefined)
|
|
84
|
-
throw
|
|
62
|
+
throw new Error("intent proof input requires txid");
|
|
85
63
|
if (input.witnessUtxo === undefined)
|
|
86
|
-
throw
|
|
64
|
+
throw new Error("intent proof input requires witness utxo");
|
|
87
65
|
return true;
|
|
88
66
|
}
|
|
89
67
|
function validateInputs(inputs) {
|
|
@@ -92,9 +70,9 @@ function validateInputs(inputs) {
|
|
|
92
70
|
}
|
|
93
71
|
function validateOutput(output) {
|
|
94
72
|
if (output.amount === undefined)
|
|
95
|
-
throw
|
|
73
|
+
throw new Error("intent proof output requires amount");
|
|
96
74
|
if (output.script === undefined)
|
|
97
|
-
throw
|
|
75
|
+
throw new Error("intent proof output requires script");
|
|
98
76
|
return true;
|
|
99
77
|
}
|
|
100
78
|
function validateOutputs(outputs) {
|
|
@@ -102,7 +80,7 @@ function validateOutputs(outputs) {
|
|
|
102
80
|
return true;
|
|
103
81
|
}
|
|
104
82
|
// craftToSpendTx creates the initial transaction that will be spent in the proof
|
|
105
|
-
|
|
83
|
+
function craftToSpendTx(message, pkScript) {
|
|
106
84
|
const messageHash = hashMessage(message);
|
|
107
85
|
const tx = new Transaction({
|
|
108
86
|
version: 0,
|
|
@@ -148,11 +126,16 @@ function craftToSignTx(toSpend, inputs, outputs) {
|
|
|
148
126
|
sighashType: SigHash.ALL,
|
|
149
127
|
});
|
|
150
128
|
// add other inputs
|
|
151
|
-
for (const input of inputs) {
|
|
129
|
+
for (const [i, input] of inputs.entries()) {
|
|
152
130
|
tx.addInput({
|
|
153
131
|
...input,
|
|
154
132
|
sighashType: SigHash.ALL,
|
|
155
133
|
});
|
|
134
|
+
if (input.unknown?.length) {
|
|
135
|
+
tx.updateInput(i + 1, {
|
|
136
|
+
unknown: input.unknown,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
156
139
|
}
|
|
157
140
|
// add the special OP_RETURN output if no outputs are provided
|
|
158
141
|
if (outputs.length === 0) {
|
|
@@ -172,5 +155,5 @@ function craftToSignTx(toSpend, inputs, outputs) {
|
|
|
172
155
|
return tx;
|
|
173
156
|
}
|
|
174
157
|
function hashMessage(message) {
|
|
175
|
-
return schnorr.utils.taggedHash(
|
|
158
|
+
return schnorr.utils.taggedHash(TAG_INTENT_PROOF, new TextEncoder().encode(message));
|
|
176
159
|
}
|
|
@@ -32,25 +32,36 @@ export class RestArkProvider {
|
|
|
32
32
|
}
|
|
33
33
|
const fromServer = await response.json();
|
|
34
34
|
return {
|
|
35
|
-
...fromServer,
|
|
36
|
-
vtxoTreeExpiry: BigInt(fromServer.vtxoTreeExpiry ?? 0),
|
|
37
|
-
unilateralExitDelay: BigInt(fromServer.unilateralExitDelay ?? 0),
|
|
38
|
-
roundInterval: BigInt(fromServer.roundInterval ?? 0),
|
|
39
|
-
dust: BigInt(fromServer.dust ?? 0),
|
|
40
|
-
utxoMinAmount: BigInt(fromServer.utxoMinAmount ?? 0),
|
|
41
|
-
utxoMaxAmount: BigInt(fromServer.utxoMaxAmount ?? -1),
|
|
42
|
-
vtxoMinAmount: BigInt(fromServer.vtxoMinAmount ?? 0),
|
|
43
|
-
vtxoMaxAmount: BigInt(fromServer.vtxoMaxAmount ?? -1),
|
|
44
35
|
boardingExitDelay: BigInt(fromServer.boardingExitDelay ?? 0),
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
checkpointTapscript: fromServer.checkpointTapscript ?? "",
|
|
37
|
+
deprecatedSigners: fromServer.deprecatedSigners?.map((signer) => ({
|
|
38
|
+
cutoffDate: BigInt(signer.cutoffDate ?? 0),
|
|
39
|
+
pubkey: signer.pubkey ?? "",
|
|
40
|
+
})) ?? [],
|
|
41
|
+
digest: fromServer.digest ?? "",
|
|
42
|
+
dust: BigInt(fromServer.dust ?? 0),
|
|
43
|
+
fees: fromServer.fees,
|
|
44
|
+
forfeitAddress: fromServer.forfeitAddress ?? "",
|
|
45
|
+
forfeitPubkey: fromServer.forfeitPubkey ?? "",
|
|
46
|
+
network: fromServer.network ?? "",
|
|
47
|
+
scheduledSession: "scheduledSession" in fromServer &&
|
|
48
|
+
fromServer.scheduledSession != null
|
|
47
49
|
? {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
duration: BigInt(fromServer.scheduledSession.duration ?? 0),
|
|
51
|
+
nextStartTime: BigInt(fromServer.scheduledSession.nextStartTime ?? 0),
|
|
52
|
+
nextEndTime: BigInt(fromServer.scheduledSession.nextEndTime ?? 0),
|
|
53
|
+
period: BigInt(fromServer.scheduledSession.period ?? 0),
|
|
52
54
|
}
|
|
53
55
|
: undefined,
|
|
56
|
+
serviceStatus: fromServer.serviceStatus ?? {},
|
|
57
|
+
sessionDuration: BigInt(fromServer.sessionDuration ?? 0),
|
|
58
|
+
signerPubkey: fromServer.signerPubkey ?? "",
|
|
59
|
+
unilateralExitDelay: BigInt(fromServer.unilateralExitDelay ?? 0),
|
|
60
|
+
utxoMaxAmount: BigInt(fromServer.utxoMaxAmount ?? -1),
|
|
61
|
+
utxoMinAmount: BigInt(fromServer.utxoMinAmount ?? 0),
|
|
62
|
+
version: fromServer.version ?? "",
|
|
63
|
+
vtxoMaxAmount: BigInt(fromServer.vtxoMaxAmount ?? -1),
|
|
64
|
+
vtxoMinAmount: BigInt(fromServer.vtxoMinAmount ?? 0),
|
|
54
65
|
};
|
|
55
66
|
}
|
|
56
67
|
async submitTx(signedArkTx, checkpointTxs) {
|
|
@@ -61,8 +72,8 @@ export class RestArkProvider {
|
|
|
61
72
|
"Content-Type": "application/json",
|
|
62
73
|
},
|
|
63
74
|
body: JSON.stringify({
|
|
64
|
-
signedArkTx
|
|
65
|
-
checkpointTxs
|
|
75
|
+
signedArkTx,
|
|
76
|
+
checkpointTxs,
|
|
66
77
|
}),
|
|
67
78
|
});
|
|
68
79
|
if (!response.ok) {
|
|
@@ -111,7 +122,7 @@ export class RestArkProvider {
|
|
|
111
122
|
},
|
|
112
123
|
body: JSON.stringify({
|
|
113
124
|
intent: {
|
|
114
|
-
|
|
125
|
+
proof: intent.proof,
|
|
115
126
|
message: intent.message,
|
|
116
127
|
},
|
|
117
128
|
}),
|
|
@@ -132,7 +143,7 @@ export class RestArkProvider {
|
|
|
132
143
|
},
|
|
133
144
|
body: JSON.stringify({
|
|
134
145
|
proof: {
|
|
135
|
-
|
|
146
|
+
proof: intent.proof,
|
|
136
147
|
message: intent.message,
|
|
137
148
|
},
|
|
138
149
|
}),
|
|
@@ -305,6 +316,31 @@ export class RestArkProvider {
|
|
|
305
316
|
}
|
|
306
317
|
}
|
|
307
318
|
}
|
|
319
|
+
async getPendingTxs(intent) {
|
|
320
|
+
const url = `${this.serverUrl}/v1/tx/pending`;
|
|
321
|
+
const response = await fetch(url, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: {
|
|
324
|
+
"Content-Type": "application/json",
|
|
325
|
+
},
|
|
326
|
+
body: JSON.stringify({ intent }),
|
|
327
|
+
});
|
|
328
|
+
if (!response.ok) {
|
|
329
|
+
const errorText = await response.text();
|
|
330
|
+
try {
|
|
331
|
+
const grpcError = JSON.parse(errorText);
|
|
332
|
+
// gRPC errors usually have a message and code field
|
|
333
|
+
throw new Error(`Failed to get pending transactions: ${grpcError.message || grpcError.error || errorText}`);
|
|
334
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
335
|
+
}
|
|
336
|
+
catch (_) {
|
|
337
|
+
// If JSON parse fails, use the raw error text
|
|
338
|
+
throw new Error(`Failed to get pending transactions: ${errorText}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
return data.pendingTxs;
|
|
343
|
+
}
|
|
308
344
|
parseSettlementEvent(data) {
|
|
309
345
|
// Check for BatchStarted event
|
|
310
346
|
if (data.batchStarted) {
|
|
@@ -383,6 +419,10 @@ export class RestArkProvider {
|
|
|
383
419
|
signature: data.treeSignature.signature,
|
|
384
420
|
};
|
|
385
421
|
}
|
|
422
|
+
// TODO: Handle TreeNoncesEvent when implemented server-side
|
|
423
|
+
if (data.treeNonces) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
386
426
|
// Skip heartbeat events
|
|
387
427
|
if (data.heartbeat) {
|
|
388
428
|
return null;
|
|
@@ -426,17 +466,16 @@ function encodeMusig2Nonces(nonces) {
|
|
|
426
466
|
for (const [txid, nonce] of nonces) {
|
|
427
467
|
noncesObject[txid] = hex.encode(nonce.pubNonce);
|
|
428
468
|
}
|
|
429
|
-
return
|
|
469
|
+
return noncesObject;
|
|
430
470
|
}
|
|
431
471
|
function encodeMusig2Signatures(signatures) {
|
|
432
472
|
const sigObject = {};
|
|
433
473
|
for (const [txid, sig] of signatures) {
|
|
434
474
|
sigObject[txid] = hex.encode(sig.encode());
|
|
435
475
|
}
|
|
436
|
-
return
|
|
476
|
+
return sigObject;
|
|
437
477
|
}
|
|
438
|
-
function decodeMusig2Nonces(
|
|
439
|
-
const noncesObject = JSON.parse(str);
|
|
478
|
+
function decodeMusig2Nonces(noncesObject) {
|
|
440
479
|
return new Map(Object.entries(noncesObject).map(([txid, nonce]) => {
|
|
441
480
|
if (typeof nonce !== "string") {
|
|
442
481
|
throw new Error("invalid nonce");
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { RestArkProvider, isFetchTimeoutError, } from './ark.js';
|
|
2
|
+
import { getExpoFetch, sseStreamIterator } from './utils.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 './utils.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
|
+
}
|
|
@@ -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
|
|
@@ -55,3 +55,90 @@ export async function* eventSourceIterator(eventSource) {
|
|
|
55
55
|
eventSource.removeEventListener("error", errorHandler);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Dynamically imports expo/fetch with fallback to standard fetch.
|
|
60
|
+
* @returns A fetch function suitable for SSE streaming
|
|
61
|
+
*/
|
|
62
|
+
export async function getExpoFetch(options) {
|
|
63
|
+
const requireExpo = options?.requireExpo ?? false;
|
|
64
|
+
try {
|
|
65
|
+
const expoFetchModule = await import("expo/fetch");
|
|
66
|
+
console.debug("Using expo/fetch for streaming");
|
|
67
|
+
return expoFetchModule.fetch;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (requireExpo) {
|
|
71
|
+
throw new Error("expo/fetch is unavailable in this environment. " +
|
|
72
|
+
"Please ensure expo/fetch is installed and properly configured.");
|
|
73
|
+
}
|
|
74
|
+
console.warn("Using standard fetch instead of expo/fetch. " +
|
|
75
|
+
"Streaming may not be fully supported in some environments.", error);
|
|
76
|
+
return fetch;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Generic SSE stream processor using fetch API with ReadableStream.
|
|
81
|
+
* Handles SSE format parsing, buffer management, and abort signals.
|
|
82
|
+
*
|
|
83
|
+
* @param url - The SSE endpoint URL
|
|
84
|
+
* @param abortSignal - Signal to abort the stream
|
|
85
|
+
* @param fetchFn - Fetch function to use (defaults to standard fetch)
|
|
86
|
+
* @param headers - Additional headers to send
|
|
87
|
+
* @param parseData - Function to parse and yield data from SSE events
|
|
88
|
+
*/
|
|
89
|
+
export async function* sseStreamIterator(url, abortSignal, fetchFn, headers, parseData) {
|
|
90
|
+
const fetchController = new AbortController();
|
|
91
|
+
const cleanup = () => fetchController.abort();
|
|
92
|
+
abortSignal?.addEventListener("abort", cleanup, { once: true });
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetchFn(url, {
|
|
95
|
+
headers: {
|
|
96
|
+
Accept: "text/event-stream",
|
|
97
|
+
...headers,
|
|
98
|
+
},
|
|
99
|
+
signal: fetchController.signal,
|
|
100
|
+
});
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error(`Unexpected status ${response.status} when fetching SSE stream`);
|
|
103
|
+
}
|
|
104
|
+
if (!response.body) {
|
|
105
|
+
throw new Error("Response body is null");
|
|
106
|
+
}
|
|
107
|
+
const reader = response.body.getReader();
|
|
108
|
+
const decoder = new TextDecoder();
|
|
109
|
+
let buffer = "";
|
|
110
|
+
while (!abortSignal?.aborted) {
|
|
111
|
+
const { done, value } = await reader.read();
|
|
112
|
+
if (done) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
buffer += decoder.decode(value, { stream: true });
|
|
116
|
+
const lines = buffer.split("\n");
|
|
117
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
118
|
+
const line = lines[i].trim();
|
|
119
|
+
if (!line)
|
|
120
|
+
continue;
|
|
121
|
+
if (line.startsWith("data:")) {
|
|
122
|
+
const jsonStr = line.substring(5).trim();
|
|
123
|
+
if (!jsonStr)
|
|
124
|
+
continue;
|
|
125
|
+
try {
|
|
126
|
+
const data = JSON.parse(jsonStr);
|
|
127
|
+
const parsed = parseData(data);
|
|
128
|
+
if (parsed !== null) {
|
|
129
|
+
yield parsed;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (parseError) {
|
|
133
|
+
console.error("Failed to parse SSE data:", parseError);
|
|
134
|
+
throw parseError;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
buffer = lines[lines.length - 1];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
abortSignal?.removeEventListener("abort", cleanup);
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/esm/script/base.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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';
|
|
@@ -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';
|