diff --git a/e2e/helpers/rpc.helper.ts b/e2e/helpers/rpc.helper.ts new file mode 100644 index 0000000..81b16f2 --- /dev/null +++ b/e2e/helpers/rpc.helper.ts @@ -0,0 +1,127 @@ +import { readFileSync } from 'fs'; +import * as yaml from 'js-yaml'; +import axios, { AxiosRequestConfig } from 'axios'; + +export class BitcoinRPCUtil { + private readonly url: string; + private readonly axiosConfig: AxiosRequestConfig; + + constructor(configPath = './config/e2e.config.yaml') { + const config = yaml.load(readFileSync(configPath, 'utf8')) as Record; + const user = config.bitcoinCore.rpcUser; + const password = config.bitcoinCore.rpcPass; + const host = config.bitcoinCore.rpcHost; + const port = config.bitcoinCore.rpcPort; + const protocol = config.bitcoinCore.protocol; + this.url = `${protocol}://${user}:${password}@${host}:${port}/`; + this.axiosConfig = { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + }; + } + + public async request(config: AxiosRequestConfig): Promise { + try { + const response = await axios.request({ + ...this.axiosConfig, + ...config, + url: this.url, + }); + return response.data?.result; + } catch (error) { + throw new Error(`Request failed with status code ${error.response.status}`); + } + } + + async getBlockHeight(): Promise { + return await this.request({ + data: { + method: 'getblockcount', + params: [], + }, + }); + } + + async createWallet(walletName: string): Promise { + return await this.request({ + data: { + method: 'createwallet', + params: [walletName], + }, + }); + } + + async loadWallet(walletName: string): Promise { + return await this.request({ + data: { + method: 'loadwallet', + params: [walletName], + }, + }); + } + + async getNewAddress(): Promise { + return await this.request({ + data: { + method: 'getnewaddress', + params: [], + }, + }); + } + + async mineToAddress(numBlocks: number, address: string): Promise { + return await this.request({ + data: { + method: 'generatetoaddress', + params: [numBlocks, address], + }, + }); + } + + async sendToAddress(address: string, amount: number): Promise { + return await this.request({ + data: { + method: 'sendtoaddress', + params: [address, amount], + }, + }); + } + + async sendRawTransaction(rawTx: string): Promise { + return await this.request({ + data: { + method: 'sendrawtransaction', + params: [rawTx], + }, + }); + } + + async getTxOut(txid: string, vout: number): Promise { + return await this.request({ + data: { + method: 'gettxout', + params: [txid, vout], + }, + }); + } + + async getRawTransaction(txid: string): Promise { + return await this.request({ + data: { + method: 'getrawtransaction', + params: [txid], + }, + }); + } + + async listUnspent(addresses: string[]): Promise { + return await this.request({ + data: { + method: 'listunspent', + params: [addresses], + }, + }); + } +} \ No newline at end of file diff --git a/e2e/helpers/wallet.helper.ts b/e2e/helpers/wallet.helper.ts new file mode 100644 index 0000000..250bfa0 --- /dev/null +++ b/e2e/helpers/wallet.helper.ts @@ -0,0 +1,107 @@ +import { randomBytes } from 'crypto'; +import { mnemonicToSeedSync, generateMnemonic } from 'bip39'; +import { BIP32Factory } from 'bip32'; +import * as ecc from 'tiny-secp256k1'; +import { payments, Psbt } from 'bitcoinjs-lib'; + +const bip32 = BIP32Factory(ecc); + +export class WalletHelper { + private mnemonic: string; + private seed: Buffer; + private root: any; + + constructor(mnemonic: string = 'test test test test test test test test test test test test') { + this.mnemonic = mnemonic; + this.seed = mnemonicToSeedSync(this.mnemonic); + this.root = bip32.fromSeed(this.seed); + } + + getMnemonic(): string { + return this.mnemonic; + } + + generateAddresses(count: number, type: 'p2wpkh' | 'p2wsh' | 'p2tr'): string[] { + const addresses: string[] = []; + for (let i = 0; i < count; i++) { + const path = `m/84'/0'/0'/0/${i}`; + const child = this.root.derivePath(path); + let address: string; + + switch (type) { + case 'p2wpkh': + address = payments.p2wpkh({ pubkey: child.publicKey }).address!; + break; + case 'p2wsh': + address = payments.p2wsh({ + redeem: payments.p2ms({ m: 2, pubkeys: [child.publicKey, randomBytes(33)] }), + }).address!; + break; + case 'p2tr': + address = payments.p2tr({ + internalPubkey: child.publicKey.slice(1, 33), + }).address!; + break; + default: + throw new Error('Unsupported address type'); + } + + addresses.push(address); + } + return addresses; + } + + createWallet(): { mnemonic: string; addresses: string[] } { + const addresses = this.generateAddresses(10, 'p2wpkh'); + return { mnemonic: this.mnemonic, addresses }; + } + + /** + * Craft and sign a transaction sending 6 BTC to the provided Taproot address. + * + * @param utxos - Array of UTXOs to spend from. + * @param taprootAddress - The Taproot address to send to. + * @param fee - The fee to apply in satoshis. + * @returns {string} The raw signed transaction hex. + */ + craftTransaction( + utxos: Array<{ txid: string; vout: number; value: number; rawTx: string }>, + taprootAddress: string + ): string { + const psbt = new Psbt(); + + utxos.forEach((utxo, index) => { + psbt.addInput({ + hash: utxo.txid, + index: utxo.vout, + nonWitnessUtxo: Buffer.from(utxo.rawTx, 'hex'), + }); + }); + + // Add the output to the Taproot address (6 BTC) + const totalInputValue = utxos.reduce((acc, utxo) => acc + utxo.value, 0); + const outputValue = 6 * 1e8; + const fee = 1 * 1e8; + + if (totalInputValue < outputValue + fee) { + throw new Error('Insufficient funds'); + } + + psbt.addOutput({ + address: taprootAddress, + value: BigInt(outputValue), + }); + + // Sign the inputs with the corresponding private keys + utxos.forEach((utxo, index) => { + const child = this.root.derivePath(`m/84'/0'/0'/0/${index}`); + const keyPair = child; + psbt.signInput(index, keyPair); + }); + + psbt.finalizeAllInputs(); + + const rawTx = psbt.extractTransaction().toHex(); + return rawTx; + } +} diff --git a/e2e/indexer.e2e-spec.ts b/e2e/indexer.e2e-spec.ts new file mode 100644 index 0000000..999815b --- /dev/null +++ b/e2e/indexer.e2e-spec.ts @@ -0,0 +1,80 @@ +import { WalletHelper } from '@e2e/helpers/wallet.helper'; +import { BitcoinRPCUtil } from '@e2e/helpers/rpc.helper'; +import { ApiHelper } from '@e2e/helpers/api.helper'; +import { parseSilentBlock } from '@/common/common'; + +describe('WalletHelper Integration Tests', () => { + let walletHelper: WalletHelper; + let bitcoinRPCUtil: BitcoinRPCUtil; + let apiHelper: ApiHelper; + let initialAddress: string; + let newAddress: string; + let p2wkhAddresses: string[]; + let taprootAddress: string; + let utxos: { txid: string, vout: number, value: number, rawTx: string }[]; + + beforeAll(async () => { + walletHelper = new WalletHelper(); + bitcoinRPCUtil = new BitcoinRPCUtil(); + apiHelper = new ApiHelper(); + + await bitcoinRPCUtil.createWallet('test_wallet'); + initialAddress = await bitcoinRPCUtil.getNewAddress(); + + taprootAddress = walletHelper.generateAddresses(1, 'p2tr')[0]; + p2wkhAddresses = walletHelper.generateAddresses(8, 'p2wpkh'); + + await bitcoinRPCUtil.mineToAddress(101, initialAddress); + + for (const address of p2wkhAddresses) { + await bitcoinRPCUtil.sendToAddress(address, 1); + } + + newAddress = await bitcoinRPCUtil.getNewAddress(); + await bitcoinRPCUtil.mineToAddress(6, newAddress); + + utxos = []; + for (let i = 0; i < 6; i++) { + const txid = await bitcoinRPCUtil.sendToAddress(p2wkhAddresses[i], 1); + for (let vout = 0; vout < 2; vout++) { + const utxo = await bitcoinRPCUtil.getTxOut(txid, vout); + if (utxo && Math.round(utxo.value * 1e8) === 1e8) { + utxos.push({ + txid: txid, + vout: vout, + value: Math.round(utxo.value * 1e8), + rawTx: await bitcoinRPCUtil.getRawTransaction(txid), + }); + break; + } + } + } + }); + + it('should craft and broadcast transactions, then verify them', async () => { + const rawTx = walletHelper.craftTransaction( + utxos.slice(0, 5), + taprootAddress, // Send 6 BTC to taproot address with 1 BTC fee + ); + + await bitcoinRPCUtil.sendRawTransaction(rawTx); + await bitcoinRPCUtil.mineToAddress(1, initialAddress); + + await new Promise(resolve => setTimeout(resolve, 30000)); + + const response = await apiHelper.get(`/silent-block/height/108`); + const silentBlock = response.data; + + const decodedBlock = parseSilentBlock(silentBlock); + const transactions = decodedBlock.transactions; + + const foundTx = transactions.find((tx: any) => tx.txid === rawTx); + expect(foundTx).toBeDefined(); + + expect(foundTx.vout.length).toBeGreaterThan(0); + + const output = foundTx.vout.find((vout: any) => vout.address === taprootAddress); + expect(output).toBeDefined(); + expect(output.value).toEqual(5 * 1e8); + }); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 47adfb3..67abc31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,16 @@ "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", "axios": "^1.7.2", + "bip32": "^5.0.0-rc.0", + "bip39": "^3.1.0", + "bitcoinjs-lib": "^7.0.0-rc.0", "currency.js": "^2.0.4", "js-yaml": "^4.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", + "tiny-secp256k1": "^2.2.3", "typeorm": "^0.3.20" }, "devDependencies": { @@ -2173,6 +2177,17 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/@noble/hashes": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz", + "integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2286,6 +2301,14 @@ "node": ">=14" } }, + "node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3545,6 +3568,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3572,6 +3600,11 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3592,6 +3625,128 @@ "file-uri-to-path": "1.0.0" } }, + "node_modules/bip174": { + "version": "3.0.0-rc.1", + "resolved": "https://registry.npmjs.org/bip174/-/bip174-3.0.0-rc.1.tgz", + "integrity": "sha512-+8P3BpSairVNF2Nee6Ksdc1etIjWjBOi/MH0MwKtq9YaYp+S2Hk2uvup0e8hCT4IKlS58nXJyyQVmW92zPoD4Q==", + "dependencies": { + "uint8array-tools": "^0.0.9", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bip174/node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/bip32": { + "version": "5.0.0-rc.0", + "resolved": "https://registry.npmjs.org/bip32/-/bip32-5.0.0-rc.0.tgz", + "integrity": "sha512-5hVFGrdCnF8GB1Lj2eEo4PRE7+jp+3xBLnfNjydivOkMvKmUKeJ9GG8uOy8prmWl3Oh154uzgfudR1FRkNBudA==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "@scure/base": "^1.1.1", + "uint8array-tools": "^0.0.8", + "valibot": "^0.37.0", + "wif": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bip32/node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/bip32/node_modules/valibot": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.37.0.tgz", + "integrity": "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bitcoinjs-lib": { + "version": "7.0.0-rc.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-7.0.0-rc.0.tgz", + "integrity": "sha512-7CQgOIbREemKR/NT2uc3uO/fkEy+6CM0sLxboVVY6bv6DbZmPt3gg5Y/hhWgQFeZu5lfTbtVAv32MIxf7lMh4g==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^3.0.0-rc.0", + "bs58check": "^4.0.0", + "uint8array-tools": "^0.0.9", + "valibot": "^0.38.0", + "varuint-bitcoin": "^2.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/bitcoinjs-lib/node_modules/typescript": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/bitcoinjs-lib/node_modules/uint8array-tools": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.9.tgz", + "integrity": "sha512-9vqDWmoSXOoi+K14zNaf6LBV51Q8MayF0/IiQs3GlygIKUYtog603e6virExkjjFosfJUBI4LhbQK1iq8IG11A==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/bitcoinjs-lib/node_modules/valibot": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.38.0.tgz", + "integrity": "sha512-RCJa0fetnzp+h+KN9BdgYOgtsMAG9bfoJ9JSjIhFHobKWVWyzM3jjaeNTdpFK9tQtf3q1sguXeERJ/LcmdFE7w==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -3727,6 +3882,23 @@ "node": ">= 6" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "dependencies": { + "base-x": "^5.0.0" + } + }, + "node_modules/bs58check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-4.0.0.tgz", + "integrity": "sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^6.0.0" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -11424,6 +11596,25 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/tiny-secp256k1": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz", + "integrity": "sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q==", + "dependencies": { + "uint8array-tools": "0.0.7" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tiny-secp256k1/node_modules/uint8array-tools": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", + "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -12002,6 +12193,14 @@ "node": ">=8" } }, + "node_modules/uint8array-tools": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.8.tgz", + "integrity": "sha512-xS6+s8e0Xbx++5/0L+yyexukU7pz//Yg6IHg3BKhXotg1JcYtgxVcUctQ0HxLByiJzpAkNFawz1Nz5Xadzo82g==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -12141,6 +12340,14 @@ "node": ">= 0.10" } }, + "node_modules/varuint-bitcoin": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/varuint-bitcoin/-/varuint-bitcoin-2.0.0.tgz", + "integrity": "sha512-6QZbU/rHO2ZQYpWFDALCDSRsXbAs1VOEmXAxtbtjLtKuMJ/FQ8YbhfxlaiKv5nklci0M6lZtlZyxo9Q+qNnyog==", + "dependencies": { + "uint8array-tools": "^0.0.8" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -12316,6 +12523,14 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wif": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/wif/-/wif-5.0.0.tgz", + "integrity": "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA==", + "dependencies": { + "bs58check": "^4.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 0af3aa6..3e7fb5a 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,16 @@ "@nestjs/typeorm": "^10.0.2", "@nestjs/websockets": "^10.3.7", "axios": "^1.7.2", + "bip32": "^5.0.0-rc.0", + "bip39": "^3.1.0", + "bitcoinjs-lib": "^7.0.0-rc.0", "currency.js": "^2.0.4", "js-yaml": "^4.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0", "secp256k1": "^5.0.0", "sqlite3": "^5.1.7", + "tiny-secp256k1": "^2.2.3", "typeorm": "^0.3.20" }, "devDependencies": { diff --git a/src/common/common.ts b/src/common/common.ts index ac8a415..1eb18bd 100644 --- a/src/common/common.ts +++ b/src/common/common.ts @@ -147,3 +147,54 @@ export const varIntSize = (value: number): number => { else if (value <= 0xffffffff) return 5; else return 9; }; + +export const readVarInt = (data: Buffer, cursor: number): number => { + const firstByte = data.readUInt8(cursor); + if (firstByte < 0xfd) { + return firstByte; + } else if (firstByte === 0xfd) { + return data.readUInt16LE(cursor + 1); + } else if (firstByte === 0xfe) { + return data.readUInt32LE(cursor + 1); + } else { + return Number(data.readBigUInt64LE(cursor + 1)); + } +}; + +export const parseSilentBlock = (data: Buffer) => { + const type = data.readUInt8(0); + const transactions = []; + + let cursor = 1; + const count = readVarInt(data, cursor); + cursor += varIntSize(count); + + for (let i = 0; i < count; i++) { + const txid = data.subarray(cursor, cursor + 32).toString('hex'); + cursor += 32; + + const outputs = []; + const outputCount = readVarInt(data, cursor); + cursor += varIntSize(outputCount); + + for (let j = 0; j < outputCount; j++) { + const value = Number(data.readBigUInt64BE(cursor)); + cursor += 8; + + const pubkey = data.subarray(cursor, cursor + 32).toString('hex'); + cursor += 32; + + const vout = data.readUint32BE(cursor); + cursor += 4; + + outputs.push({ value, pubkey, vout }); + } + + const scanTweak = data.subarray(cursor, cursor + 33).toString('hex'); + cursor += 33; + + transactions.push({ txid, outputs, scanTweak }); + } + + return { type, transactions }; +};