@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.4
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 +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/psbt.d.ts +12 -2
- package/{packages/sdk/dist → dist}/psbt.js +17 -12
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +345 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Esplora Client - Platform-agnostic HTTP client for Bitcoin blockchain queries
|
|
3
|
+
*
|
|
4
|
+
* Uses standard fetch API (works in browser, Node.js 18+, React Native)
|
|
5
|
+
* Supports mempool.space API for testnet/mainnet
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { hexToBytes } from "../crypto";
|
|
9
|
+
|
|
10
|
+
export interface EsploraTransaction {
|
|
11
|
+
txid: string;
|
|
12
|
+
version: number;
|
|
13
|
+
locktime: number;
|
|
14
|
+
vin: EsploraVin[];
|
|
15
|
+
vout: EsploraVout[];
|
|
16
|
+
size: number;
|
|
17
|
+
weight: number;
|
|
18
|
+
fee: number;
|
|
19
|
+
status: EsploraStatus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EsploraVin {
|
|
23
|
+
txid: string;
|
|
24
|
+
vout: number;
|
|
25
|
+
prevout: EsploraVout | null;
|
|
26
|
+
scriptsig: string;
|
|
27
|
+
scriptsig_asm: string;
|
|
28
|
+
witness?: string[];
|
|
29
|
+
is_coinbase: boolean;
|
|
30
|
+
sequence: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EsploraVout {
|
|
34
|
+
scriptpubkey: string;
|
|
35
|
+
scriptpubkey_asm: string;
|
|
36
|
+
scriptpubkey_type: string;
|
|
37
|
+
scriptpubkey_address?: string;
|
|
38
|
+
value: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EsploraStatus {
|
|
42
|
+
confirmed: boolean;
|
|
43
|
+
block_height?: number;
|
|
44
|
+
block_hash?: string;
|
|
45
|
+
block_time?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EsploraAddressInfo {
|
|
49
|
+
address: string;
|
|
50
|
+
chain_stats: {
|
|
51
|
+
funded_txo_count: number;
|
|
52
|
+
funded_txo_sum: number;
|
|
53
|
+
spent_txo_count: number;
|
|
54
|
+
spent_txo_sum: number;
|
|
55
|
+
tx_count: number;
|
|
56
|
+
};
|
|
57
|
+
mempool_stats: {
|
|
58
|
+
funded_txo_count: number;
|
|
59
|
+
funded_txo_sum: number;
|
|
60
|
+
spent_txo_count: number;
|
|
61
|
+
spent_txo_sum: number;
|
|
62
|
+
tx_count: number;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EsploraUtxo {
|
|
67
|
+
txid: string;
|
|
68
|
+
vout: number;
|
|
69
|
+
status: EsploraStatus;
|
|
70
|
+
value: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface EsploraMerkleProof {
|
|
74
|
+
block_height: number;
|
|
75
|
+
merkle: string[];
|
|
76
|
+
pos: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type EsploraNetwork = "mainnet" | "testnet" | "testnet4" | "signet" | "regtest";
|
|
80
|
+
|
|
81
|
+
const NETWORK_URLS: Record<EsploraNetwork, string> = {
|
|
82
|
+
mainnet: "https://mempool.space/api",
|
|
83
|
+
testnet: "https://mempool.space/testnet/api",
|
|
84
|
+
testnet4: "https://mempool.space/testnet4/api",
|
|
85
|
+
signet: "https://mempool.space/signet/api",
|
|
86
|
+
regtest: "http://localhost:2140",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export class EsploraClient {
|
|
90
|
+
private baseUrl: string;
|
|
91
|
+
private network: EsploraNetwork;
|
|
92
|
+
|
|
93
|
+
constructor(
|
|
94
|
+
networkOrUrl: EsploraNetwork | string = "testnet",
|
|
95
|
+
customBaseUrl?: string
|
|
96
|
+
) {
|
|
97
|
+
if (customBaseUrl) {
|
|
98
|
+
this.baseUrl = customBaseUrl.replace(/\/$/, "");
|
|
99
|
+
this.network = networkOrUrl as EsploraNetwork;
|
|
100
|
+
} else if (networkOrUrl in NETWORK_URLS) {
|
|
101
|
+
this.network = networkOrUrl as EsploraNetwork;
|
|
102
|
+
this.baseUrl = NETWORK_URLS[this.network];
|
|
103
|
+
} else {
|
|
104
|
+
// Assume it's a custom URL
|
|
105
|
+
this.baseUrl = networkOrUrl.replace(/\/$/, "");
|
|
106
|
+
this.network = "testnet";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async fetch<T>(endpoint: string): Promise<T> {
|
|
111
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
112
|
+
const res = await fetch(url);
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new Error(`Esplora API error: ${res.status} ${res.statusText}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const contentType = res.headers.get("content-type");
|
|
119
|
+
if (contentType?.includes("application/json")) {
|
|
120
|
+
return res.json() as Promise<T>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// For text responses (like block height)
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
return text as T;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =========================================================================
|
|
129
|
+
// Address endpoints
|
|
130
|
+
// =========================================================================
|
|
131
|
+
|
|
132
|
+
async getAddress(address: string): Promise<EsploraAddressInfo> {
|
|
133
|
+
return this.fetch<EsploraAddressInfo>(`/address/${address}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async getAddressTxs(
|
|
137
|
+
address: string,
|
|
138
|
+
lastSeenTxid?: string
|
|
139
|
+
): Promise<EsploraTransaction[]> {
|
|
140
|
+
const endpoint = lastSeenTxid
|
|
141
|
+
? `/address/${address}/txs/chain/${lastSeenTxid}`
|
|
142
|
+
: `/address/${address}/txs`;
|
|
143
|
+
return this.fetch<EsploraTransaction[]>(endpoint);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getAddressTxsMempool(address: string): Promise<EsploraTransaction[]> {
|
|
147
|
+
return this.fetch<EsploraTransaction[]>(`/address/${address}/txs/mempool`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getAddressUtxos(address: string): Promise<EsploraUtxo[]> {
|
|
151
|
+
return this.fetch<EsploraUtxo[]>(`/address/${address}/utxo`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =========================================================================
|
|
155
|
+
// Transaction endpoints
|
|
156
|
+
// =========================================================================
|
|
157
|
+
|
|
158
|
+
async getTransaction(txid: string): Promise<EsploraTransaction> {
|
|
159
|
+
return this.fetch<EsploraTransaction>(`/tx/${txid}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getTxStatus(txid: string): Promise<EsploraStatus> {
|
|
163
|
+
return this.fetch<EsploraStatus>(`/tx/${txid}/status`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async getTxHex(txid: string): Promise<string> {
|
|
167
|
+
return this.fetch<string>(`/tx/${txid}/hex`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getTxRaw(txid: string): Promise<Uint8Array> {
|
|
171
|
+
const hex = await this.getTxHex(txid);
|
|
172
|
+
return hexToBytes(hex);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async getTxMerkleProof(txid: string): Promise<EsploraMerkleProof> {
|
|
176
|
+
return this.fetch<EsploraMerkleProof>(`/tx/${txid}/merkle-proof`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async getTxOutspend(
|
|
180
|
+
txid: string,
|
|
181
|
+
vout: number
|
|
182
|
+
): Promise<{
|
|
183
|
+
spent: boolean;
|
|
184
|
+
txid?: string;
|
|
185
|
+
vin?: number;
|
|
186
|
+
status?: EsploraStatus;
|
|
187
|
+
}> {
|
|
188
|
+
return this.fetch(`/tx/${txid}/outspend/${vout}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// =========================================================================
|
|
192
|
+
// Block endpoints
|
|
193
|
+
// =========================================================================
|
|
194
|
+
|
|
195
|
+
async getBlockHeight(): Promise<number> {
|
|
196
|
+
const height = await this.fetch<string>("/blocks/tip/height");
|
|
197
|
+
return parseInt(height, 10);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async getBlockHash(height: number): Promise<string> {
|
|
201
|
+
return this.fetch<string>(`/block-height/${height}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async getBlockHeader(hash: string): Promise<string> {
|
|
205
|
+
return this.fetch<string>(`/block/${hash}/header`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async getBlockTxids(hash: string): Promise<string[]> {
|
|
209
|
+
return this.fetch<string[]>(`/block/${hash}/txids`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// =========================================================================
|
|
213
|
+
// Helper methods
|
|
214
|
+
// =========================================================================
|
|
215
|
+
|
|
216
|
+
async getConfirmations(txid: string): Promise<number> {
|
|
217
|
+
const status = await this.getTxStatus(txid);
|
|
218
|
+
if (!status.confirmed || status.block_height === undefined) {
|
|
219
|
+
return 0;
|
|
220
|
+
}
|
|
221
|
+
const tipHeight = await this.getBlockHeight();
|
|
222
|
+
return tipHeight - status.block_height + 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async waitForTransaction(
|
|
226
|
+
address: string,
|
|
227
|
+
timeoutMs: number = 600000,
|
|
228
|
+
pollIntervalMs: number = 5000
|
|
229
|
+
): Promise<EsploraTransaction | null> {
|
|
230
|
+
const startTime = Date.now();
|
|
231
|
+
|
|
232
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
233
|
+
// Check mempool first
|
|
234
|
+
const mempoolTxs = await this.getAddressTxsMempool(address);
|
|
235
|
+
if (mempoolTxs.length > 0) {
|
|
236
|
+
return mempoolTxs[0];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check confirmed
|
|
240
|
+
const confirmedTxs = await this.getAddressTxs(address);
|
|
241
|
+
if (confirmedTxs.length > 0) {
|
|
242
|
+
return confirmedTxs[0];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await sleep(pollIntervalMs);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async waitForConfirmations(
|
|
252
|
+
txid: string,
|
|
253
|
+
requiredConfirmations: number = 6,
|
|
254
|
+
timeoutMs: number = 3600000,
|
|
255
|
+
pollIntervalMs: number = 30000
|
|
256
|
+
): Promise<number> {
|
|
257
|
+
const startTime = Date.now();
|
|
258
|
+
|
|
259
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
260
|
+
const confirmations = await this.getConfirmations(txid);
|
|
261
|
+
if (confirmations >= requiredConfirmations) {
|
|
262
|
+
return confirmations;
|
|
263
|
+
}
|
|
264
|
+
await sleep(pollIntervalMs);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return await this.getConfirmations(txid);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Find a deposit to a specific address matching the expected amount
|
|
272
|
+
*/
|
|
273
|
+
async findDeposit(
|
|
274
|
+
address: string,
|
|
275
|
+
expectedAmount?: bigint
|
|
276
|
+
): Promise<{ tx: EsploraTransaction; vout: number; amount: number } | null> {
|
|
277
|
+
// Check mempool first
|
|
278
|
+
const mempoolTxs = await this.getAddressTxsMempool(address);
|
|
279
|
+
for (const tx of mempoolTxs) {
|
|
280
|
+
const result = this.findMatchingOutput(tx, address, expectedAmount);
|
|
281
|
+
if (result) return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check confirmed transactions
|
|
285
|
+
const confirmedTxs = await this.getAddressTxs(address);
|
|
286
|
+
for (const tx of confirmedTxs) {
|
|
287
|
+
const result = this.findMatchingOutput(tx, address, expectedAmount);
|
|
288
|
+
if (result) return result;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private findMatchingOutput(
|
|
295
|
+
tx: EsploraTransaction,
|
|
296
|
+
address: string,
|
|
297
|
+
expectedAmount?: bigint
|
|
298
|
+
): { tx: EsploraTransaction; vout: number; amount: number } | null {
|
|
299
|
+
for (let i = 0; i < tx.vout.length; i++) {
|
|
300
|
+
const output = tx.vout[i];
|
|
301
|
+
if (output.scriptpubkey_address === address) {
|
|
302
|
+
if (
|
|
303
|
+
expectedAmount === undefined ||
|
|
304
|
+
BigInt(output.value) === expectedAmount
|
|
305
|
+
) {
|
|
306
|
+
return { tx, vout: i, amount: output.value };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
getNetwork(): EsploraNetwork {
|
|
314
|
+
return this.network;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
getBaseUrl(): string {
|
|
318
|
+
return this.baseUrl;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// =========================================================================
|
|
323
|
+
// Utility functions
|
|
324
|
+
// =========================================================================
|
|
325
|
+
|
|
326
|
+
function sleep(ms: number): Promise<void> {
|
|
327
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Default client for testnet
|
|
331
|
+
export const esploraTestnet = new EsploraClient("testnet");
|
|
332
|
+
export const esploraMainnet = new EsploraClient("mainnet");
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mempool.space API Client for SPV Verification
|
|
3
|
+
*
|
|
4
|
+
* Extended client for fetching Bitcoin SPV proof data.
|
|
5
|
+
* Builds on EsploraClient with additional SPV-specific methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EsploraClient, type EsploraNetwork, type EsploraMerkleProof } from "./esplora";
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Types
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface BlockHeader {
|
|
15
|
+
height: number;
|
|
16
|
+
hash: string;
|
|
17
|
+
version: number;
|
|
18
|
+
previousBlockHash: string;
|
|
19
|
+
merkleRoot: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
bits: number;
|
|
22
|
+
nonce: number;
|
|
23
|
+
/** Raw 80-byte header in hex */
|
|
24
|
+
rawHeader: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TransactionInfo {
|
|
28
|
+
txid: string;
|
|
29
|
+
confirmed: boolean;
|
|
30
|
+
blockHeight?: number;
|
|
31
|
+
blockHash?: string;
|
|
32
|
+
blockTime?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SPVProofData {
|
|
36
|
+
txInfo: TransactionInfo;
|
|
37
|
+
blockHeader: BlockHeader;
|
|
38
|
+
merkleProof: EsploraMerkleProof & { blockHash: string };
|
|
39
|
+
confirmations: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// MempoolClient (extends EsploraClient with SPV methods)
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
export class MempoolClient extends EsploraClient {
|
|
47
|
+
constructor(network: EsploraNetwork = "testnet") {
|
|
48
|
+
super(network);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get transaction info including block hash
|
|
53
|
+
*/
|
|
54
|
+
async getTransactionInfo(txid: string): Promise<TransactionInfo> {
|
|
55
|
+
const tx = await this.getTransaction(txid);
|
|
56
|
+
return {
|
|
57
|
+
txid: tx.txid,
|
|
58
|
+
confirmed: tx.status.confirmed,
|
|
59
|
+
blockHeight: tx.status.block_height,
|
|
60
|
+
blockHash: tx.status.block_hash,
|
|
61
|
+
blockTime: tx.status.block_time,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get block header with raw 80-byte header
|
|
67
|
+
*/
|
|
68
|
+
async getBlockHeaderFull(blockHash: string): Promise<BlockHeader> {
|
|
69
|
+
const baseUrl = this.getBaseUrl();
|
|
70
|
+
|
|
71
|
+
// Fetch block info
|
|
72
|
+
const blockRes = await fetch(`${baseUrl}/block/${blockHash}`);
|
|
73
|
+
if (!blockRes.ok) {
|
|
74
|
+
throw new Error(`Failed to fetch block: ${blockRes.statusText}`);
|
|
75
|
+
}
|
|
76
|
+
const blockInfo = await blockRes.json();
|
|
77
|
+
|
|
78
|
+
// Fetch raw header (80 bytes hex)
|
|
79
|
+
const rawHeader = await this.getBlockHeader(blockHash);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
height: blockInfo.height,
|
|
83
|
+
hash: blockHash,
|
|
84
|
+
version: blockInfo.version,
|
|
85
|
+
previousBlockHash: blockInfo.previousblockhash,
|
|
86
|
+
merkleRoot: blockInfo.merkle_root,
|
|
87
|
+
timestamp: blockInfo.timestamp,
|
|
88
|
+
bits: blockInfo.bits,
|
|
89
|
+
nonce: blockInfo.nonce,
|
|
90
|
+
rawHeader,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get block header by height
|
|
96
|
+
*/
|
|
97
|
+
async getBlockHeaderByHeight(height: number): Promise<BlockHeader> {
|
|
98
|
+
const blockHash = await this.getBlockHash(height);
|
|
99
|
+
return this.getBlockHeaderFull(blockHash);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all data needed for SPV verification
|
|
104
|
+
*/
|
|
105
|
+
async getSPVProofData(txid: string): Promise<SPVProofData> {
|
|
106
|
+
// Get transaction info
|
|
107
|
+
const txInfo = await this.getTransactionInfo(txid);
|
|
108
|
+
|
|
109
|
+
if (!txInfo.confirmed || !txInfo.blockHash) {
|
|
110
|
+
throw new Error("Transaction not confirmed yet");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get block header
|
|
114
|
+
const blockHeader = await this.getBlockHeaderFull(txInfo.blockHash);
|
|
115
|
+
|
|
116
|
+
// Get merkle proof
|
|
117
|
+
const merkleProof = await this.getTxMerkleProof(txid);
|
|
118
|
+
|
|
119
|
+
// Get confirmations
|
|
120
|
+
const tipHeight = await this.getBlockHeight();
|
|
121
|
+
const confirmations = tipHeight - txInfo.blockHeight! + 1;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
txInfo,
|
|
125
|
+
blockHeader,
|
|
126
|
+
merkleProof: {
|
|
127
|
+
...merkleProof,
|
|
128
|
+
blockHash: txInfo.blockHash,
|
|
129
|
+
},
|
|
130
|
+
confirmations,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// =============================================================================
|
|
136
|
+
// Utility Functions
|
|
137
|
+
// =============================================================================
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reverse bytes (for Bitcoin internal byte order)
|
|
141
|
+
*/
|
|
142
|
+
export function reverseBytes(bytes: Uint8Array): Uint8Array {
|
|
143
|
+
const reversed = new Uint8Array(bytes.length);
|
|
144
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
145
|
+
reversed[i] = bytes[bytes.length - 1 - i];
|
|
146
|
+
}
|
|
147
|
+
return reversed;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// hexToBytes / bytesToHex live in ../crypto (single source); re-exported here
|
|
151
|
+
// to preserve this module's public surface.
|
|
152
|
+
export { hexToBytes, bytesToHex } from "../crypto";
|
|
153
|
+
|
|
154
|
+
// =============================================================================
|
|
155
|
+
// Default Instances
|
|
156
|
+
// =============================================================================
|
|
157
|
+
|
|
158
|
+
export const mempoolTestnet = new MempoolClient("testnet");
|
|
159
|
+
export const mempoolMainnet = new MempoolClient("mainnet");
|