@mainnet-cash/bcmr 2.7.23

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.
@@ -0,0 +1,978 @@
1
+ import {
2
+ initProviders,
3
+ disconnectProviders,
4
+ setupFetchMock,
5
+ removeFetchMock,
6
+ RegTestWallet,
7
+ OpReturnData,
8
+ SendRequest,
9
+ Network,
10
+ mine,
11
+ NFTCapability,
12
+ ElectrumNetworkProvider,
13
+ } from "mainnet-js";
14
+ import { AuthChain, BCMR } from "./Bcmr.js";
15
+ import { Registry } from "./bcmr-v2.schema.js";
16
+ import { binToHex, hexToBin, sha256, utf8ToBin } from "@bitauth/libauth";
17
+
18
+ beforeAll(async () => {
19
+ await initProviders();
20
+ });
21
+ afterAll(async () => {
22
+ await disconnectProviders();
23
+ });
24
+ afterEach(async () => {
25
+ BCMR.resetRegistries();
26
+ });
27
+
28
+ describe(`Test BCMR support`, () => {
29
+ const registry: Registry = {
30
+ $schema: "https://cashtokens.org/bcmr-v2.schema.json",
31
+ version: {
32
+ major: 0,
33
+ minor: 1,
34
+ patch: 0,
35
+ },
36
+ latestRevision: "2023-01-26T18:51:35.115Z",
37
+ registryIdentity: {
38
+ name: "example bcmr",
39
+ description: "example bcmr for tokens on chipnet",
40
+ },
41
+ identities: {
42
+ "0000000000000000000000000000000000000000000000000000000000000000": {
43
+ "2023-01-26T18:51:35.115Z": {
44
+ name: "test tokens",
45
+ description: "",
46
+ uris: {
47
+ icon: "https://example.com/nft",
48
+ },
49
+ token: {
50
+ category:
51
+ "0000000000000000000000000000000000000000000000000000000000000000",
52
+ symbol: "TOK",
53
+ decimals: 8,
54
+ nfts: {
55
+ description: "",
56
+ parse: {
57
+ bytecode: "00d2",
58
+ types: {
59
+ "00": {
60
+ name: "NFT Item 0",
61
+ description: "NFT Item 0 in the collection",
62
+ uris: {
63
+ icon: "https://example.com/nft/00.jpg",
64
+ },
65
+ },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ },
72
+ },
73
+ };
74
+
75
+ const registryContent = JSON.stringify(registry, null, 2);
76
+ const registryContentHashBin = sha256.hash(utf8ToBin(registryContent));
77
+ const registryContentHash = binToHex(registryContentHashBin);
78
+ const registryContentHashBinBitcoinByteOrder = registryContentHashBin;
79
+
80
+ test("Add metadata registry and get token info", async () => {
81
+ expect(
82
+ BCMR.getTokenInfo(
83
+ "0000000000000000000000000000000000000000000000000000000000000000"
84
+ )
85
+ ).toBe(undefined);
86
+ BCMR.addMetadataRegistry(registry);
87
+ const tokenInfo = BCMR.getTokenInfo(
88
+ "0000000000000000000000000000000000000000000000000000000000000000"
89
+ );
90
+ expect(tokenInfo?.token?.symbol).toBe("TOK");
91
+ expect(tokenInfo?.token?.decimals).toBe(8);
92
+
93
+ // check adding the same registry does not produce a duplicate
94
+ expect(BCMR.metadataRegistries.length).toBe(1);
95
+ BCMR.addMetadataRegistry(registry);
96
+ expect(BCMR.metadataRegistries.length).toBe(1);
97
+
98
+ expect(
99
+ BCMR.getTokenInfo(
100
+ "1111111111111111111111111111111111111111111111111111111111111111"
101
+ )
102
+ ).toBe(undefined);
103
+ });
104
+
105
+ test("Add metadata from uri and get token info", async () => {
106
+ setupFetchMock(
107
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
108
+ registry
109
+ );
110
+
111
+ expect(
112
+ BCMR.getTokenInfo(
113
+ "0000000000000000000000000000000000000000000000000000000000000000"
114
+ )
115
+ ).toBe(undefined);
116
+ await BCMR.addMetadataRegistryFromUri(
117
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
118
+ );
119
+ const tokenInfo = BCMR.getTokenInfo(
120
+ "0000000000000000000000000000000000000000000000000000000000000000"
121
+ );
122
+ expect(tokenInfo?.token?.symbol).toBe("TOK");
123
+ expect(tokenInfo?.token?.decimals).toBe(8);
124
+
125
+ // check adding the same registry does not produce a duplicate
126
+ expect(BCMR.metadataRegistries.length).toBe(1);
127
+ await BCMR.addMetadataRegistryFromUri(
128
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
129
+ );
130
+ expect(BCMR.metadataRegistries.length).toBe(1);
131
+
132
+ removeFetchMock(
133
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
134
+ );
135
+ });
136
+
137
+ test("Add metadata from uri with contenthash and get token info", async () => {
138
+ setupFetchMock(
139
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
140
+ registryContent
141
+ );
142
+
143
+ expect(
144
+ BCMR.getTokenInfo(
145
+ "0000000000000000000000000000000000000000000000000000000000000000"
146
+ )
147
+ ).toBe(undefined);
148
+ await expect(
149
+ BCMR.addMetadataRegistryFromUri(
150
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
151
+ "00"
152
+ )
153
+ ).rejects.toThrow("mismatch");
154
+ await BCMR.addMetadataRegistryFromUri(
155
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
156
+ registryContentHash
157
+ );
158
+ const tokenInfo = BCMR.getTokenInfo(
159
+ "0000000000000000000000000000000000000000000000000000000000000000"
160
+ );
161
+ expect(tokenInfo?.token?.symbol).toBe("TOK");
162
+ expect(tokenInfo?.token?.decimals).toBe(8);
163
+
164
+ removeFetchMock(
165
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
166
+ );
167
+ });
168
+
169
+ test("Auth chain: invalid transaction", async () => {
170
+ await expect(
171
+ BCMR.buildAuthChain({
172
+ transactionHash:
173
+ "0000000000000000000000000000000000000000000000000000000000000000",
174
+ })
175
+ ).rejects.toThrow("Could not decode transaction");
176
+ });
177
+
178
+ test("Auth chain: no BCMR", async () => {
179
+ const alice = await RegTestWallet.fromId(
180
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
181
+ );
182
+ const bob = await RegTestWallet.newRandom();
183
+
184
+ const chunks = ["Hello"];
185
+ const opreturnData = OpReturnData.fromArray(chunks);
186
+
187
+ const response = await alice.send([
188
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
189
+ opreturnData,
190
+ ]);
191
+ const authChain = await BCMR.buildAuthChain({
192
+ transactionHash: response.txId!,
193
+ network: Network.REGTEST,
194
+ });
195
+ expect(authChain.length).toBe(0);
196
+ });
197
+
198
+ test("Auth chain: BCMR, no hash", async () => {
199
+ const alice = await RegTestWallet.fromId(
200
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
201
+ );
202
+ const bob = await RegTestWallet.newRandom();
203
+
204
+ const chunks = ["BCMR"];
205
+ const opreturnData = OpReturnData.fromArray(chunks);
206
+
207
+ const response = await alice.send([
208
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
209
+ opreturnData,
210
+ ]);
211
+ await expect(
212
+ BCMR.buildAuthChain({
213
+ transactionHash: response.txId!,
214
+ network: Network.REGTEST,
215
+ })
216
+ ).rejects.toThrow("Malformed BCMR output");
217
+ });
218
+
219
+ test("Auth chain: BCMR, ipfs hash", async () => {
220
+ const alice = await RegTestWallet.fromId(
221
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
222
+ );
223
+ const bob = await RegTestWallet.newRandom();
224
+
225
+ const chunks = ["BCMR", "QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"];
226
+ const opreturnData = OpReturnData.fromArray(chunks);
227
+
228
+ const response = await alice.send([
229
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
230
+ opreturnData,
231
+ ]);
232
+ const chain = await BCMR.buildAuthChain({
233
+ transactionHash: response.txId!,
234
+ network: Network.REGTEST,
235
+ });
236
+ expect(chain.length).toBe(1);
237
+ expect(chain[0].contentHash).toBe(
238
+ "516d62577247354173703569476d557751486f67534a47525832367a75526e754c575079745a66694c3735735a76"
239
+ );
240
+ expect(chain[0].uris[0]).toBe(
241
+ "ipfs://QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"
242
+ );
243
+ expect(chain[0].httpsUrl).toBe(
244
+ "https://dweb.link/ipfs/QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"
245
+ );
246
+ expect(chain[0].txHash).toBe(response.txId);
247
+ });
248
+
249
+ test("Auth chain: BCMR, ipfs hash and uri", async () => {
250
+ const alice = await RegTestWallet.fromId(
251
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
252
+ );
253
+ const bob = await RegTestWallet.newRandom();
254
+
255
+ const chunks = [
256
+ "BCMR",
257
+ sha256.hash(utf8ToBin("registry_contents")),
258
+ "ipfs://bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm",
259
+ ];
260
+ const opreturnData = OpReturnData.fromArray(chunks);
261
+
262
+ const response = await alice.send([
263
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
264
+ opreturnData,
265
+ ]);
266
+ const chain = await BCMR.buildAuthChain({
267
+ transactionHash: response.txId!,
268
+ network: Network.REGTEST,
269
+ });
270
+ expect(chain.length).toBe(1);
271
+ expect(chain[0].contentHash).toBe(
272
+ "e073b89a80c77c533ad364692db15df01adb9df404592f608d2c0cdd8960ed0e"
273
+ );
274
+ expect(chain[0].uris[0]).toBe(
275
+ "ipfs://bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm"
276
+ );
277
+ expect(chain[0].httpsUrl).toBe(
278
+ "https://dweb.link/ipfs/bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm"
279
+ );
280
+ expect(chain[0].txHash).toBe(response.txId);
281
+ });
282
+
283
+ test("Auth chain: BCMR, ipfs https url", async () => {
284
+ const alice = await RegTestWallet.fromId(
285
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
286
+ );
287
+ const bob = await RegTestWallet.newRandom();
288
+
289
+ const chunks = [
290
+ "BCMR",
291
+ sha256.hash(utf8ToBin("registry_contents")),
292
+ "bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm.ipfs.dweb.link",
293
+ ];
294
+ const opreturnData = OpReturnData.fromArray(chunks);
295
+
296
+ const response = await alice.send([
297
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
298
+ opreturnData,
299
+ ]);
300
+ const chain = await BCMR.buildAuthChain({
301
+ transactionHash: response.txId!,
302
+ network: Network.REGTEST,
303
+ });
304
+ expect(chain.length).toBe(1);
305
+ expect(chain[0].contentHash).toBe(
306
+ "e073b89a80c77c533ad364692db15df01adb9df404592f608d2c0cdd8960ed0e"
307
+ );
308
+ expect(chain[0].uris[0]).toBe(
309
+ "bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm.ipfs.dweb.link"
310
+ );
311
+ expect(chain[0].httpsUrl).toBe(
312
+ "https://bafkreiejafiz23ewtyh6m3dpincmxouohdcimrd33abacrq3h2pacewwjm.ipfs.dweb.link"
313
+ );
314
+ expect(chain[0].txHash).toBe(response.txId);
315
+ });
316
+
317
+ test("Auth chain: BCMR, sha256 content hash, uri", async () => {
318
+ const alice = await RegTestWallet.fromId(
319
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
320
+ );
321
+ const bob = await RegTestWallet.newRandom();
322
+
323
+ const contentHashBin = sha256.hash(utf8ToBin("registry_contents"));
324
+ const chunks = ["BCMR", contentHashBin, "mainnet.cash"];
325
+ const opreturnData = OpReturnData.fromArray(chunks);
326
+
327
+ const response = await alice.send([
328
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
329
+ opreturnData,
330
+ ]);
331
+ const chain = await BCMR.buildAuthChain({
332
+ transactionHash: response.txId!,
333
+ network: Network.REGTEST,
334
+ });
335
+ expect(chain.length).toBe(1);
336
+ expect(chain[0].contentHash).toBe(binToHex(contentHashBin));
337
+ expect(chain[0].uris[0]).toBe("mainnet.cash");
338
+ expect(chain[0].httpsUrl).toBe(
339
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
340
+ );
341
+ expect(chain[0].txHash).toBe(response.txId);
342
+ });
343
+
344
+ test("Auth chain: BCMR, sha256 content hash, 2 uris", async () => {
345
+ const alice = await RegTestWallet.fromId(
346
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
347
+ );
348
+ const bob = await RegTestWallet.newRandom();
349
+
350
+ const chunks = [
351
+ "BCMR",
352
+ sha256.hash(utf8ToBin("registry_contents")),
353
+ "mainnet.cash",
354
+ "ipfs://QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv",
355
+ ];
356
+ const opreturnData = OpReturnData.fromArray(chunks);
357
+ const response = await alice.send([
358
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
359
+ opreturnData,
360
+ ]);
361
+ const chain = await BCMR.buildAuthChain({
362
+ transactionHash: response.txId!,
363
+ network: Network.REGTEST,
364
+ });
365
+
366
+ expect(chain.length).toBe(1);
367
+ expect(chain[0].uris.length).toBe(2);
368
+ expect(chain[0].uris[0]).toBe("mainnet.cash");
369
+ expect(chain[0].uris[1]).toBe(
370
+ "ipfs://QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"
371
+ );
372
+ expect(chain[0].httpsUrl).toBe(
373
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
374
+ );
375
+ });
376
+
377
+ test("Auth chain: all OP_PUSDHDATA encodings", async () => {
378
+ const alice = await RegTestWallet.fromId(
379
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
380
+ );
381
+ const bob = await RegTestWallet.newRandom();
382
+
383
+ const opreturnData = OpReturnData.fromUint8Array(
384
+ hexToBin(
385
+ "6a0442434d524c20e073b89a80c77c533ad364692db15df01adb9df404592f608d2c0cdd8960ed0e4d440068747470733a2f2f6d61696e6e65742e636173682f2e77656c6c2d6b6e6f776e2f626974636f696e2d636173682d6d657461646174612d72656769737472792e6a736f6e"
386
+ )
387
+ );
388
+ const response = await alice.send([
389
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 1000, unit: "sat" }),
390
+ opreturnData,
391
+ ]);
392
+ await BCMR.buildAuthChain({
393
+ transactionHash: response.txId!,
394
+ network: Network.REGTEST,
395
+ });
396
+ });
397
+
398
+ test("Auth chain with 1 element, add resolved registry", async () => {
399
+ const alice = await RegTestWallet.fromId(
400
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
401
+ );
402
+ const bob = await RegTestWallet.newRandom();
403
+
404
+ let chunks = [
405
+ "BCMR",
406
+ registryContentHashBinBitcoinByteOrder,
407
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
408
+ ];
409
+ const opreturnData = OpReturnData.fromArray(chunks);
410
+ const response = await alice.send([
411
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 10000, unit: "sat" }),
412
+ opreturnData,
413
+ ]);
414
+
415
+ setupFetchMock(
416
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
417
+ registry
418
+ );
419
+
420
+ expect(
421
+ BCMR.getTokenInfo(
422
+ "0000000000000000000000000000000000000000000000000000000000000000"
423
+ )
424
+ ).toBe(undefined);
425
+ const chain = await BCMR.addMetadataRegistryAuthChain({
426
+ transactionHash: response.txId!,
427
+ network: Network.REGTEST,
428
+ });
429
+ expect(chain.length).toBe(1);
430
+ expect(chain[0].txHash).toBe(response.txId!);
431
+ expect(chain[0].uris[0]).toBe(
432
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
433
+ );
434
+ expect(chain[0].httpsUrl).toBe(
435
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
436
+ );
437
+
438
+ const tokenInfo = BCMR.getTokenInfo(
439
+ "0000000000000000000000000000000000000000000000000000000000000000"
440
+ );
441
+ expect(tokenInfo?.token?.symbol).toBe("TOK");
442
+ expect(tokenInfo?.token?.decimals).toBe(8);
443
+
444
+ // check adding the same registry does not produce a duplicate
445
+ expect(BCMR.metadataRegistries.length).toBe(1);
446
+ const otherChain = await BCMR.addMetadataRegistryAuthChain({
447
+ transactionHash: response.txId!,
448
+ network: Network.REGTEST,
449
+ });
450
+ expect(otherChain.length).toBe(1);
451
+ expect(BCMR.metadataRegistries.length).toBe(1);
452
+
453
+ removeFetchMock(
454
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
455
+ );
456
+ });
457
+
458
+ test("Auth chain with 3 elements", async () => {
459
+ // tests authchain of 3 elements with all possible confirmed and unconfirmed transaction chains
460
+ // Also change of authchain holding address is assessed
461
+ for (const [index, mineCombo] of [
462
+ [0, 0, 0],
463
+ [0, 0, 1],
464
+ [0, 1, 0],
465
+ [0, 1, 1],
466
+ [1, 0, 0],
467
+ [1, 0, 1],
468
+ [1, 1, 0],
469
+ [1, 1, 1],
470
+ ].entries()) {
471
+ const alice = await RegTestWallet.fromId(
472
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
473
+ );
474
+ const bob = await RegTestWallet.newRandom();
475
+ const charlie = await RegTestWallet.newRandom();
476
+
477
+ let chunks = [
478
+ "BCMR",
479
+ registryContentHashBinBitcoinByteOrder,
480
+ "mainnet.cash",
481
+ ];
482
+ const opreturnData = OpReturnData.fromArray(chunks);
483
+ const response = await alice.send([
484
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 10000, unit: "sat" }),
485
+ opreturnData,
486
+ ]);
487
+ if (mineCombo[0]) await mine({ cashaddr: alice.cashaddr!, blocks: 1 });
488
+
489
+ chunks[2] =
490
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json";
491
+ const opreturnData2 = OpReturnData.fromArray(chunks);
492
+ const response2 = await bob.send([
493
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 9500, unit: "sat" }),
494
+ opreturnData2,
495
+ ]);
496
+ if (mineCombo[1]) await mine({ cashaddr: alice.cashaddr!, blocks: 1 });
497
+
498
+ chunks[2] =
499
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json";
500
+ const opreturnData3 = OpReturnData.fromArray(chunks);
501
+ const response3 = await bob.send([
502
+ new SendRequest({
503
+ cashaddr: charlie.cashaddr!,
504
+ value: 9000,
505
+ unit: "sat",
506
+ }),
507
+ opreturnData3,
508
+ ]);
509
+ if (mineCombo[2]) await mine({ cashaddr: alice.cashaddr!, blocks: 1 });
510
+
511
+ const chain = await BCMR.buildAuthChain({
512
+ transactionHash: response.txId!,
513
+ network: Network.REGTEST,
514
+ });
515
+
516
+ expect(chain.length).toBe(3);
517
+ expect(chain[0].txHash).toBe(response.txId!);
518
+ expect(chain[0].uris[0]).toBe("mainnet.cash");
519
+ expect(chain[0].httpsUrl).toBe(
520
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
521
+ );
522
+
523
+ expect(chain[1].txHash).toBe(response2.txId!);
524
+ expect(chain[1].uris[0]).toBe(
525
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
526
+ );
527
+ expect(chain[1].httpsUrl).toBe(
528
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
529
+ );
530
+
531
+ expect(chain[2].txHash).toBe(response3.txId!);
532
+ expect(chain[2].uris[0]).toBe(
533
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
534
+ );
535
+ expect(chain[2].httpsUrl).toBe(
536
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
537
+ );
538
+
539
+ // extra checks for resolving chains not from head
540
+ if (index === 0) {
541
+ const noFollow = await BCMR.buildAuthChain({
542
+ transactionHash: response2.txId!,
543
+ network: Network.REGTEST,
544
+ followToHead: false,
545
+ });
546
+ expect(noFollow.length).toBe(1);
547
+ expect(noFollow[0].txHash).toBe(response2.txId!);
548
+ expect(noFollow[0].uris[0]).toBe(
549
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
550
+ );
551
+ expect(noFollow[0].httpsUrl).toBe(
552
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
553
+ );
554
+
555
+ const follow = await BCMR.buildAuthChain({
556
+ transactionHash: response2.txId!,
557
+ network: Network.REGTEST,
558
+ followToHead: true,
559
+ });
560
+ expect(follow.length).toBe(2);
561
+
562
+ expect(follow[0].txHash).toBe(response2.txId!);
563
+ expect(follow[0].uris[0]).toBe(
564
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
565
+ );
566
+ expect(follow[0].httpsUrl).toBe(
567
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
568
+ );
569
+
570
+ expect(follow[1].txHash).toBe(response3.txId!);
571
+ expect(follow[1].uris[0]).toBe(
572
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
573
+ );
574
+ expect(follow[1].httpsUrl).toBe(
575
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
576
+ );
577
+ }
578
+ }
579
+ });
580
+
581
+ test("Authchain tail resolution info in registry acceleration path", async () => {
582
+ const alice = await RegTestWallet.fromId(
583
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
584
+ );
585
+ const bob = await RegTestWallet.newRandom();
586
+
587
+ const registry_v1 = { ...registry };
588
+ registry_v1.extensions = { authchain: {} };
589
+ const contentHash_v1 = sha256.hash(
590
+ utf8ToBin(JSON.stringify(registry_v1, null, 2))
591
+ );
592
+ setupFetchMock(
593
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v1.json",
594
+ JSON.stringify(registry_v1, null, 2)
595
+ );
596
+ let chunks = [
597
+ "BCMR",
598
+ contentHash_v1,
599
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v1.json",
600
+ ];
601
+ const opreturnData = OpReturnData.fromArray(chunks);
602
+ const response = await alice.send([
603
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 10000, unit: "sat" }),
604
+ opreturnData,
605
+ ]);
606
+
607
+ const registry_v2 = { ...registry };
608
+ registry_v2.extensions = {
609
+ authchain: { 0: await bob.provider!.getRawTransaction(response.txId!) },
610
+ };
611
+ const contentHash_v2 = sha256.hash(
612
+ utf8ToBin(JSON.stringify(registry_v2, null, 2))
613
+ );
614
+ setupFetchMock(
615
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json",
616
+ JSON.stringify(registry_v2, null, 2)
617
+ );
618
+ chunks = [
619
+ "BCMR",
620
+ contentHash_v2,
621
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json",
622
+ ];
623
+ const opreturnData2 = OpReturnData.fromArray(chunks);
624
+ const response2 = await bob.send([
625
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 9500, unit: "sat" }),
626
+ opreturnData2,
627
+ ]);
628
+
629
+ const registry_v3 = { ...registry };
630
+ registry_v3.extensions = {
631
+ authchain: {
632
+ 0: await bob.provider!.getRawTransaction(response.txId!),
633
+ 1: await bob.provider!.getRawTransaction(response2.txId!),
634
+ },
635
+ };
636
+ const contentHash_v3 = sha256.hash(
637
+ utf8ToBin(JSON.stringify(registry_v3, null, 2))
638
+ );
639
+ setupFetchMock(
640
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json",
641
+ JSON.stringify(registry_v3, null, 2)
642
+ );
643
+ chunks = [
644
+ "BCMR",
645
+ contentHash_v3,
646
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json",
647
+ ];
648
+ const opreturnData3 = OpReturnData.fromArray(chunks);
649
+ const response3 = await bob.send([
650
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 9000, unit: "sat" }),
651
+ opreturnData3,
652
+ ]);
653
+
654
+ const registry_v4 = { ...registry };
655
+ registry_v4.extensions = {};
656
+ const contentHash_v4 = sha256.hash(
657
+ utf8ToBin(JSON.stringify(registry_v4, null, 2))
658
+ );
659
+ setupFetchMock(
660
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v4.json",
661
+ JSON.stringify(registry_v4, null, 2)
662
+ );
663
+ chunks = [
664
+ "BCMR",
665
+ contentHash_v4,
666
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v4.json",
667
+ ];
668
+ const opreturnData4 = OpReturnData.fromArray(chunks);
669
+ const response4 = await bob.send([
670
+ new SendRequest({ cashaddr: bob.cashaddr!, value: 8500, unit: "sat" }),
671
+ opreturnData4,
672
+ ]);
673
+
674
+ let chain = await BCMR.buildAuthChain({
675
+ transactionHash: response.txId!,
676
+ network: Network.REGTEST,
677
+ });
678
+ expect(chain.length).toBe(4);
679
+
680
+ chain = await BCMR.buildAuthChain({
681
+ transactionHash: response.txId!,
682
+ network: Network.REGTEST,
683
+ followToHead: false,
684
+ });
685
+ expect(chain.length).toBe(1);
686
+
687
+ // tail acceleration available, do not follow head
688
+ chain = await BCMR.buildAuthChain({
689
+ transactionHash: response3.txId!,
690
+ network: Network.REGTEST,
691
+ followToHead: false,
692
+ resolveBase: true,
693
+ });
694
+ expect(chain.length).toBe(3);
695
+
696
+ // resolve single element
697
+ chain = await BCMR.buildAuthChain({
698
+ transactionHash: response3.txId!,
699
+ network: Network.REGTEST,
700
+ followToHead: false,
701
+ resolveBase: false,
702
+ });
703
+ expect(chain.length).toBe(1);
704
+
705
+ // no acceleration available, will scan network
706
+ chain = await BCMR.buildAuthChain({
707
+ transactionHash: response4.txId!,
708
+ network: Network.REGTEST,
709
+ resolveBase: true,
710
+ });
711
+ expect(chain.length).toBe(4);
712
+
713
+ expect(chain[0].txHash).toBe(response.txId!);
714
+ expect(chain[0].uris[0]).toBe(
715
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v1.json"
716
+ );
717
+ expect(chain[0].httpsUrl).toBe(
718
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v1.json"
719
+ );
720
+
721
+ expect(chain[1].txHash).toBe(response2.txId!);
722
+ expect(chain[1].uris[0]).toBe(
723
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
724
+ );
725
+ expect(chain[1].httpsUrl).toBe(
726
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
727
+ );
728
+
729
+ expect(chain[2].txHash).toBe(response3.txId!);
730
+ expect(chain[2].uris[0]).toBe(
731
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
732
+ );
733
+ expect(chain[2].httpsUrl).toBe(
734
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
735
+ );
736
+
737
+ expect(chain[3].txHash).toBe(response4.txId!);
738
+ expect(chain[3].uris[0]).toBe(
739
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v4.json"
740
+ );
741
+ expect(chain[3].httpsUrl).toBe(
742
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v4.json"
743
+ );
744
+
745
+ removeFetchMock(
746
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v1.json"
747
+ );
748
+ removeFetchMock(
749
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v2.json"
750
+ );
751
+ removeFetchMock(
752
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v3.json"
753
+ );
754
+ removeFetchMock(
755
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry_v4.json"
756
+ );
757
+ });
758
+
759
+ test("Test NFT cashtoken genesis with BCMR output", async () => {
760
+ const chunks = ["BCMR", "QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"];
761
+ const opreturnData = OpReturnData.fromArray(chunks);
762
+
763
+ const alice = await RegTestWallet.fromId(process.env.ALICE_ID!);
764
+ const genesisResponse = await alice.tokenGenesis(
765
+ {
766
+ cashaddr: alice.cashaddr!,
767
+ capability: NFTCapability.mutable,
768
+ commitment: "abcd",
769
+ },
770
+ opreturnData
771
+ );
772
+
773
+ const tokenId = genesisResponse.tokenIds![0];
774
+ const tokenBalance = await alice.getTokenBalance(tokenId);
775
+ expect(tokenBalance).toBe(0n);
776
+ const nftTokenBalance = await alice.getNftTokenBalance(tokenId);
777
+ expect(nftTokenBalance).toBe(1);
778
+ const tokenUtxos = await alice.getTokenUtxos(tokenId);
779
+ expect(tokenUtxos.length).toBe(1);
780
+
781
+ const transaction = await (
782
+ alice.provider as ElectrumNetworkProvider
783
+ ).getRawTransactionObject(genesisResponse.txId!);
784
+ expect(transaction.vout[0].tokenData?.category).toBe(tokenId);
785
+ expect(transaction.vout[1].scriptPubKey.type).toBe("nulldata");
786
+
787
+ const chain = await BCMR.buildAuthChain({
788
+ transactionHash: genesisResponse.txId!,
789
+ network: Network.REGTEST,
790
+ });
791
+ expect(chain.length).toBe(1);
792
+ expect(chain[0].contentHash).toBe(
793
+ "516d62577247354173703569476d557751486f67534a47525832367a75526e754c575079745a66694c3735735a76"
794
+ );
795
+ expect(chain[0].uris[0]).toBe(
796
+ "ipfs://QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"
797
+ );
798
+ expect(chain[0].httpsUrl).toBe(
799
+ "https://dweb.link/ipfs/QmbWrG5Asp5iGmUwQHogSJGRX26zuRnuLWPytZfiL75sZv"
800
+ );
801
+ expect(chain[0].txHash).toBe(genesisResponse.txId);
802
+
803
+ const chainByTokenId = await BCMR.buildAuthChain({
804
+ transactionHash: tokenId,
805
+ network: Network.REGTEST,
806
+ });
807
+
808
+ expect(JSON.stringify(chain)).toBe(JSON.stringify(chainByTokenId));
809
+ });
810
+
811
+ test("Auth chain with forwards gaps", async () => {
812
+ const alice = await RegTestWallet.fromId(
813
+ `wif:regtest:${process.env.PRIVATE_WIF!}`
814
+ );
815
+
816
+ const contentHashBin = sha256.hash(utf8ToBin("registry_contents"));
817
+ const chunks = [
818
+ "BCMR",
819
+ contentHashBin,
820
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json",
821
+ ];
822
+ const opreturnData = OpReturnData.fromArray(chunks);
823
+
824
+ const response = await alice.send([
825
+ new SendRequest({ cashaddr: alice.cashaddr!, value: 3000, unit: "sat" }),
826
+ opreturnData,
827
+ ]);
828
+ const chain = await BCMR.buildAuthChain({
829
+ transactionHash: response.txId!,
830
+ network: Network.REGTEST,
831
+ });
832
+ expect(chain.length).toBe(1);
833
+ expect(chain[0].contentHash).toBe(binToHex(contentHashBin));
834
+ expect(chain[0].uris[0]).toBe(
835
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
836
+ );
837
+ expect(chain[0].httpsUrl).toBe(
838
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
839
+ );
840
+ expect(chain[0].txHash).toBe(response.txId);
841
+
842
+ const gapTxResponse = await alice.send(
843
+ [
844
+ new SendRequest({
845
+ cashaddr: alice.cashaddr!,
846
+ value: 2000,
847
+ unit: "sat",
848
+ }),
849
+ ],
850
+ { utxoIds: [`${response.txId}:0:3000`] }
851
+ );
852
+
853
+ const chainHeadResponse = await alice.send(
854
+ [
855
+ new SendRequest({
856
+ cashaddr: alice.cashaddr!,
857
+ value: 1000,
858
+ unit: "sat",
859
+ }),
860
+ opreturnData,
861
+ ],
862
+ { utxoIds: [`${gapTxResponse.txId}:0:2000`] }
863
+ );
864
+
865
+ const gappedChain = await BCMR.buildAuthChain({
866
+ transactionHash: response.txId!,
867
+ network: Network.REGTEST,
868
+ });
869
+ expect(gappedChain.length).toBe(2);
870
+ expect(gappedChain[0].contentHash).toBe(binToHex(contentHashBin));
871
+ expect(gappedChain[0].uris[0]).toBe(
872
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
873
+ );
874
+ expect(gappedChain[0].httpsUrl).toBe(
875
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
876
+ );
877
+ expect(gappedChain[0].txHash).toBe(response.txId);
878
+
879
+ expect(gappedChain[1].contentHash).toBe(binToHex(contentHashBin));
880
+ expect(gappedChain[1].uris[0]).toBe(
881
+ "mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
882
+ );
883
+ expect(gappedChain[1].httpsUrl).toBe(
884
+ "https://mainnet.cash/.well-known/bitcoin-cash-metadata-registry.json"
885
+ );
886
+ expect(gappedChain[1].txHash).toBe(chainHeadResponse.txId);
887
+ });
888
+
889
+ test("Test fetching BCMR authchain from chaingraph", async () => {
890
+ setupFetchMock("https://gql.mainnet.cash/v1/graphql", {
891
+ data: {
892
+ transaction: [
893
+ {
894
+ hash: "\\x07275f68d14780c737279898e730cec3a7b189a761caf43b4197b60a7c891a97",
895
+ authchains: [
896
+ {
897
+ authchain_length: 330,
898
+ migrations: [
899
+ {
900
+ transaction: [
901
+ {
902
+ hash: "\\xd5721db8841ecb61ec73daeb2df7df88b180d5029061d4845efc7cb29c42183b",
903
+ inputs: [
904
+ {
905
+ outpoint_index: "0",
906
+ },
907
+ ],
908
+ outputs: [
909
+ {
910
+ output_index: "1",
911
+ locking_bytecode:
912
+ "\\x6a0442434d5220107b1719c865e8ab631f9e63f1140b51e710a86606992adc7f901b2291746abe4c506261666b7265696171706d6c727473646635637677676834366d707972696332723434696b717a71677465766e79373471646d726a6335646b78792e697066732e6e667473746f726167652e6c696e6b",
913
+ },
914
+ ],
915
+ },
916
+ ],
917
+ },
918
+ {
919
+ transaction: [
920
+ {
921
+ hash: "\\x4bdcdd9a347b287e6d26d743ee4404f530a8f35501ff1adb31766edcfb2d20a9",
922
+ inputs: [
923
+ {
924
+ outpoint_index: "0",
925
+ },
926
+ {
927
+ outpoint_index: "0",
928
+ },
929
+ ],
930
+ outputs: [
931
+ {
932
+ output_index: "1",
933
+ locking_bytecode:
934
+ "\\x6a0442434d5240393231666263306665623665666666613639316331346633656636346234333139656138613461643266636637313064303362613661363534633962346661643f697066732e7061742e6d6e2f697066732f516d556e6661524c4356516d4e453567745274705464476b39544d6939364472507a7351554c31505a7874686637",
935
+ },
936
+ ],
937
+ },
938
+ ],
939
+ },
940
+ {
941
+ transaction: [
942
+ {
943
+ hash: "\\x0d7a26fcc472d519ef83a1ca9c3a44a394b27423a55a40f7aacd1552c873e2a5",
944
+ inputs: [
945
+ {
946
+ outpoint_index: "0",
947
+ },
948
+ ],
949
+ outputs: [
950
+ {
951
+ output_index: "1",
952
+ locking_bytecode:
953
+ "\\x6a0442434d5240323065326630623531343333636566633639393732373765643239616365303438363963326366393136366465653139366538656331333561666630613162343f697066732e7061742e6d6e2f697066732f516d5339687a786a6e42394168416f46584b53626b376a454d7255354577397653726d6e624a593435555932567a",
954
+ },
955
+ ],
956
+ },
957
+ ],
958
+ },
959
+ ],
960
+ },
961
+ ],
962
+ },
963
+ ],
964
+ },
965
+ });
966
+
967
+ const result: AuthChain = await BCMR.fetchAuthChainFromChaingraph({
968
+ chaingraphUrl: "https://gql.mainnet.cash/v1/graphql",
969
+ transactionHash:
970
+ "07275f68d14780c737279898e730cec3a7b189a761caf43b4197b60a7c891a97",
971
+ });
972
+ expect(result.length).toBe(3);
973
+ expect(result.at(-1)?.uris[0]).toBe(
974
+ "ipfs.pat.mn/ipfs/QmS9hzxjnB9AhAoFXKSbk7jEMrU5Ew9vSrmnbJY45UY2Vz"
975
+ );
976
+ removeFetchMock("https://gql.mainnet.cash/v1/graphql");
977
+ });
978
+ });