@secondlayer/stacks 2.8.1 → 2.9.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 +88 -0
- package/dist/shared/{stacks-sfpr4b0y.js → stacks-53bjdb6p.js} +2 -2
- package/dist/shared/{stacks-9z833ww5.js → stacks-5nmz5ad9.js} +3 -3
- package/dist/shared/{stacks-9z833ww5.js.map → stacks-5nmz5ad9.js.map} +1 -1
- package/dist/shared/{stacks-s9148m6h.js → stacks-8mj5jw77.js} +6 -4
- package/dist/shared/{stacks-s9148m6h.js.map → stacks-8mj5jw77.js.map} +2 -2
- package/dist/shared/stacks-cdvn8qbh.cjs +20 -0
- package/dist/shared/stacks-cdvn8qbh.cjs.map +10 -0
- package/dist/shared/{stacks-y9txtw3p.cjs → stacks-maamjqma.cjs} +2 -20
- package/dist/shared/{stacks-y9txtw3p.cjs.map → stacks-maamjqma.cjs.map} +3 -4
- package/dist/shared/stacks-q1mjkt15.js +34 -0
- package/dist/shared/stacks-q1mjkt15.js.map +10 -0
- package/dist/shared/{stacks-a63xq06y.js → stacks-z1j5d6fd.js} +3 -21
- package/dist/shared/{stacks-a63xq06y.js.map → stacks-z1j5d6fd.js.map} +3 -4
- package/dist/src/actions/index.js +7 -5
- package/dist/src/actions/index.js.map +1 -1
- package/dist/src/bitcoin/index.cjs +679 -0
- package/dist/src/bitcoin/index.cjs.map +19 -0
- package/dist/src/bitcoin/index.d.cts +743 -0
- package/dist/src/bitcoin/index.d.ts +743 -0
- package/dist/src/bitcoin/index.js +704 -0
- package/dist/src/bitcoin/index.js.map +19 -0
- package/dist/src/bns/index.js +4 -3
- package/dist/src/bns/index.js.map +2 -2
- package/dist/src/index.js +6 -4
- package/dist/src/index.js.map +2 -2
- package/dist/src/pox/index.js +5 -4
- package/dist/src/pox/index.js.map +2 -2
- package/dist/src/sbtc/index.js +5 -4
- package/dist/src/sbtc/index.js.map +2 -2
- package/dist/src/stackingdao/index.js +4 -3
- package/dist/src/stackingdao/index.js.map +2 -2
- package/package.json +8 -4
- /package/dist/shared/{stacks-sfpr4b0y.js.map → stacks-53bjdb6p.js.map} +0 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// src/bitcoin/index.ts
|
|
2
|
+
var exports_bitcoin = {};
|
|
3
|
+
__export(exports_bitcoin, {
|
|
4
|
+
verifyBitcoinPayment: () => verifyBitcoinPayment,
|
|
5
|
+
stripWitness: () => stripWitness,
|
|
6
|
+
rootFromProof: () => rootFromProof,
|
|
7
|
+
reverseBytes: () => reverseBytes,
|
|
8
|
+
parseOutputScript: () => parseOutputScript,
|
|
9
|
+
parseBlockHeader: () => parseBlockHeader,
|
|
10
|
+
parseBitcoinTx: () => parseBitcoinTx,
|
|
11
|
+
merkleRoot: () => merkleRoot,
|
|
12
|
+
isClarity6Active: () => isClarity6Active,
|
|
13
|
+
getBurnBlockHeight: () => getBurnBlockHeight,
|
|
14
|
+
formatBitcoinAddress: () => formatBitcoinAddress,
|
|
15
|
+
fallbackProofSource: () => fallbackProofSource,
|
|
16
|
+
esploraSource: () => esploraSource,
|
|
17
|
+
encodeMerkleProofArgs: () => encodeMerkleProofArgs,
|
|
18
|
+
doubleSha256: () => doubleSha256,
|
|
19
|
+
decodeTxOutput: () => decodeTxOutput,
|
|
20
|
+
buildTxProof: () => buildTxProof,
|
|
21
|
+
buildMerkleProof: () => buildMerkleProof,
|
|
22
|
+
blockHash: () => blockHash,
|
|
23
|
+
bitcoinVerifier: () => bitcoinVerifier,
|
|
24
|
+
bitcoinTxid: () => bitcoinTxid,
|
|
25
|
+
bitcoinRpcSource: () => bitcoinRpcSource,
|
|
26
|
+
SPV_ADAPTER_CONTRACTS: () => SPV_ADAPTER_CONTRACTS,
|
|
27
|
+
SPV_ADAPTER_ABI: () => SPV_ADAPTER_ABI,
|
|
28
|
+
BtcReader: () => BtcReader
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(exports_bitcoin);
|
|
31
|
+
|
|
32
|
+
// src/bitcoin/serialize.ts
|
|
33
|
+
function doubleSha256(bytes) {
|
|
34
|
+
return import_sha2.sha256(import_sha2.sha256(bytes));
|
|
35
|
+
}
|
|
36
|
+
function reverseBytes(bytes) {
|
|
37
|
+
const out = new Uint8Array(bytes.length);
|
|
38
|
+
for (let i = 0;i < bytes.length; i++) {
|
|
39
|
+
out[i] = bytes[bytes.length - 1 - i];
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class BtcReader {
|
|
45
|
+
data;
|
|
46
|
+
view;
|
|
47
|
+
offset = 0;
|
|
48
|
+
constructor(data) {
|
|
49
|
+
this.data = data;
|
|
50
|
+
this.view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
51
|
+
}
|
|
52
|
+
get length() {
|
|
53
|
+
return this.data.length;
|
|
54
|
+
}
|
|
55
|
+
readUInt8() {
|
|
56
|
+
const v = this.view.getUint8(this.offset);
|
|
57
|
+
this.offset += 1;
|
|
58
|
+
return v;
|
|
59
|
+
}
|
|
60
|
+
peekUInt8() {
|
|
61
|
+
return this.view.getUint8(this.offset);
|
|
62
|
+
}
|
|
63
|
+
readUInt32LE() {
|
|
64
|
+
const v = this.view.getUint32(this.offset, true);
|
|
65
|
+
this.offset += 4;
|
|
66
|
+
return v;
|
|
67
|
+
}
|
|
68
|
+
readUInt64LE() {
|
|
69
|
+
const v = this.view.getBigUint64(this.offset, true);
|
|
70
|
+
this.offset += 8;
|
|
71
|
+
return v;
|
|
72
|
+
}
|
|
73
|
+
readBytes(n) {
|
|
74
|
+
if (this.offset + n > this.data.length) {
|
|
75
|
+
throw new Error(`Bitcoin tx underflow: need ${n} bytes at offset ${this.offset}, have ${this.data.length}`);
|
|
76
|
+
}
|
|
77
|
+
const slice = this.data.slice(this.offset, this.offset + n);
|
|
78
|
+
this.offset += n;
|
|
79
|
+
return slice;
|
|
80
|
+
}
|
|
81
|
+
readVarInt() {
|
|
82
|
+
const first = this.readUInt8();
|
|
83
|
+
if (first < 253)
|
|
84
|
+
return first;
|
|
85
|
+
if (first === 253) {
|
|
86
|
+
const v = this.view.getUint16(this.offset, true);
|
|
87
|
+
this.offset += 2;
|
|
88
|
+
return v;
|
|
89
|
+
}
|
|
90
|
+
if (first === 254) {
|
|
91
|
+
return this.readUInt32LE();
|
|
92
|
+
}
|
|
93
|
+
const big = this.readUInt64LE();
|
|
94
|
+
if (big > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
95
|
+
throw new Error("Bitcoin varint exceeds safe integer range");
|
|
96
|
+
}
|
|
97
|
+
return Number(big);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function scanTx(rawTx) {
|
|
101
|
+
const r = new BtcReader(rawTx);
|
|
102
|
+
const version = r.readUInt32LE();
|
|
103
|
+
let hasWitness = false;
|
|
104
|
+
if (r.peekUInt8() === 0) {
|
|
105
|
+
r.readUInt8();
|
|
106
|
+
const flag = r.readUInt8();
|
|
107
|
+
if (flag !== 1) {
|
|
108
|
+
throw new Error(`Unexpected SegWit flag 0x${flag.toString(16)}`);
|
|
109
|
+
}
|
|
110
|
+
hasWitness = true;
|
|
111
|
+
}
|
|
112
|
+
const bodyStart = r.offset;
|
|
113
|
+
const inputCount = r.readVarInt();
|
|
114
|
+
const inputs = [];
|
|
115
|
+
for (let i = 0;i < inputCount; i++) {
|
|
116
|
+
const prevTxid = r.readBytes(32);
|
|
117
|
+
const vout = r.readUInt32LE();
|
|
118
|
+
const scriptSig = r.readBytes(r.readVarInt());
|
|
119
|
+
const sequence = r.readUInt32LE();
|
|
120
|
+
inputs.push({ prevTxid, vout, scriptSig, sequence });
|
|
121
|
+
}
|
|
122
|
+
const outputCount = r.readVarInt();
|
|
123
|
+
const outputs = [];
|
|
124
|
+
for (let i = 0;i < outputCount; i++) {
|
|
125
|
+
const value = r.readUInt64LE();
|
|
126
|
+
const scriptPubKey = r.readBytes(r.readVarInt());
|
|
127
|
+
outputs.push({ value, scriptPubKey });
|
|
128
|
+
}
|
|
129
|
+
const bodyEnd = r.offset;
|
|
130
|
+
if (hasWitness) {
|
|
131
|
+
for (let i = 0;i < inputCount; i++) {
|
|
132
|
+
const itemCount = r.readVarInt();
|
|
133
|
+
for (let j = 0;j < itemCount; j++) {
|
|
134
|
+
r.readBytes(r.readVarInt());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const locktime = r.readUInt32LE();
|
|
139
|
+
const legacy = concatBytes(rawTx.slice(0, 4), rawTx.slice(bodyStart, bodyEnd), rawTx.slice(r.offset - 4, r.offset));
|
|
140
|
+
return { version, hasWitness, inputs, outputs, locktime, legacy };
|
|
141
|
+
}
|
|
142
|
+
function parseBitcoinTx(rawTx) {
|
|
143
|
+
const s = scanTx(rawTx);
|
|
144
|
+
return {
|
|
145
|
+
version: s.version,
|
|
146
|
+
hasWitness: s.hasWitness,
|
|
147
|
+
inputs: s.inputs,
|
|
148
|
+
outputs: s.outputs,
|
|
149
|
+
locktime: s.locktime,
|
|
150
|
+
txidInternal: doubleSha256(s.legacy)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function stripWitness(rawTx) {
|
|
154
|
+
return scanTx(rawTx).legacy;
|
|
155
|
+
}
|
|
156
|
+
function bitcoinTxid(rawTx, { display = false } = {}) {
|
|
157
|
+
const txid = doubleSha256(scanTx(rawTx).legacy);
|
|
158
|
+
return display ? reverseBytes(txid) : txid;
|
|
159
|
+
}
|
|
160
|
+
function parseBlockHeader(header) {
|
|
161
|
+
if (header.length !== 80) {
|
|
162
|
+
throw new Error(`Bitcoin block header must be 80 bytes, got ${header.length}`);
|
|
163
|
+
}
|
|
164
|
+
const r = new BtcReader(header);
|
|
165
|
+
const version = r.readUInt32LE();
|
|
166
|
+
const prevBlock = r.readBytes(32);
|
|
167
|
+
const merkleRoot = r.readBytes(32);
|
|
168
|
+
const timestamp = r.readUInt32LE();
|
|
169
|
+
const bits = r.readUInt32LE();
|
|
170
|
+
const nonce = r.readUInt32LE();
|
|
171
|
+
return { version, prevBlock, merkleRoot, timestamp, bits, nonce };
|
|
172
|
+
}
|
|
173
|
+
function blockHash(header, { display = false } = {}) {
|
|
174
|
+
const hash = doubleSha256(header);
|
|
175
|
+
return display ? reverseBytes(hash) : hash;
|
|
176
|
+
}
|
|
177
|
+
// src/bitcoin/merkle.ts
|
|
178
|
+
function hashPair(left, right) {
|
|
179
|
+
return doubleSha256(concatBytes(left, right));
|
|
180
|
+
}
|
|
181
|
+
function nextLevel(level) {
|
|
182
|
+
const next = [];
|
|
183
|
+
for (let i = 0;i < level.length; i += 2) {
|
|
184
|
+
const left = level[i];
|
|
185
|
+
if (left === undefined)
|
|
186
|
+
throw new Error("merkle: missing left node");
|
|
187
|
+
const right = level[i + 1] ?? left;
|
|
188
|
+
next.push(hashPair(left, right));
|
|
189
|
+
}
|
|
190
|
+
return next;
|
|
191
|
+
}
|
|
192
|
+
function merkleRoot(txidsInternal) {
|
|
193
|
+
if (txidsInternal.length === 0) {
|
|
194
|
+
throw new Error("merkleRoot: empty tx list");
|
|
195
|
+
}
|
|
196
|
+
let level = txidsInternal.slice();
|
|
197
|
+
while (level.length > 1) {
|
|
198
|
+
level = nextLevel(level);
|
|
199
|
+
}
|
|
200
|
+
const root = level[0];
|
|
201
|
+
if (root === undefined)
|
|
202
|
+
throw new Error("merkleRoot: no root");
|
|
203
|
+
return root;
|
|
204
|
+
}
|
|
205
|
+
function buildMerkleProof(txidsInternal, txIndex) {
|
|
206
|
+
const txCount = txidsInternal.length;
|
|
207
|
+
if (txCount === 0) {
|
|
208
|
+
throw new Error("buildMerkleProof: empty tx list");
|
|
209
|
+
}
|
|
210
|
+
if (txIndex < 0 || txIndex >= txCount) {
|
|
211
|
+
throw new Error(`buildMerkleProof: txIndex ${txIndex} out of range for ${txCount} txs`);
|
|
212
|
+
}
|
|
213
|
+
const siblings = [];
|
|
214
|
+
let index = txIndex;
|
|
215
|
+
let level = txidsInternal.slice();
|
|
216
|
+
while (level.length > 1) {
|
|
217
|
+
const isRight = index % 2 === 1;
|
|
218
|
+
const siblingIndex = isRight ? index - 1 : index + 1;
|
|
219
|
+
const self = level[index];
|
|
220
|
+
if (self === undefined)
|
|
221
|
+
throw new Error("buildMerkleProof: missing node");
|
|
222
|
+
siblings.push(level[siblingIndex] ?? self);
|
|
223
|
+
level = nextLevel(level);
|
|
224
|
+
index = Math.floor(index / 2);
|
|
225
|
+
}
|
|
226
|
+
return { siblings, txIndex, txCount };
|
|
227
|
+
}
|
|
228
|
+
function rootFromProof(leafInternal, proof) {
|
|
229
|
+
let hash = leafInternal;
|
|
230
|
+
let index = proof.txIndex;
|
|
231
|
+
for (const sibling of proof.siblings) {
|
|
232
|
+
hash = index % 2 === 1 ? hashPair(sibling, hash) : hashPair(hash, sibling);
|
|
233
|
+
index = Math.floor(index / 2);
|
|
234
|
+
}
|
|
235
|
+
return hash;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/bitcoin/proof.ts
|
|
239
|
+
function normalizeTxid(txid) {
|
|
240
|
+
return without0x(txid).toLowerCase();
|
|
241
|
+
}
|
|
242
|
+
async function buildTxProof(source, params) {
|
|
243
|
+
const { txid, vout } = params;
|
|
244
|
+
const want = normalizeTxid(txid);
|
|
245
|
+
const [rawTx, block] = await Promise.all([
|
|
246
|
+
source.getRawTx(txid),
|
|
247
|
+
source.getBlockForTx(txid)
|
|
248
|
+
]);
|
|
249
|
+
const parsed = parseBitcoinTx(rawTx);
|
|
250
|
+
const gotDisplay = bytesToHex(reverseBytes(parsed.txidInternal));
|
|
251
|
+
if (gotDisplay !== want) {
|
|
252
|
+
throw new Error(`source returned a tx whose id ${gotDisplay} does not match requested ${want}`);
|
|
253
|
+
}
|
|
254
|
+
const atIndex = block.txidsInternal[block.txIndex];
|
|
255
|
+
if (!atIndex || bytesToHex(atIndex) !== bytesToHex(parsed.txidInternal)) {
|
|
256
|
+
throw new Error(`source txIndex ${block.txIndex} does not point at tx ${want}`);
|
|
257
|
+
}
|
|
258
|
+
const merkle = buildMerkleProof(block.txidsInternal, block.txIndex);
|
|
259
|
+
const headerRoot = parseBlockHeader(block.header).merkleRoot;
|
|
260
|
+
const computed = rootFromProof(parsed.txidInternal, merkle);
|
|
261
|
+
if (bytesToHex(computed) !== bytesToHex(headerRoot)) {
|
|
262
|
+
throw new Error("constructed merkle proof does not reconcile with the block header merkle root");
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
rawTx,
|
|
266
|
+
txidInternal: parsed.txidInternal,
|
|
267
|
+
vout,
|
|
268
|
+
merkle,
|
|
269
|
+
header: block.header,
|
|
270
|
+
height: block.height
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function fallbackProofSource(sources) {
|
|
274
|
+
if (sources.length === 0) {
|
|
275
|
+
throw new Error("fallbackProofSource: at least one source is required");
|
|
276
|
+
}
|
|
277
|
+
async function firstSuccess(fn) {
|
|
278
|
+
let lastError;
|
|
279
|
+
for (const source of sources) {
|
|
280
|
+
try {
|
|
281
|
+
return await fn(source);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
lastError = error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
throw lastError;
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
getRawTx: (txid) => firstSuccess((s) => s.getRawTx(txid)),
|
|
290
|
+
getBlockForTx: (txid) => firstSuccess((s) => s.getBlockForTx(txid))
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function bitcoinRpcSource(config) {
|
|
294
|
+
const doFetch = config.fetch ?? fetch;
|
|
295
|
+
async function rpc(method, rpcParams) {
|
|
296
|
+
const headers = {
|
|
297
|
+
"content-type": "application/json"
|
|
298
|
+
};
|
|
299
|
+
if (config.auth) {
|
|
300
|
+
const basic = typeof config.auth === "string" ? config.auth : btoa(`${config.auth.username}:${config.auth.password}`);
|
|
301
|
+
headers.authorization = `Basic ${basic}`;
|
|
302
|
+
}
|
|
303
|
+
const res = await doFetch(config.url, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers,
|
|
306
|
+
body: JSON.stringify({
|
|
307
|
+
jsonrpc: "1.0",
|
|
308
|
+
id: "secondlayer",
|
|
309
|
+
method,
|
|
310
|
+
params: rpcParams
|
|
311
|
+
})
|
|
312
|
+
});
|
|
313
|
+
if (!res.ok) {
|
|
314
|
+
throw new Error(`bitcoin rpc ${method} failed: HTTP ${res.status}`);
|
|
315
|
+
}
|
|
316
|
+
const json = await res.json();
|
|
317
|
+
if (json.error) {
|
|
318
|
+
throw new Error(`bitcoin rpc ${method} error: ${json.error.message}`);
|
|
319
|
+
}
|
|
320
|
+
return json.result;
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
async getRawTx(txid) {
|
|
324
|
+
return hexToBytes(await rpc("getrawtransaction", [txid, false]));
|
|
325
|
+
},
|
|
326
|
+
async getBlockForTx(txid) {
|
|
327
|
+
const tx = await rpc("getrawtransaction", [
|
|
328
|
+
txid,
|
|
329
|
+
true
|
|
330
|
+
]);
|
|
331
|
+
if (!tx.blockhash) {
|
|
332
|
+
throw new Error(`tx ${txid} is not in a block (node needs -txindex, or the tx is unconfirmed)`);
|
|
333
|
+
}
|
|
334
|
+
const block = await rpc("getblock", [
|
|
335
|
+
tx.blockhash,
|
|
336
|
+
1
|
|
337
|
+
]);
|
|
338
|
+
const header = await rpc("getblockheader", [tx.blockhash, false]);
|
|
339
|
+
const txIndex = block.tx.indexOf(normalizeTxid(txid));
|
|
340
|
+
if (txIndex < 0) {
|
|
341
|
+
throw new Error(`tx ${txid} not found in block ${tx.blockhash}`);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
header: hexToBytes(header),
|
|
345
|
+
height: block.height,
|
|
346
|
+
txidsInternal: block.tx.map((t) => reverseBytes(hexToBytes(t))),
|
|
347
|
+
txIndex
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function esploraSource(config) {
|
|
353
|
+
const doFetch = config.fetch ?? fetch;
|
|
354
|
+
const base = config.url.replace(/\/+$/, "");
|
|
355
|
+
async function get(path) {
|
|
356
|
+
const res = await doFetch(`${base}${path}`);
|
|
357
|
+
if (!res.ok) {
|
|
358
|
+
throw new Error(`esplora GET ${path} failed: HTTP ${res.status}`);
|
|
359
|
+
}
|
|
360
|
+
return res;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
async getRawTx(txid) {
|
|
364
|
+
return hexToBytes((await (await get(`/tx/${txid}/hex`)).text()).trim());
|
|
365
|
+
},
|
|
366
|
+
async getBlockForTx(txid) {
|
|
367
|
+
const status = await (await get(`/tx/${txid}`)).json();
|
|
368
|
+
if (!status.status.confirmed) {
|
|
369
|
+
throw new Error(`tx ${txid} is unconfirmed`);
|
|
370
|
+
}
|
|
371
|
+
const blockHashHex = status.status.block_hash;
|
|
372
|
+
const header = (await (await get(`/block/${blockHashHex}/header`)).text()).trim();
|
|
373
|
+
const txids = await (await get(`/block/${blockHashHex}/txids`)).json();
|
|
374
|
+
const txIndex = txids.indexOf(normalizeTxid(txid));
|
|
375
|
+
if (txIndex < 0) {
|
|
376
|
+
throw new Error(`tx ${txid} not found in block ${blockHashHex}`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
header: hexToBytes(header),
|
|
380
|
+
height: status.status.block_height,
|
|
381
|
+
txidsInternal: txids.map((t) => reverseBytes(hexToBytes(t))),
|
|
382
|
+
txIndex
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
// src/bitcoin/codec.ts
|
|
388
|
+
function merkleDepth(txCount) {
|
|
389
|
+
let depth = 0;
|
|
390
|
+
let n = txCount;
|
|
391
|
+
while (n > 1) {
|
|
392
|
+
n = Math.ceil(n / 2);
|
|
393
|
+
depth++;
|
|
394
|
+
}
|
|
395
|
+
return depth;
|
|
396
|
+
}
|
|
397
|
+
var MAX_SIBLINGS = 24;
|
|
398
|
+
function assertHash32(bytes, label) {
|
|
399
|
+
if (bytes.length !== 32) {
|
|
400
|
+
throw new Error(`${label} must be 32 bytes, got ${bytes.length}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function encodeMerkleProofArgs(params) {
|
|
404
|
+
const { leaf, root, proof } = params;
|
|
405
|
+
assertHash32(leaf, "merkle leaf");
|
|
406
|
+
assertHash32(root, "merkle root");
|
|
407
|
+
if (proof.txIndex < 0 || proof.txIndex >= proof.txCount) {
|
|
408
|
+
throw new Error(`tx-index ${proof.txIndex} out of range for tx-count ${proof.txCount}`);
|
|
409
|
+
}
|
|
410
|
+
if (proof.siblings.length > MAX_SIBLINGS) {
|
|
411
|
+
throw new Error(`proof has ${proof.siblings.length} siblings, native verify-merkle-proof caps at ${MAX_SIBLINGS}`);
|
|
412
|
+
}
|
|
413
|
+
const expectedDepth = merkleDepth(proof.txCount);
|
|
414
|
+
if (proof.siblings.length !== expectedDepth) {
|
|
415
|
+
throw new Error(`proof has ${proof.siblings.length} siblings but tx-count ${proof.txCount} requires exactly ${expectedDepth}`);
|
|
416
|
+
}
|
|
417
|
+
for (const [i, sibling] of proof.siblings.entries()) {
|
|
418
|
+
assertHash32(sibling, `sibling ${i}`);
|
|
419
|
+
}
|
|
420
|
+
return [
|
|
421
|
+
bufferCV(leaf),
|
|
422
|
+
bufferCV(root),
|
|
423
|
+
uintCV(proof.txIndex),
|
|
424
|
+
uintCV(proof.txCount),
|
|
425
|
+
listCV(proof.siblings.map((s) => bufferCV(s)))
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
function expectBuffer(cv, label) {
|
|
429
|
+
if (cv?.type !== "buffer") {
|
|
430
|
+
throw new Error(`expected ${label} to be a buffer, got ${cv?.type}`);
|
|
431
|
+
}
|
|
432
|
+
return hexToBytes(cv.value);
|
|
433
|
+
}
|
|
434
|
+
function decodeTxOutput(cv) {
|
|
435
|
+
const tuple = cv.type === "ok" ? cv.value : cv;
|
|
436
|
+
if (tuple.type !== "tuple") {
|
|
437
|
+
throw new Error(`expected a tuple, got ${tuple.type}`);
|
|
438
|
+
}
|
|
439
|
+
const fields = tuple.value;
|
|
440
|
+
const amount = fields.amount;
|
|
441
|
+
if (amount?.type !== "uint") {
|
|
442
|
+
throw new Error(`expected tuple field "amount" to be uint, got ${amount?.type}`);
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
script: expectBuffer(fields.script, 'tuple field "script"'),
|
|
446
|
+
amount: amount.value,
|
|
447
|
+
txid: expectBuffer(fields.txid, 'tuple field "txid"')
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
var OP_DUP = 118;
|
|
451
|
+
var OP_HASH160 = 169;
|
|
452
|
+
var OP_EQUALVERIFY = 136;
|
|
453
|
+
var OP_CHECKSIG = 172;
|
|
454
|
+
var OP_EQUAL = 135;
|
|
455
|
+
var OP_RETURN = 106;
|
|
456
|
+
var OP_0 = 0;
|
|
457
|
+
var OP_1 = 81;
|
|
458
|
+
function parseOutputScript(script) {
|
|
459
|
+
const n = script.length;
|
|
460
|
+
if (n === 25 && script[0] === OP_DUP && script[1] === OP_HASH160 && script[2] === 20 && script[23] === OP_EQUALVERIFY && script[24] === OP_CHECKSIG) {
|
|
461
|
+
return { type: "p2pkh", hash: script.slice(3, 23) };
|
|
462
|
+
}
|
|
463
|
+
if (n === 23 && script[0] === OP_HASH160 && script[1] === 20 && script[22] === OP_EQUAL) {
|
|
464
|
+
return { type: "p2sh", hash: script.slice(2, 22) };
|
|
465
|
+
}
|
|
466
|
+
if (n === 22 && script[0] === OP_0 && script[1] === 20) {
|
|
467
|
+
return { type: "p2wpkh", hash: script.slice(2, 22) };
|
|
468
|
+
}
|
|
469
|
+
if (n === 34 && script[0] === OP_0 && script[1] === 32) {
|
|
470
|
+
return { type: "p2wsh", hash: script.slice(2, 34) };
|
|
471
|
+
}
|
|
472
|
+
if (n === 34 && script[0] === OP_1 && script[1] === 32) {
|
|
473
|
+
return { type: "p2tr", hash: script.slice(2, 34) };
|
|
474
|
+
}
|
|
475
|
+
if (n >= 1 && script[0] === OP_RETURN) {
|
|
476
|
+
return { type: "op_return", data: script.slice(1) };
|
|
477
|
+
}
|
|
478
|
+
return { type: "unknown" };
|
|
479
|
+
}
|
|
480
|
+
// src/bitcoin/constants.ts
|
|
481
|
+
var SPV_ADAPTER_CONTRACTS = {};
|
|
482
|
+
// src/bitcoin/abi/spvAdapter.ts
|
|
483
|
+
var SPV_ADAPTER_ABI = {
|
|
484
|
+
functions: [
|
|
485
|
+
{
|
|
486
|
+
name: "get-tx-output",
|
|
487
|
+
access: "read-only",
|
|
488
|
+
args: [
|
|
489
|
+
{ name: "tx", type: { buff: { length: 4096 } } },
|
|
490
|
+
{ name: "vout", type: "uint128" }
|
|
491
|
+
],
|
|
492
|
+
outputs: {
|
|
493
|
+
response: {
|
|
494
|
+
ok: {
|
|
495
|
+
tuple: [
|
|
496
|
+
{ name: "script", type: { buff: { length: 1024 } } },
|
|
497
|
+
{ name: "amount", type: "uint128" },
|
|
498
|
+
{ name: "txid", type: { buff: { length: 32 } } }
|
|
499
|
+
]
|
|
500
|
+
},
|
|
501
|
+
error: "uint128"
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
name: "verify-merkle",
|
|
507
|
+
access: "read-only",
|
|
508
|
+
args: [
|
|
509
|
+
{ name: "leaf", type: { buff: { length: 32 } } },
|
|
510
|
+
{ name: "root", type: { buff: { length: 32 } } },
|
|
511
|
+
{ name: "tx-index", type: "uint128" },
|
|
512
|
+
{ name: "tx-count", type: "uint128" },
|
|
513
|
+
{
|
|
514
|
+
name: "siblings",
|
|
515
|
+
type: { list: { type: { buff: { length: 32 } }, length: 24 } }
|
|
516
|
+
}
|
|
517
|
+
],
|
|
518
|
+
outputs: "bool"
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: "was-tx-mined",
|
|
522
|
+
access: "read-only",
|
|
523
|
+
args: [
|
|
524
|
+
{ name: "header", type: { buff: { length: 80 } } },
|
|
525
|
+
{ name: "height", type: "uint128" },
|
|
526
|
+
{ name: "leaf", type: { buff: { length: 32 } } },
|
|
527
|
+
{ name: "tx-index", type: "uint128" },
|
|
528
|
+
{ name: "tx-count", type: "uint128" },
|
|
529
|
+
{
|
|
530
|
+
name: "siblings",
|
|
531
|
+
type: { list: { type: { buff: { length: 32 } }, length: 24 } }
|
|
532
|
+
}
|
|
533
|
+
],
|
|
534
|
+
outputs: { response: { ok: "bool", error: "uint128" } }
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
};
|
|
538
|
+
// src/bitcoin/activation.ts
|
|
539
|
+
async function getBurnBlockHeight(client) {
|
|
540
|
+
const info = await client.request("/v2/info");
|
|
541
|
+
if (typeof info?.burn_block_height !== "number") {
|
|
542
|
+
throw new Error("could not read burn_block_height from /v2/info");
|
|
543
|
+
}
|
|
544
|
+
return info.burn_block_height;
|
|
545
|
+
}
|
|
546
|
+
async function isClarity6Active(client, gate = {}) {
|
|
547
|
+
if (gate.activationBurnHeight == null) {
|
|
548
|
+
throw new Error("Clarity 6 (SIP-044 / Epoch 4.0) activation burn height is not yet known — pass { activationBurnHeight } once the vote selects it");
|
|
549
|
+
}
|
|
550
|
+
const current = await getBurnBlockHeight(client);
|
|
551
|
+
return current >= gate.activationBurnHeight;
|
|
552
|
+
}
|
|
553
|
+
// src/bitcoin/verifier.ts
|
|
554
|
+
function clarityToBool(cv) {
|
|
555
|
+
const value = cv.type === "ok" ? cv.value : cv;
|
|
556
|
+
if (value.type === "true")
|
|
557
|
+
return true;
|
|
558
|
+
if (value.type === "false")
|
|
559
|
+
return false;
|
|
560
|
+
throw new Error(`expected a boolean result, got ${value.type}`);
|
|
561
|
+
}
|
|
562
|
+
function bitcoinVerifier(client, config) {
|
|
563
|
+
const { contract, sender } = config;
|
|
564
|
+
async function verifyMerkleProof(input) {
|
|
565
|
+
const args = encodeMerkleProofArgs(input);
|
|
566
|
+
const result = await readContract(client, {
|
|
567
|
+
contract,
|
|
568
|
+
functionName: "verify-merkle",
|
|
569
|
+
args,
|
|
570
|
+
sender
|
|
571
|
+
});
|
|
572
|
+
return clarityToBool(result);
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
verifyMerkleProof,
|
|
576
|
+
verifySpvProof(proof) {
|
|
577
|
+
const root = parseBlockHeader(proof.header).merkleRoot;
|
|
578
|
+
return verifyMerkleProof({
|
|
579
|
+
leaf: proof.txidInternal,
|
|
580
|
+
root,
|
|
581
|
+
proof: proof.merkle
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
async wasTxMined(proof) {
|
|
585
|
+
const result = await readContract(client, {
|
|
586
|
+
contract,
|
|
587
|
+
functionName: "was-tx-mined",
|
|
588
|
+
args: [
|
|
589
|
+
bufferCV(proof.header),
|
|
590
|
+
uintCV(proof.height),
|
|
591
|
+
bufferCV(proof.txidInternal),
|
|
592
|
+
uintCV(proof.merkle.txIndex),
|
|
593
|
+
uintCV(proof.merkle.txCount),
|
|
594
|
+
listCV(proof.merkle.siblings.map((s) => bufferCV(s)))
|
|
595
|
+
],
|
|
596
|
+
sender
|
|
597
|
+
});
|
|
598
|
+
if (result.type === "err")
|
|
599
|
+
return false;
|
|
600
|
+
return clarityToBool(result);
|
|
601
|
+
},
|
|
602
|
+
async getTxOutput(rawTx, vout) {
|
|
603
|
+
const result = await readContract(client, {
|
|
604
|
+
contract,
|
|
605
|
+
functionName: "get-tx-output",
|
|
606
|
+
args: [bufferCV(rawTx), uintCV(vout)],
|
|
607
|
+
sender
|
|
608
|
+
});
|
|
609
|
+
if (result.type === "err") {
|
|
610
|
+
throw new Error(`get-tx-output failed on-chain: ${JSON.stringify(result.value)}`);
|
|
611
|
+
}
|
|
612
|
+
return decodeTxOutput(result);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
// src/bitcoin/address.ts
|
|
617
|
+
var NETWORK_PARAMS = {
|
|
618
|
+
mainnet: { p2pkh: 0, p2sh: 5, hrp: "bc" },
|
|
619
|
+
testnet: { p2pkh: 111, p2sh: 196, hrp: "tb" }
|
|
620
|
+
};
|
|
621
|
+
function base58Check(version, payload) {
|
|
622
|
+
const data = concatBytes(Uint8Array.of(version), payload);
|
|
623
|
+
const checksum = doubleSha256(data).slice(0, 4);
|
|
624
|
+
return base58.encode(concatBytes(data, checksum));
|
|
625
|
+
}
|
|
626
|
+
function formatBitcoinAddress(parsed, network = "mainnet") {
|
|
627
|
+
const params = NETWORK_PARAMS[network];
|
|
628
|
+
const hash = parsed.hash;
|
|
629
|
+
if (!hash)
|
|
630
|
+
return;
|
|
631
|
+
switch (parsed.type) {
|
|
632
|
+
case "p2pkh":
|
|
633
|
+
return base58Check(params.p2pkh, hash);
|
|
634
|
+
case "p2sh":
|
|
635
|
+
return base58Check(params.p2sh, hash);
|
|
636
|
+
case "p2wpkh":
|
|
637
|
+
case "p2wsh":
|
|
638
|
+
return bech32.encode(params.hrp, [0, ...bech32.toWords(hash)]);
|
|
639
|
+
case "p2tr":
|
|
640
|
+
return bech32m.encode(params.hrp, [1, ...bech32m.toWords(hash)]);
|
|
641
|
+
default:
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// src/bitcoin/actions.ts
|
|
646
|
+
async function verifyBitcoinPayment(client, params) {
|
|
647
|
+
const {
|
|
648
|
+
contract,
|
|
649
|
+
vout,
|
|
650
|
+
network = "mainnet",
|
|
651
|
+
expect,
|
|
652
|
+
authenticateHeader = true,
|
|
653
|
+
sender
|
|
654
|
+
} = params;
|
|
655
|
+
const proof = "proof" in params ? params.proof : await buildTxProof(params.source, { txid: params.txid, vout });
|
|
656
|
+
const verifier = bitcoinVerifier(client, { contract, sender });
|
|
657
|
+
const mined = authenticateHeader ? await verifier.wasTxMined(proof) : await verifier.verifySpvProof(proof);
|
|
658
|
+
const parsedTx = parseBitcoinTx(proof.rawTx);
|
|
659
|
+
const out = parsedTx.outputs[vout];
|
|
660
|
+
if (!out) {
|
|
661
|
+
throw new Error(`vout ${vout} out of range (tx has ${parsedTx.outputs.length} outputs)`);
|
|
662
|
+
}
|
|
663
|
+
const parsedScript = parseOutputScript(out.scriptPubKey);
|
|
664
|
+
const address = formatBitcoinAddress(parsedScript, network);
|
|
665
|
+
const output = {
|
|
666
|
+
vout,
|
|
667
|
+
script: out.scriptPubKey,
|
|
668
|
+
amount: out.value,
|
|
669
|
+
type: parsedScript.type,
|
|
670
|
+
address
|
|
671
|
+
};
|
|
672
|
+
const amountOk = expect?.amount === undefined || out.value === expect.amount;
|
|
673
|
+
const addressOk = expect?.address === undefined || address === expect.address;
|
|
674
|
+
const verified = mined && amountOk && addressOk;
|
|
675
|
+
return { verified, mined, output, proof };
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
//# debugId=1FFA7B1EF14A3F8864756E2164756E21
|
|
679
|
+
//# sourceMappingURL=index.cjs.map
|