diff --git a/.gitignore b/.gitignore index b2008f4c..83b39d9c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,8 @@ out/ # Dotenv file .env +# node modules +node_modules/* + # generated docs /docs/docs/api/* \ No newline at end of file diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 2c2b4ab5..eacd8180 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -33,6 +33,17 @@ const config = { locales: ["en"], }, + plugins: [ + [ + 'docusaurus-plugin-typedoc', + { + entryPoints: ['../sdk/src/*'], + out: 'api/ts-sdk', + tsconfig: 'tsconfig.json', + }, + ], + + ], presets: [ [ "classic", @@ -82,6 +93,12 @@ const config = { position: "left", label: "Build", }, + { + type: "docSidebar", + sidebarId: "apiSidebar", + position: "left", + label: "API", + }, { href: GITHUB_LINK, label: "GitHub", diff --git a/docs/package.json b/docs/package.json index 37c7efb6..12ccf2a9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,10 +15,10 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "2.4.1", - "@docusaurus/preset-classic": "2.4.1", - "@docusaurus/theme-live-codeblock": "^2.4.1", - "@docusaurus/theme-mermaid": "^2.4.1", + "@docusaurus/core": "^2.4.3", + "@docusaurus/preset-classic": "^2.4.3", + "@docusaurus/theme-live-codeblock": "^2.4.3", + "@docusaurus/theme-mermaid": "^2.4.3", "@easyops-cn/docusaurus-search-local": "^0.36.0", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", @@ -29,6 +29,9 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^2.0.0", + "docusaurus-plugin-typedoc": "^0.20.2", + "typedoc": "^0.25.2", + "typedoc-plugin-markdown": "^3.16.0", "typescript": "^5.1.6" }, "browserslist": { diff --git a/docs/sidebars.js b/docs/sidebars.js index 8fa64533..88fc7f6e 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -16,8 +16,48 @@ const sidebars = { // By default, Docusaurus generates a sidebar from the docs folder structure rootSidebar: [{type: 'autogenerated', dirName: '.'}], learnSidebar: [{type: 'autogenerated', dirName: 'learn'}], - // guideSidebar: [{type: 'autogenerated', dirName: 'guide'}], buildSidebar: [{type: 'autogenerated', dirName: 'build'}], + apiSidebar: [ + { + type: 'category', + label: 'TypeScript API', + collapsed: false, + items: [ + "api/ts-sdk/modules", + { + type: 'category', + label: 'Modules', + items: [ + { + type: 'autogenerated', + dirName: 'api/ts-sdk/modules' + } + ] + }, + { + type: 'category', + label: 'Interfaces', + items: [ + { + type: 'autogenerated', + dirName: 'api/ts-sdk/interfaces' + } + ] + }, + { + type: 'category', + label: 'Classes', + items: [ + { + type: 'autogenerated', + dirName: 'api/ts-sdk/classes' + } + ] + } + ], + } + ], + // guideSidebar: [{type: 'autogenerated', dirName: 'guide'}], // runSidebar: [{type: 'autogenerated', dirName: 'run'}], // But you can create a sidebar manually diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 6f475698..fc6a2c5b 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -3,5 +3,7 @@ "extends": "@tsconfig/docusaurus/tsconfig.json", "compilerOptions": { "baseUrl": "." - } + }, + "include": ["../sdk/src/*"], // Adjust the path as needed + "exclude": ["../sdk/src/index.ts"] } diff --git a/sdk/src/electrs.ts b/sdk/src/electrs.ts index a0d1d5d5..369bfafc 100644 --- a/sdk/src/electrs.ts +++ b/sdk/src/electrs.ts @@ -1,7 +1,22 @@ +/** + * Base path for the mainnet Esplora API. + * @default "https://btc-mainnet.interlay.io" + */ export const MAINNET_ESPLORA_BASE_PATH = "https://btc-mainnet.interlay.io"; +/** + * Base path for the testnet Esplora API. + * @default "https://btc-testnet.interlay.io" + */ export const TESTNET_ESPLORA_BASE_PATH = "https://btc-testnet.interlay.io"; -export const REGTEST_ESPLORA_BASE_PATH = "http://localhost:3002"; +/** + * Base path for the regtest Esplora API. + * @default "http://localhost:3003" + */ +export const REGTEST_ESPLORA_BASE_PATH = "http://localhost:3003"; +/** + * @ignore + */ export interface MerkleProof { blockHeight: number merkle: string, @@ -10,35 +25,129 @@ export interface MerkleProof { export interface ElectrsClient { /** - * @param height The height of the Bitcoin block - * @returns The block hash of the Bitcoin block + * Get the block hash of the Bitcoin block at a specific height. + * + * This function retrieves the block hash for the Bitcoin block at the given height. + * + * @param {number} height - The height of the Bitcoin block. + * @returns {Promise} A promise that resolves to the block hash of the Bitcoin block. + * + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const blockHeight = 123456; + * electrsClient.getBlockHash(blockHeight) + * .then((blockHash) => { + * console.log(`Block hash at height ${blockHeight}: ${blockHash}`); + * }) + * .catch((error) => { + * console.error(`Error: ${error}`); + * }); + * ``` */ getBlockHash(height: number): Promise; + /** - * @param height The hash of the Bitcoin block - * @returns The raw block header, represented as a hex string + * Get the raw block header, represented as a hex string, for a Bitcoin block with a given hash. + * + * @param {string} hash - The hash of the Bitcoin block. + * @returns {Promise} A promise that resolves to the raw block header as a hex string. + * + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const blockHash = 'your_block_hash_here'; + * electrsClient.getBlockHeader(blockHash) + * .then((blockHeader) => { + * console.log(`Raw block header for block with hash ${blockHash}: ${blockHeader}`); + * }) + * .catch((error) => { + * console.error(`Error: ${error}`); + * }); + * ``` */ getBlockHeader(hash: string): Promise; + /** - * @param txId The ID of a Bitcoin transaction - * @returns The transaction data, represented as a hex string + * Get the transaction data, represented as a hex string, for a Bitcoin transaction with a given ID (txId). + * + * @param {string} txId - The ID of a Bitcoin transaction. + * @returns {Promise} A promise that resolves to the transaction data as a hex string. + * + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const transactionId = 'your_transaction_id_here'; + * electrsClient.getTransactionHex(transactionId) + * .then((transactionHex) => { + * console.log(`Transaction hex for transaction with ID ${transactionId}: ${transactionHex}`); + * }) + * .catch((error) => { + * console.error(`Error: ${error}`); + * }); + * ``` */ getTransactionHex(txId: string): Promise; + /** - * @param txId The ID of a Bitcoin transaction - * @returns The encoded merkle inclusion proof for the transaction + * Get the encoded merkle inclusion proof for a Bitcoin transaction with a given ID (txId). + * + * @param {string} txId - The ID of a Bitcoin transaction. + * @returns {Promise} A promise that resolves to the encoded merkle inclusion proof. + * + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const transactionId = 'your_transaction_id_here'; + * electrsClient.getMerkleProof(transactionId) + * .then((merkleProof) => { + * console.log(`Merkle inclusion proof for transaction with ID ${transactionId}: ${merkleProof}`); + * }) + * .catch((error) => { + * console.error(`Error: ${error}`); + * }); + * ``` */ getMerkleProof(txId: string): Promise; -} + } + + /** + * @ignore + */ function encodeElectrsMerkleProof(merkle: string[]): string { // convert to little-endian return merkle.map(item => Buffer.from(item, "hex").reverse().toString("hex")).join(''); } + +/** + * The `DefaultElectrsClient` class provides a client for interacting with an Esplora API + * for Bitcoin network data retrieval. + */ export class DefaultElectrsClient implements ElectrsClient { private basePath: string; + /** + * Create an instance of the `DefaultElectrsClient` with the specified network or URL. + * If the `networkOrUrl` parameter is omitted, it defaults to "mainnet." + * + * @param networkOrUrl The Bitcoin network (e.g., "mainnet," "testnet," "regtest") + * + * @returns An instance of the `DefaultElectrsClient` configured for the specified network or URL. + * + * @example + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * + * @example + * // Create a client for the mainnet using the default URL. + * const electrsClientMainnet = new DefaultElectrsClient(); + */ constructor(networkOrUrl: string = "mainnet") { switch (networkOrUrl) { case "mainnet": @@ -55,18 +164,34 @@ export class DefaultElectrsClient implements ElectrsClient { } } + + /** + * @ignore + */ async getBlockHash(height: number): Promise { return this.getText(`${this.basePath}/block-height/${height}`); } + + /** + * @ignore + */ async getBlockHeader(hash: string): Promise { return this.getText(`${this.basePath}/block/${hash}/header`); } + + /** + * @ignore + */ async getTransactionHex(txId: string): Promise { return this.getText(`${this.basePath}/tx/${txId}/hex`); } + + /** + * @ignore + */ async getMerkleProof(txId: string): Promise { const response = await this.getJson<{ "block_height": number, @@ -80,6 +205,10 @@ export class DefaultElectrsClient implements ElectrsClient { }; } + + /** + * @ignore + */ async getJson(url: string): Promise { const response = await fetch(url); if (!response.ok) { @@ -88,6 +217,10 @@ export class DefaultElectrsClient implements ElectrsClient { return await response.json() as Promise; } + + /** + * @ignore + */ async getText(url: string): Promise { const response = await fetch(url); if (!response.ok) { diff --git a/sdk/src/relay.ts b/sdk/src/relay.ts index 4218bb6e..9de303ad 100644 --- a/sdk/src/relay.ts +++ b/sdk/src/relay.ts @@ -1,19 +1,63 @@ +//@ts-nocheck +/** + * @ignore + */ import { Transaction } from "bitcoinjs-lib"; +//@ts-nocheck +/** + * @ignore + */ import { ElectrsClient } from "./electrs"; -import { encodeRawInput, encodeRawOutput, encodeRawWitness } from "./utils"; +/** + * @ignore + */ +import { encodeRawInput, encodeRawOutput, encodeRawWitness, encodeRawWitness } from "./utils"; +/** + * Represents information about a Bitcoin transaction. + */ export interface BitcoinTxInfo { + /** + * The transaction version. + */ version: string, + /** + * The input vector of the transaction, encoded as a hex string. + */ inputVector: string, + /** + * The output vector of the transaction, encoded as a hex string. + */ outputVector: string, - locktime: string, + /** + * The transaction locktime. + */ + locktime: string; + /** + * The transaction witness. + */ witnessVector?: string, } +/** + * Retrieves information about a Bitcoin transaction, such as version, input vector, output vector, and locktime. + * + * @param electrsClient - An ElectrsClient instance for interacting with the Electrum server. + * @param txId - The ID of the Bitcoin transaction. + * @returns A promise that resolves to a BitcoinTxInfo object. + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const txId = "279121610d9575d132c95312c032116d6b8a58a3a31f69adf9736b493de96a16"; //enter the transaction id here + * const info = await getBitcoinTxInfo(electrsClient, txId); + * ``` + */ export async function getBitcoinTxInfo( electrsClient: ElectrsClient, txId: string, forWitness?: boolean, + forWitness?: boolean, ): Promise { const txHex = await electrsClient.getTransactionHex(txId); const tx = Transaction.fromHex(txHex); @@ -30,15 +74,43 @@ export async function getBitcoinTxInfo( outputVector: encodeRawOutput(tx).toString("hex"), locktime: locktimeBuffer.toString("hex"), witnessVector: forWitness ? encodeRawWitness(tx).toString("hex") : undefined, + witnessVector: forWitness ? encodeRawWitness(tx).toString("hex") : undefined, } } +/** + * Represents a Bitcoin transaction proof, including the merkle proof, transaction index in a block, and Bitcoin headers. + */ export interface BitcoinTxProof { - merkleProof: string, - txIndexInBlock: number, - bitcoinHeaders: string, + /** + * The merkle proof for the Bitcoin transaction. + */ + merkleProof: string; + /** + * The index of the transaction in the block. + */ + txIndexInBlock: number; + /** + * Concatenated Bitcoin headers for proof verification. + */ + bitcoinHeaders: string; } +/** + * Retrieves a proof for a Bitcoin transaction, including the merkle proof, transaction index in the block, and Bitcoin headers. + * + * @param electrsClient - An ElectrsClient instance for interacting with the Electrum server. + * @param txId - The ID of the Bitcoin transaction. + * @param txProofDifficultyFactor - The number of block headers to retrieve for proof verification. + * @example + * ```typescript + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const txId = "279121610d9575d132c95312c032116d6b8a58a3a31f69adf9736b493de96a16";//enter the transaction id here + * const txProofDifficultyFactor = "1";//enter the difficulty factor + * const info = await getBitcoinTxProof(electrsClient, txId, txProofDifficultyFactor); + * ``` + */ export async function getBitcoinTxProof( electrsClient: ElectrsClient, txId: string, @@ -53,7 +125,31 @@ export async function getBitcoinTxProof( bitcoinHeaders: bitcoinHeaders, } } - + +/** + * Retrieves Bitcoin block headers using an Electrs client. + * + * @param electrsClient - The ElectrsClient instance used to interact with the Esplora API. + * @param startHeight - The starting block height from which to fetch headers. + * @param numBlocks - The number of consecutive block headers to retrieve. + * @returns A Promise that resolves to a concatenated string of Bitcoin block headers. + * + * @throws {Error} If there is an issue with fetching block headers. + * + * @example + * const BITCOIN_NETWORK = "regtest"; + * const electrsClient = new DefaultElectrsClient(BITCOIN_NETWORK); + * const startHeight = 0; + * const numBlocks = 10; + * + * getBitcoinHeaders(electrsClient, startHeight, numBlocks) + * .then(headers => { + * console.log(headers); // Concatenated block headers as a string. + * }) + * .catch(error => { + * console.error(`Error: ${error.message}`); + * }); + */ export async function getBitcoinHeaders( electrsClient: ElectrsClient, startHeight: number, diff --git a/sdk/src/utils.ts b/sdk/src/utils.ts index b4574f7f..04bd6c9a 100644 --- a/sdk/src/utils.ts +++ b/sdk/src/utils.ts @@ -1,13 +1,35 @@ +//@ts-nocheck +/** + * @ignore + */ import { Block } from "bitcoinjs-lib"; +//@ts-nocheck +/** + * @ignore + */ import { BufferWriter, varuint } from "bitcoinjs-lib/src/bufferutils"; +//@ts-nocheck +/** + * @ignore + */ import { hash256 } from "bitcoinjs-lib/src/crypto"; +//@ts-nocheck +/** + * @ignore + */ import { Output, Transaction } from "bitcoinjs-lib/src/transaction"; +/** + * @ignore + */ function varSliceSize(someScript: Buffer): number { const length = someScript.length; return varuint.encodingLength(length) + length; } +/** + * @ignore + */ export function encodeRawInput(tx: Transaction) { const inputSize = varuint.encodingLength(tx.ins.length) + tx.ins.reduce((sum, input) => { return sum + 40 + varSliceSize(input.script); @@ -27,10 +49,16 @@ export function encodeRawInput(tx: Transaction) { return inputBuffer; } +/** + * @ignore + */ function isOutput(out: Output): boolean { return out.value !== undefined; } +/** + * @ignore + */ export function encodeRawOutput(tx: Transaction) { const outputSize = varuint.encodingLength(tx.outs.length) + tx.outs.reduce((sum, output) => { return sum + 8 + varSliceSize(output.script); @@ -53,6 +81,9 @@ export function encodeRawOutput(tx: Transaction) { return outputBuffer; } +/** + * @ignore + */ function vectorSize(someVector: Buffer[]): number { const length = someVector.length; @@ -64,6 +95,9 @@ function vectorSize(someVector: Buffer[]): number { ); } +/** + * @ignore + */ export function encodeRawWitness(tx: Transaction) { const witnessSize = tx.ins.reduce((sum, input) => { return sum + vectorSize(input.witness); @@ -79,6 +113,9 @@ export function encodeRawWitness(tx: Transaction) { return witnessBuffer; } +/** + * @ignore + */ function chunkArray(array: T[], chunkSize: number): T[][] { const chunkedArray: T[][] = []; let index = 0; @@ -89,6 +126,13 @@ function chunkArray(array: T[], chunkSize: number): T[][] { return chunkedArray; } +/** + * Create a Merkle branch and root based on a list of hashes and a specific index. + * + * @param hashes - An array of hashes for Merkle construction. + * @param index - The index of the hash for which the branch and root are calculated. + * @returns An object containing the Merkle branch and root. + */ // https://github.com/Blockstream/electrs/blob/fd35014283c7d3a7a85c77b9fd647c9f09de12c9/src/util/electrum_merkle.rs#L86-L105 function createMerkleBranchAndRoot(hashes: Buffer[], index: number): { merkle: Buffer[], @@ -111,8 +155,14 @@ function createMerkleBranchAndRoot(hashes: Buffer[], index: number): { }; } -// used to construct merkle proofs from the raw block data, especially useful for -// constructing a non-standard "witness proof" which is not currently supported by electrs +/** + * Retrieve a Merkle proof for a Bitcoin transaction from a block's raw data. + * + * @param block - The Bitcoin block containing the transaction. + * @param txHash - The transaction hash to construct a proof for. + * @param forWitness - Set to `true` to construct a witness proof (default is `false`). + * @returns An object containing the position, proof, and root of the Merkle proof. + */ export function getMerkleProof(block: Block, txHash: string, forWitness?: boolean) { const txIds = block.transactions!.map(tx => tx.getHash(forWitness)); const pos = txIds.map(value => value.toString("hex")).indexOf(txHash);