@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.
package/src/Bcmr.ts ADDED
@@ -0,0 +1,560 @@
1
+ import {
2
+ binToHex,
3
+ binToUtf8,
4
+ decodeTransaction,
5
+ hexToBin,
6
+ IdentitySnapshot,
7
+ sha256,
8
+ Transaction,
9
+ utf8ToBin,
10
+ } from "@bitauth/libauth";
11
+ import {
12
+ ElectrumRawTransaction,
13
+ OpReturnData,
14
+ Config,
15
+ Network,
16
+ TxI,
17
+ ElectrumNetworkProvider,
18
+ initProvider,
19
+ } from "mainnet-js";
20
+ import { Registry } from "./bcmr-v2.schema";
21
+
22
+ export interface AuthChainElement {
23
+ txHash: string;
24
+ contentHash: string;
25
+ uris: string[];
26
+ httpsUrl: string;
27
+ }
28
+
29
+ export type AuthChain = AuthChainElement[];
30
+
31
+ // Implementation of CHIP-BCMR v2.0.0-draft, refer to https://github.com/bitjson/chip-bcmr
32
+ export class BCMR {
33
+ // List of tracked registries
34
+ public static metadataRegistries: Registry[] = [];
35
+
36
+ public static getRegistries(): Registry[] {
37
+ return this.metadataRegistries;
38
+ }
39
+
40
+ public static resetRegistries(): void {
41
+ this.metadataRegistries = [];
42
+ }
43
+
44
+ /**
45
+ * fetchMetadataRegistry Fetch the BCMR registry JSON file from a remote URI, optionally verifying its content hash
46
+ *
47
+ * @param {string} uri URI of the registry to fetch from
48
+ * @param {string?} contentHash SHA256 hash of the resource the `uri` parameter points at.
49
+ * If specified, calculates the hash of the data fetched from `uri` and matches it with provided one.
50
+ * Yields an error upon mismatch.
51
+ *
52
+ * @returns {Registry} resolved registry
53
+ */
54
+ public static async fetchMetadataRegistry(
55
+ uri: string,
56
+ contentHash?: string
57
+ ): Promise<Registry> {
58
+ if (uri.indexOf("https://") < 0) {
59
+ uri = `https://${uri}`;
60
+ }
61
+
62
+ // content hashes HTTPS Publication Outputs per spec
63
+ if (contentHash) {
64
+ // request as text and verify hash
65
+ const response = await fetch(uri);
66
+ const data = await response.text();
67
+ const hash = binToHex(sha256.hash(utf8ToBin(data)));
68
+ if (contentHash != hash) {
69
+ throw new Error(
70
+ `Content hash mismatch for URI: ${uri}\nreceived: ${hash}\nrequired: ${contentHash}`
71
+ );
72
+ }
73
+
74
+ return JSON.parse(data) as Registry;
75
+ }
76
+
77
+ // request as JSON
78
+ const response = await fetch(uri);
79
+ const data = await response.json();
80
+ return data as Registry;
81
+ }
82
+
83
+ /**
84
+ * addMetadataRegistry Add the metadata registry to the list of tracked registries
85
+ *
86
+ * @param {Registry} registry Registry object per schema specification, see https://raw.githubusercontent.com/bitjson/chip-bcmr/master/bcmr-v1.schema.json
87
+ *
88
+ */
89
+ public static addMetadataRegistry(registry: Registry): void {
90
+ if (
91
+ this.metadataRegistries.some(
92
+ (val) => JSON.stringify(val) === JSON.stringify(registry)
93
+ )
94
+ ) {
95
+ return;
96
+ }
97
+ this.metadataRegistries.push(registry);
98
+ }
99
+
100
+ /**
101
+ * addMetadataRegistryFromUri Add the metadata registry by fetching a JSON file from a remote URI, optionally verifying its content hash
102
+ *
103
+ * @param {string} uri URI of the registry to fetch from
104
+ * @param {string?} contentHash SHA256 hash of the resource the `uri` parameter points at.
105
+ * If specified, calculates the hash of the data fetched from `uri` and matches it with provided one.
106
+ * Yields an error upon mismatch.
107
+ *
108
+ */
109
+ public static async addMetadataRegistryFromUri(
110
+ uri: string,
111
+ contentHash?: string
112
+ ): Promise<void> {
113
+ const registry = await this.fetchMetadataRegistry(uri, contentHash);
114
+ this.addMetadataRegistry(registry);
115
+ }
116
+
117
+ // helper function to enforce the constraints on the 0th output, decode the BCMR's OP_RETURN data
118
+ // returns resolved AuthChainElement
119
+ public static makeAuthChainElement(
120
+ rawTx: ElectrumRawTransaction | Transaction,
121
+ hash: string
122
+ ): AuthChainElement {
123
+ let opReturns: string[];
124
+ let spends0thOutput = false;
125
+ if (rawTx.hasOwnProperty("vout")) {
126
+ const electrumTransaction = rawTx as ElectrumRawTransaction;
127
+ opReturns = electrumTransaction.vout
128
+ .filter((val) => val.scriptPubKey.type === "nulldata")
129
+ .map((val) => val.scriptPubKey.hex);
130
+ spends0thOutput = electrumTransaction.vin.some((val) => val.vout === 0);
131
+ } else {
132
+ const libauthTransaction = rawTx as Transaction;
133
+ opReturns = libauthTransaction.outputs
134
+ .map((val) => binToHex(val.lockingBytecode))
135
+ .filter((val) => val.indexOf("6a") === 0);
136
+ spends0thOutput = libauthTransaction.inputs.some(
137
+ (val) => val.outpointIndex === 0
138
+ );
139
+ }
140
+
141
+ if (!spends0thOutput) {
142
+ throw new Error(
143
+ "Invalid authchain transaction (does not spend 0th output of previous transaction)"
144
+ );
145
+ }
146
+
147
+ const bcmrOpReturns = opReturns.filter(
148
+ (val) =>
149
+ val.indexOf("6a0442434d52") === 0 ||
150
+ val.indexOf("6a4c0442434d52") === 0 ||
151
+ val.indexOf("6a4d040042434d52") === 0 ||
152
+ val.indexOf("6a4e0400000042434d52") === 0
153
+ );
154
+
155
+ if (bcmrOpReturns.length === 0) {
156
+ return {
157
+ txHash: hash,
158
+ contentHash: "",
159
+ uris: [],
160
+ httpsUrl: "",
161
+ };
162
+ }
163
+
164
+ const opReturnHex = opReturns[0];
165
+ const chunks = OpReturnData.parseBinary(hexToBin(opReturnHex));
166
+ if (chunks.length < 2) {
167
+ throw new Error(`Malformed BCMR output: ${opReturnHex}`);
168
+ }
169
+
170
+ const result: AuthChainElement = {
171
+ txHash: hash,
172
+ contentHash: "",
173
+ uris: [],
174
+ httpsUrl: "",
175
+ };
176
+
177
+ if (chunks.length === 2) {
178
+ // IPFS Publication Output
179
+ result.contentHash = binToHex(chunks[1]);
180
+ const ipfsCid = binToUtf8(chunks[1]);
181
+ result.uris = [`ipfs://${ipfsCid}`];
182
+ result.httpsUrl = `${Config.DefaultIpfsGateway}${ipfsCid}`;
183
+ } else {
184
+ // URI Publication Output
185
+ // content hash is in OP_SHA256 byte order per spec
186
+ result.contentHash = binToHex(chunks[1].slice());
187
+
188
+ const uris = chunks.slice(2);
189
+
190
+ for (const uri of uris) {
191
+ const uriString = binToUtf8(uri);
192
+ result.uris.push(uriString);
193
+
194
+ if (result.httpsUrl) {
195
+ continue;
196
+ }
197
+
198
+ if (uriString.indexOf("ipfs://") === 0) {
199
+ const ipfsCid = uriString.replace("ipfs://", "");
200
+ result.httpsUrl = `${Config.DefaultIpfsGateway}${ipfsCid}`;
201
+ } else if (uriString.indexOf("https://") === 0) {
202
+ result.httpsUrl = uriString;
203
+ } else if (uriString.indexOf("https://") === -1) {
204
+ result.httpsUrl = uriString;
205
+
206
+ // case for domain name specifier, like example.com
207
+ if (uriString.indexOf("/") === -1) {
208
+ const parts = uriString.toLowerCase().split(".");
209
+ if (!(parts?.[0]?.indexOf("baf") === 0 && parts?.[1] === "ipfs")) {
210
+ result.httpsUrl = `${result.httpsUrl}/.well-known/bitcoin-cash-metadata-registry.json`;
211
+ }
212
+ }
213
+
214
+ result.httpsUrl = `https://${result.httpsUrl}`;
215
+ } else {
216
+ throw new Error(`Unsupported uri type: ${uriString}`);
217
+ }
218
+ }
219
+ }
220
+ return result;
221
+ }
222
+
223
+ /**
224
+ * buildAuthChain Build an authchain - Zeroth-Descendant Transaction Chain, refer to https://github.com/bitjson/chip-bcmr#zeroth-descendant-transaction-chains
225
+ * The authchain in this implementation is specific to resolve to a valid metadata registry
226
+ *
227
+ * @param {string} options.transactionHash (required) transaction hash from which to build the auth chain
228
+ * @param {Network?} options.network (default=mainnet) network to query the data from
229
+ * @param {boolean?} options.resolveBase (default=false) boolean flag to indicate that autchain building should resolve and verify elements back to base or be stopped at this exact chain element
230
+ * @param {boolean?} options.followToHead (default=true) boolean flag to indicate that autchain building should progress to head or be stopped at this exact chain element
231
+ * @param {ElectrumRawTransaction?} options.rawTx cached raw transaction obtained previously, spares a Fulcrum call
232
+ * @param {TxI[]?} options.historyCache cached address history to be reused if authchain building proceeds with the same address, spares a flurry of Fulcrum calls
233
+ *
234
+ * @returns {AuthChain} returns the resolved authchain
235
+ */
236
+ public static async buildAuthChain(options: {
237
+ transactionHash: string;
238
+ network?: Network;
239
+ resolveBase?: boolean;
240
+ followToHead?: boolean;
241
+ rawTx?: ElectrumRawTransaction;
242
+ historyCache?: TxI[];
243
+ }): Promise<AuthChain> {
244
+ if (options.network === undefined) {
245
+ options.network = Network.MAINNET;
246
+ }
247
+
248
+ if (options.followToHead === undefined) {
249
+ options.followToHead = true;
250
+ }
251
+
252
+ if (options.resolveBase === undefined) {
253
+ options.resolveBase = false;
254
+ }
255
+
256
+ const provider = (await initProvider(
257
+ options.network
258
+ )!) as ElectrumNetworkProvider;
259
+
260
+ if (options.rawTx === undefined) {
261
+ options.rawTx = await provider.getRawTransactionObject(
262
+ options.transactionHash
263
+ );
264
+ }
265
+
266
+ // figure out the autchain by moving towards authhead
267
+ const getAuthChainChild = async () => {
268
+ const history =
269
+ options.historyCache ||
270
+ (await provider.getHistory(
271
+ options.rawTx!.vout[0].scriptPubKey.addresses[0]
272
+ ));
273
+ const thisTx = history.find(
274
+ (val) => val.tx_hash === options.transactionHash
275
+ );
276
+ let filteredHistory = history.filter((val) =>
277
+ val.height > 0
278
+ ? val.height >= thisTx!.height || val.height <= 0
279
+ : val.height <= 0 && val.tx_hash !== thisTx!.tx_hash
280
+ );
281
+
282
+ for (const historyTx of filteredHistory) {
283
+ const historyRawTx = await provider.getRawTransactionObject(
284
+ historyTx.tx_hash
285
+ );
286
+ const authChainVin = historyRawTx.vin.find(
287
+ (val) => val.txid === options.transactionHash && val.vout === 0
288
+ );
289
+ // if we've found continuation of authchain, we shall recurse into it
290
+ if (authChainVin) {
291
+ // reuse queried address history if the next element in chain is the same address
292
+ const historyCache =
293
+ options.rawTx!.vout[0].scriptPubKey.addresses[0] ===
294
+ historyRawTx.vout[0].scriptPubKey.addresses[0]
295
+ ? filteredHistory
296
+ : undefined;
297
+
298
+ // combine the authchain element with the rest obtained
299
+ return { rawTx: historyRawTx, historyCache };
300
+ }
301
+ }
302
+ return undefined;
303
+ };
304
+
305
+ // make authchain element and combine with the rest obtained
306
+ let element: AuthChainElement;
307
+ try {
308
+ element = BCMR.makeAuthChainElement(options.rawTx, options.rawTx.hash);
309
+ } catch (error) {
310
+ // special case for cashtoken authchain lookup by categoryId - allow to fail first lookup and inspect the genesis transaction
311
+ // follow authchain to head and look for BCMR outputs
312
+ const child = await getAuthChainChild();
313
+ if (child) {
314
+ return await BCMR.buildAuthChain({
315
+ ...options,
316
+ transactionHash: child.rawTx.hash,
317
+ rawTx: child.rawTx,
318
+ historyCache: child.historyCache,
319
+ });
320
+ } else {
321
+ throw error;
322
+ }
323
+ }
324
+
325
+ let chainBase: AuthChain = [];
326
+ if (options.resolveBase) {
327
+ // check for accelerated path if "authchain" extension is in registry
328
+ const registry: Registry = await this.fetchMetadataRegistry(
329
+ element.httpsUrl,
330
+ element.contentHash
331
+ );
332
+ if (
333
+ registry.extensions &&
334
+ registry.extensions["authchain"] &&
335
+ Object.keys(registry.extensions["authchain"]).length
336
+ ) {
337
+ const chainTxArray = Object.values(
338
+ registry.extensions!["authchain"]
339
+ ) as string[];
340
+
341
+ chainBase = chainTxArray
342
+ .map((tx) => {
343
+ const transactionBin = hexToBin(tx);
344
+ const decoded = decodeTransaction(transactionBin);
345
+ if (typeof decoded === "string") {
346
+ throw new Error(
347
+ `Error decoding transaction ${JSON.stringify(tx)}, ${decoded}`
348
+ );
349
+ }
350
+ const hash = binToHex(
351
+ sha256.hash(sha256.hash(transactionBin)).reverse()
352
+ );
353
+ return { decoded, hash };
354
+ })
355
+ .map(({ decoded, hash }) => BCMR.makeAuthChainElement(decoded, hash));
356
+ } else {
357
+ // simply go back in history towards authhead
358
+ let stop = false;
359
+ let tx: ElectrumRawTransaction = { ...options.rawTx! };
360
+ let maxElements = 10;
361
+ while (stop == false || maxElements === 0) {
362
+ const vin = tx.vin.find((val) => val.vout === 0);
363
+ tx = await provider.getRawTransactionObject(vin!.txid);
364
+ try {
365
+ const pastElement = BCMR.makeAuthChainElement(tx, tx.hash);
366
+ chainBase.unshift(pastElement);
367
+ maxElements--;
368
+ } catch {
369
+ stop = true;
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // if we follow to head, we need to locate the next transaction spending our 0th output
376
+ // and repeat the building process recursively
377
+ if (options.followToHead) {
378
+ const child = await getAuthChainChild();
379
+ if (child) {
380
+ const chainHead = await BCMR.buildAuthChain({
381
+ transactionHash: child.rawTx.hash,
382
+ network: options.network,
383
+ rawTx: child.rawTx,
384
+ historyCache: child.historyCache,
385
+ followToHead: options.followToHead,
386
+ resolveBase: false,
387
+ });
388
+
389
+ // combine the authchain element with the rest obtained
390
+ return [...chainBase, element, ...chainHead].filter(
391
+ (val) => val.httpsUrl.length
392
+ );
393
+ }
394
+ }
395
+
396
+ // return the last chain element (or the only found in an edge case)
397
+ return [...chainBase, element].filter((val) => val.httpsUrl.length);
398
+ }
399
+
400
+ /**
401
+ * fetchAuthChainFromChaingraph Fetch the authchain information from a trusted external indexer
402
+ * The authchain in this implementation is specific to resolve to a valid metadata registry
403
+ *
404
+ * @param {string} options.chaingraphUrl (required) URL of a chaingraph indexer instance to fetch info from
405
+ * @param {string} options.transactionHash (required) transaction hash from which to build the auth chain
406
+ * @param {string?} options.network (default=undefined) network to query the data from, specific to the queried instance,
407
+ * can be 'mainnet', 'chipnet', or anything else.
408
+ * if left undefined all chaingraph transactions will be looked at, disregarding the chain
409
+ *
410
+ * @returns {AuthChain} returns the resolved authchain
411
+ */
412
+ public static async fetchAuthChainFromChaingraph(options: {
413
+ chaingraphUrl: string;
414
+ transactionHash: string;
415
+ network?: string;
416
+ }): Promise<AuthChain> {
417
+ if (!options.chaingraphUrl) {
418
+ throw new Error("Provide `chaingraphUrl` param.");
419
+ }
420
+
421
+ const response = await fetch(options.chaingraphUrl, {
422
+ method: "POST",
423
+ headers: {
424
+ Accept: "*/*",
425
+ "Content-Type": "application/json",
426
+ },
427
+ body: JSON.stringify({
428
+ operationName: null,
429
+ variables: {},
430
+ query: `
431
+ {
432
+ transaction(
433
+ where: {
434
+ hash:{_eq:"\\\\x${options.transactionHash}"}
435
+ }
436
+ ) {
437
+ hash
438
+ authchains {
439
+ authchain_length
440
+ migrations(
441
+ where: {
442
+ transaction: {
443
+ outputs: { locking_bytecode_pattern: { _like: "6a04%" } }
444
+ }
445
+ }
446
+ ) {
447
+ transaction {
448
+ hash
449
+ inputs(where:{ outpoint_index: { _eq:"0" } }){
450
+ outpoint_index
451
+ }
452
+ outputs(where: { locking_bytecode_pattern: { _like: "6a04%" } }) {
453
+ output_index
454
+ locking_bytecode
455
+ }
456
+ }
457
+ }
458
+ }
459
+ }
460
+ }`,
461
+ }),
462
+ });
463
+
464
+ const responseData = await response.json();
465
+
466
+ const result: AuthChain = [];
467
+ const migrations =
468
+ responseData.data.transaction[0]?.authchains[0].migrations;
469
+ if (!migrations) {
470
+ return result;
471
+ }
472
+
473
+ for (const migration of migrations) {
474
+ const transaction = migration.transaction[0];
475
+ if (!transaction) {
476
+ continue;
477
+ }
478
+ transaction.inputs.forEach(
479
+ (input) => (input.outpointIndex = Number(input.outpoint_index))
480
+ );
481
+ transaction.outputs.forEach((output) => {
482
+ output.outputIndex = Number(output.output_index);
483
+ output.lockingBytecode = hexToBin(
484
+ output.locking_bytecode.replace("\\x", "")
485
+ );
486
+ });
487
+ const txHash = transaction.hash.replace("\\x", "");
488
+ result.push(
489
+ BCMR.makeAuthChainElement(transaction as Transaction, txHash)
490
+ );
491
+ }
492
+
493
+ return result.filter(
494
+ (element) => element.contentHash.length && element.httpsUrl.length
495
+ );
496
+ }
497
+
498
+ /**
499
+ * addMetadataRegistryAuthChain Add BCMR metadata registry by resolving an authchain
500
+ *
501
+ * @param {string} options.transactionHash (required) transaction hash from which to build the auth chain
502
+ * @param {Network?} options.network (default=mainnet) network to query the data from
503
+ * @param {boolean?} options.followToHead (default=true) boolean flag to indicate that autchain building should progress to head (most recent registry version) or be stopped at this exact chain element
504
+ * @param {ElectrumRawTransaction?} options.rawTx cached raw transaction obtained previously, spares a Fulcrum call
505
+ *
506
+ * @returns {AuthChain} returns the resolved authchain
507
+ */
508
+ public static async addMetadataRegistryAuthChain(options: {
509
+ transactionHash: string;
510
+ network?: Network;
511
+ followToHead?: boolean;
512
+ rawTx?: ElectrumRawTransaction;
513
+ }): Promise<AuthChain> {
514
+ const authChain = await this.buildAuthChain({
515
+ ...options,
516
+ resolveBase: false,
517
+ });
518
+
519
+ if (!authChain.length) {
520
+ throw new Error(
521
+ `There were no BCMR entries in the resolved authchain ${JSON.stringify(
522
+ authChain,
523
+ null,
524
+ 2
525
+ )}`
526
+ );
527
+ }
528
+
529
+ const registry = await this.fetchMetadataRegistry(
530
+ authChain.reverse()[0].httpsUrl
531
+ );
532
+
533
+ this.addMetadataRegistry(registry);
534
+ return authChain;
535
+ }
536
+
537
+ /**
538
+ * getTokenInfo Return the token info (or the identity snapshot as per spec)
539
+ *
540
+ * @param {string} tokenId token id to look up
541
+ *
542
+ * @returns {IdentitySnapshot?} return the info for the token found, otherwise undefined
543
+ */
544
+ public static getTokenInfo(tokenId: string): IdentitySnapshot | undefined {
545
+ for (const registry of this.metadataRegistries.slice().reverse()) {
546
+ const history = registry.identities?.[tokenId];
547
+ if (!history) {
548
+ continue;
549
+ }
550
+ const latestIdentityIndex = Object.keys(history)[0];
551
+ if (latestIdentityIndex === undefined) {
552
+ continue;
553
+ }
554
+
555
+ return history[latestIdentityIndex];
556
+ }
557
+
558
+ return undefined;
559
+ }
560
+ }