UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

616 lines (615 loc) 26.3 kB
import { fromBase64, fromHex, toBase64, toHex } from '@btc-vision/bitcoin'; import { Address, AddressMap, AddressVerificator, BufferHelper, ChallengeSolution, } from '@btc-vision/transaction'; import '../serialize/BigInt.js'; import { Block } from '../block/Block.js'; import { BlockGasParameters } from '../block/BlockGasParameters.js'; import { parseBlockWitnesses } from '../block/BlockWitness.js'; import { CallResult } from '../contracts/CallResult.js'; import { ContractData } from '../contracts/ContractData.js'; import { TransactionOutputFlags } from '../contracts/enums/TransactionFlags.js'; import { Epoch } from '../epoch/Epoch.js'; import { EpochWithSubmissions } from '../epoch/EpochSubmission.js'; import { EpochTemplate } from '../epoch/EpochTemplate.js'; import { SubmittedEpoch } from '../epoch/SubmittedEpoch.js'; import { MempoolTransactionParser } from '../mempool/MempoolTransactionParser.js'; import { StoredValue } from '../storage/StoredValue.js'; import { TransactionReceipt } from '../transactions/metadata/TransactionReceipt.js'; import { TransactionParser } from '../transactions/TransactionParser.js'; import { decodeRevertData } from '../utils/RevertDecoder.js'; import { UTXOsManager } from '../utxos/UTXOsManager.js'; import { JSONRpcMethods } from './interfaces/JSONRpcMethods.js'; export class AbstractRpcProvider { network; nextId = 0; chainId; gasCache; lastFetchedGas = 0; challengeCache; csvCache = new AddressMap(); constructor(network) { this.network = network; } _utxoManager = new UTXOsManager(this); get utxoManager() { return this._utxoManager; } getCSV1ForAddress(address) { const cached = this.csvCache.get(address); if (cached) return cached; const csv = address.toCSV(1, this.network); this.csvCache.set(address, csv); return csv; } async getPublicKeyInfo(addressRaw, isContract) { const address = addressRaw.toString(); try { const pubKeyInfo = await this.getPublicKeysInfo(address, isContract); return (pubKeyInfo[address] || pubKeyInfo[address.startsWith('0x') ? address.slice(2) : address]); } catch (e) { if (AddressVerificator.isValidPublicKey(address, this.network)) { return Address.fromString(address); } throw e; } } validateAddress(addr, network) { let validationResult = null; if (addr instanceof Address) { validationResult = AddressVerificator.detectAddressType(addr.toHex(), network); } else if (typeof addr === 'string') { validationResult = AddressVerificator.detectAddressType(addr, network); } else { throw new Error(`Invalid type: ${typeof addr} for address: ${addr}`); } return validationResult; } async getBlockNumber() { const payload = this.buildJsonRpcPayload(JSONRpcMethods.BLOCK_BY_NUMBER, []); const rawBlockNumber = await this.callPayloadSingle(payload); const result = rawBlockNumber.result; return BigInt(result); } async getBlockByChecksum(checksum, prefetchTxs = false) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_BLOCK_BY_CHECKSUM, [checksum, prefetchTxs]); const block = await this.callPayloadSingle(payload); if ('error' in block) { throw new Error(`Error fetching block by checksum: ${block.error?.message || 'Unknown error'}`); } const result = block.result; return new Block(result, this.network); } async getChallenge() { if (this.challengeCache && Date.now() < this.challengeCache.expireAt) { return this.challengeCache.challenge; } const payload = this.buildJsonRpcPayload(JSONRpcMethods.TRANSACTION_PREIMAGE, []); const rawChallenge = await this.callPayloadSingle(payload); if ('error' in rawChallenge) { throw new Error(`Error fetching preimage: ${rawChallenge.error?.message || 'Unknown error'}`); } const result = rawChallenge.result; if (!result || !result.solution) { throw new Error('No challenge found. OPNet is probably not active yet on this blockchain.'); } const solutionHex = result.solution.startsWith('0x') ? result.solution.slice(2) : result.solution; if (solutionHex === '0'.repeat(64)) { throw new Error('No valid challenge found. OPNet is probably not active yet on this blockchain.'); } const challengeSolution = new ChallengeSolution(result); this.challengeCache = { challenge: challengeSolution, expireAt: Date.now() + 10_000, }; return challengeSolution; } async getBlock(blockNumberOrHash, prefetchTxs = false) { const method = typeof blockNumberOrHash === 'string' ? JSONRpcMethods.GET_BLOCK_BY_HASH : JSONRpcMethods.GET_BLOCK_BY_NUMBER; const payload = this.buildJsonRpcPayload(method, [ blockNumberOrHash, prefetchTxs, ]); const block = await this.callPayloadSingle(payload); if ('error' in block) { throw new Error(`Error fetching block: ${block.error?.message || 'Unknown error'}`); } const result = block.result; return new Block(result, this.network); } async getBlocks(blockNumbers, prefetchTxs = false) { const payloads = blockNumbers.map((blockNumber) => { return this.buildJsonRpcPayload(JSONRpcMethods.GET_BLOCK_BY_NUMBER, [ blockNumber, prefetchTxs, ]); }); const blocks = await this.callMultiplePayloads(payloads); if ('error' in blocks) { const error = blocks.error; throw new Error(`Error fetching block: ${error.message}`); } return blocks.map((block) => { if ('error' in block) { throw new Error(`Error fetching block: ${block.error}`); } const result = block.result; return new Block(result, this.network); }); } async getBlockByHash(blockHash) { return await this.getBlock(blockHash); } async getBalance(address, filterOrdinals = true) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_BALANCE, [ address, filterOrdinals, ]); const rawBalance = await this.callPayloadSingle(payload); const result = rawBalance.result; if (!result || (result && !result.startsWith('0x'))) { throw new Error(`Invalid balance returned from provider: ${result}`); } return BigInt(result); } async getBalances(addressesLike, filterOrdinals = true) { const payloads = addressesLike.map((address) => { return this.buildJsonRpcPayload(JSONRpcMethods.GET_BALANCE, [address, filterOrdinals]); }); const balances = await this.callMultiplePayloads(payloads); if ('error' in balances) { const error = balances.error; throw new Error(`Error fetching block: ${error.message}`); } const resultBalance = {}; for (let i = 0; i < balances.length; i++) { const balance = balances[i]; const address = addressesLike[i]; if (!address) throw new Error('Impossible index.'); if ('error' in balance) { throw new Error(`Error fetching block: ${balance.error}`); } const result = balance.result; if (!result || (result && !result.startsWith('0x'))) { throw new Error(`Invalid balance returned from provider: ${result}`); } resultBalance[address] = BigInt(result); } return resultBalance; } async getTransaction(txHash) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_TRANSACTION_BY_HASH, [txHash]); const rawTransaction = await this.callPayloadSingle(payload); const result = rawTransaction.result; if ('error' in rawTransaction) { throw new Error(`Error fetching transaction: ${rawTransaction.error?.message || 'Unknown error'}`); } return TransactionParser.parseTransaction(result, this.network); } async getTransactionReceipt(txHash) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_TRANSACTION_RECEIPT, [txHash]); const rawTransaction = await this.callPayloadSingle(payload); return new TransactionReceipt(rawTransaction.result, this.network); } getNetwork() { return this.network; } async getChainId() { if (this.chainId !== undefined) return this.chainId; const payload = this.buildJsonRpcPayload(JSONRpcMethods.CHAIN_ID, []); const rawChainId = await this.callPayloadSingle(payload); if ('error' in rawChainId) { throw new Error(`Something went wrong while fetching: ${rawChainId.error}`); } const chainId = rawChainId.result; this.chainId = BigInt(chainId); return this.chainId; } async getCode(address, onlyBytecode = false) { const addressStr = address.toString(); const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_CODE, [ addressStr, onlyBytecode, ]); const rawCode = await this.callPayloadSingle(payload); if (rawCode.error) { throw new Error(`${rawCode.error.code}: Something went wrong while fetching: ${rawCode.error.message}`); } const result = rawCode.result; if ('contractAddress' in result) { return new ContractData(result); } else { return fromBase64(result.bytecode); } } async getStorageAt(address, rawPointer, proofs = true, height) { const addressStr = address.toString(); const pointer = typeof rawPointer === 'string' ? rawPointer : this.bigintToBase64(rawPointer); const params = [addressStr, pointer, proofs]; if (height) { params.push(height.toString()); } const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_STORAGE_AT, params); const rawStorage = await this.callPayloadSingle(payload); const result = rawStorage.result; return new StoredValue(result); } async call(to, data, from, height, simulatedTransaction, accessList) { const toStr = to.toString(); const fromStr = from ? from.toHex() : undefined; const fromLegacyStr = from ? from.tweakedToHex() : undefined; let dataStr = data instanceof Uint8Array ? toHex(data) : data; if (dataStr.startsWith('0x')) { dataStr = dataStr.slice(2); } const params = [toStr, dataStr, fromStr, fromLegacyStr]; if (height) { if (typeof height === 'object') { throw new Error('Height must be a number or bigint'); } params.push(height.toString()); } else { params.push(undefined); } if (simulatedTransaction) { params.push(this.parseSimulatedTransaction(simulatedTransaction)); } else { params.push(undefined); } if (accessList) { params.push(accessList); } else { params.push(undefined); } const payload = this.buildJsonRpcPayload(JSONRpcMethods.CALL, params); const rawCall = await this.callPayloadSingle(payload); const result = rawCall.result || rawCall; if (!rawCall.result) { return { error: result.error.message, }; } if ('error' in result) { return result; } if (result.revert) { let decodedError; try { decodedError = decodeRevertData(fromBase64(result.revert)); } catch { decodedError = result.revert; } return { error: decodedError, }; } return new CallResult(result, this); } async gasParameters() { if (!this.gasCache || Date.now() - this.lastFetchedGas > 10000) { this.lastFetchedGas = Date.now(); this.gasCache = await this._gasParameters(); } return this.gasCache; } async sendRawTransaction(tx, psbt) { if (!/^[0-9A-Fa-f]+$/.test(tx)) { throw new Error('sendRawTransaction: Invalid hex string'); } const payload = this.buildJsonRpcPayload(JSONRpcMethods.BROADCAST_TRANSACTION, [tx, psbt]); const rawTx = await this.callPayloadSingle(payload); return rawTx.result; } async sendRawTransactions(txs) { const payloads = txs.map((tx) => { return this.buildJsonRpcPayload(JSONRpcMethods.BROADCAST_TRANSACTION, [tx, false]); }); const rawTxs = await this.callMultiplePayloads(payloads); if ('error' in rawTxs) { throw new Error(`Error sending transactions: ${rawTxs.error}`); } return rawTxs.map((rawTx) => { return rawTx.result; }); } async sendRawTransactionPackage(txs, isPackage = true) { if (!txs.length) { throw new Error('sendRawTransactionPackage: txs array must not be empty'); } for (let i = 0; i < txs.length; i++) { if (!/^[0-9A-Fa-f]+$/.test(txs[i])) { throw new Error(`sendRawTransactionPackage: txs[${i}] is not a valid hex string`); } } const payload = this.buildJsonRpcPayload(JSONRpcMethods.BROADCAST_TRANSACTION_PACKAGE, [txs, isPackage]); const result = await this.callPayloadSingle(payload); return result.result; } async getBlockWitness(height = -1, limit, page) { const params = [height.toString()]; if (limit !== undefined && limit !== null) params.push(limit); if (page !== undefined && page !== null) params.push(page); const payload = this.buildJsonRpcPayload(JSONRpcMethods.BLOCK_WITNESS, params); const rawWitnesses = await this.callPayloadSingle(payload); if ('error' in rawWitnesses) { throw new Error(`Error fetching block witnesses: ${rawWitnesses.error?.message || 'Unknown error'}`); } const result = rawWitnesses.result; return parseBlockWitnesses(result); } async getReorg(fromBlock, toBlock) { const params = []; if (fromBlock !== undefined && fromBlock !== null) params.push(fromBlock.toString()); if (toBlock !== undefined && toBlock !== null) params.push(toBlock.toString()); const payload = this.buildJsonRpcPayload(JSONRpcMethods.REORG, params); const rawReorg = await this.callPayloadSingle(payload); const result = rawReorg.result; if (result.length > 0) { for (let i = 0; i < result.length; i++) { const res = result[i]; res.fromBlock = BigInt('0x' + res.fromBlock.toString()); res.toBlock = BigInt('0x' + res.toBlock.toString()); } } return result; } async callPayloadSingle(payload) { const rawData = await this._send(payload); if (!rawData.length) { throw new Error('No data returned'); } const data = rawData.shift(); if (!data) { throw new Error('Block not found'); } return data; } async callMultiplePayloads(payloads) { const rawData = (await this._send(payloads)); if ('error' in rawData) { throw new Error(`Error fetching block: ${rawData.error}`); } const data = rawData.shift(); if (!data) { throw new Error('Block not found'); } return data; } buildJsonRpcPayload(method, params) { return { method: method, params: params, id: this.nextId++, jsonrpc: '2.0', }; } async getPublicKeysInfoRaw(addresses) { const addressArray = Array.isArray(addresses) ? addresses : [addresses]; for (const addr of addressArray) { if (this.validateAddress(addr, this.network) === null) { throw new Error(`Invalid address: ${addr}`); } } const method = JSONRpcMethods.PUBLIC_KEY_INFO; const payload = this.buildJsonRpcPayload(method, [addressArray]); const data = await this.callPayloadSingle(payload); if (data.error) { const errorData = data.error; const errorMessage = typeof errorData === 'string' ? errorData : errorData.message; throw new Error(errorMessage); } return data.result; } async getPublicKeysInfo(addresses, isContract = false, logErrors = false) { const result = await this.getPublicKeysInfoRaw(addresses); const response = {}; for (const pubKey of Object.keys(result)) { const info = result[pubKey]; if ('error' in info) { if (logErrors) { console.error(`Error fetching public key info for ${pubKey}: ${info.error}`); } continue; } const addressContent = isContract ? (info.mldsaHashedPublicKey ?? info.tweakedPubkey) : info.mldsaHashedPublicKey; const legacyKey = isContract ? info.tweakedPubkey : (info.originalPubKey ?? info.tweakedPubkey); if (!addressContent) { throw new Error(`No valid address content found for ${pubKey}. Use getPublicKeysInfoRaw instead.`); } const address = Address.fromString(addressContent, legacyKey); if (info.mldsaPublicKey) { address.originalMDLSAPublicKey = fromHex(info.mldsaPublicKey); address.mldsaLevel = info.mldsaLevel; } response[pubKey] = address; } return response; } async getLatestEpoch(includeSubmissions) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.LATEST_EPOCH, []); const rawEpoch = await this.callPayloadSingle(payload); const result = rawEpoch.result; return new Epoch(result); } async getEpochByNumber(epochNumber, includeSubmissions = false) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_EPOCH_BY_NUMBER, [epochNumber.toString(), includeSubmissions]); const rawEpoch = await this.callPayloadSingle(payload); if ('error' in rawEpoch) { throw new Error(`Error fetching epoch: ${rawEpoch.error?.message || 'Unknown error'}`); } const result = rawEpoch.result; return includeSubmissions || result.submissions ? new EpochWithSubmissions(result) : new Epoch(result); } async getEpochByHash(epochHash, includeSubmissions = false) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_EPOCH_BY_HASH, [ epochHash, includeSubmissions, ]); const rawEpoch = await this.callPayloadSingle(payload); if ('error' in rawEpoch) { throw new Error(`Error fetching epoch: ${rawEpoch.error?.message || 'Unknown error'}`); } const result = rawEpoch.result; return includeSubmissions || result.submissions ? new EpochWithSubmissions(result) : new Epoch(result); } async getEpochTemplate() { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_EPOCH_TEMPLATE, []); const rawTemplate = await this.callPayloadSingle(payload); if ('error' in rawTemplate) { throw new Error(`Error fetching epoch template: ${rawTemplate.error?.message || 'Unknown error'}`); } const result = rawTemplate.result; return new EpochTemplate(result); } async submitEpoch(params) { const payload = this.buildJsonRpcPayload(JSONRpcMethods.SUBMIT_EPOCH, [ { epochNumber: params.epochNumber.toString(), checksumRoot: toHex(params.checksumRoot), salt: toHex(params.salt), mldsaPublicKey: toHex(params.mldsaPublicKey), signature: toHex(params.signature), graffiti: params.graffiti ? toHex(params.graffiti) : undefined, }, ]); const rawSubmission = await this.callPayloadSingle(payload); if ('error' in rawSubmission) { throw new Error(`Error submitting epoch: ${rawSubmission.error?.message || 'Unknown error'}`); } const result = rawSubmission.result; return new SubmittedEpoch(result); } async getMempoolInfo() { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_MEMPOOL_INFO, []); const rawResult = await this.callPayloadSingle(payload); if ('error' in rawResult) { throw new Error(`Error fetching mempool info: ${rawResult.error?.message || 'Unknown error'}`); } return rawResult.result; } async getPendingTransaction(hash) { if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) { throw new Error(`getPendingTransaction: expected a 64-character hex txid, got "${hash}"`); } const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_PENDING_TRANSACTION, [hash]); const rawResult = await this.callPayloadSingle(payload); if ('error' in rawResult) { const msg = rawResult.error?.message ?? 'Unknown error'; if (/not found/i.test(msg)) return null; throw new Error(`Error fetching pending transaction: ${msg}`); } const raw = rawResult.result; if (raw == null) return null; return MempoolTransactionParser.parseTransaction(raw); } async getLatestPendingTransactions(options) { if (options?.address !== undefined) { if (this.validateAddress(options.address, this.network) === null) { throw new Error(`getLatestPendingTransactions: invalid address "${options.address}"`); } } return this._fetchPendingTransactions(options?.address ?? null, null, options?.limit); } async getLatestPendingTransactionsByAddresses(options) { if (options.addresses.length === 0) { throw new Error('getLatestPendingTransactionsByAddresses: addresses array must not be empty'); } for (const addr of options.addresses) { if (this.validateAddress(addr, this.network) === null) { throw new Error(`getLatestPendingTransactionsByAddresses: invalid address "${addr}"`); } } return this._fetchPendingTransactions(null, options.addresses, options.limit); } async _fetchPendingTransactions(address, addresses, limit) { if (address !== null && addresses !== null) { throw new Error('_fetchPendingTransactions: address and addresses are mutually exclusive'); } if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) { throw new Error(`limit must be a positive integer, got ${limit}`); } const params = [address, addresses, limit ?? null]; const payload = this.buildJsonRpcPayload(JSONRpcMethods.GET_LATEST_PENDING_TRANSACTIONS, params); const rawResult = await this.callPayloadSingle(payload); if (rawResult.error != null) { throw new Error(`Error fetching latest pending transactions: ${rawResult.error.message}`); } const result = rawResult.result; if (result == null) { return []; } if (typeof result !== 'object' || Array.isArray(result)) { throw new Error('Error fetching latest pending transactions: unexpected response shape'); } if (result.transactions == null) { return []; } if (!Array.isArray(result.transactions)) { throw new Error('Error fetching latest pending transactions: expected transactions to be an array'); } return MempoolTransactionParser.parseTransactions(result.transactions); } async _gasParameters() { const payload = this.buildJsonRpcPayload(JSONRpcMethods.GAS, []); const rawCall = await this.callPayloadSingle(payload); if ('error' in rawCall) { throw new Error(`Error fetching gas parameters: ${rawCall.error}`); } const result = rawCall.result; return new BlockGasParameters(result); } parseSimulatedTransaction(transaction) { return { inputs: transaction.inputs.map((input) => { return { txId: toBase64(input.txId), outputIndex: input.outputIndex, scriptSig: toBase64(input.scriptSig), witnesses: input.witnesses.map((w) => toBase64(w)), coinbase: input.coinbase ? toBase64(input.coinbase) : undefined, flags: input.flags, }; }), outputs: transaction.outputs.map((output) => { return { index: output.index, to: output.to, value: output.value.toString(), scriptPubKey: output.scriptPubKey ? toBase64(output.scriptPubKey) : undefined, flags: output.flags || TransactionOutputFlags.hasTo, }; }), }; } bigintToBase64(bigint) { return toBase64(BufferHelper.pointerToUint8Array(bigint)); } }