opnet
Version:
The perfect library for building Bitcoin-based applications.
531 lines (530 loc) • 22.2 kB
JavaScript
import { Address, AddressMap, AddressVerificator, BufferHelper, ChallengeSolution, } from '@btc-vision/transaction';
import '../serialize/BigInt.js';
import { decodeRevertData } from '../utils/RevertDecoder.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 { StoredValue } from '../storage/StoredValue.js';
import { TransactionReceipt } from '../transactions/metadata/TransactionReceipt.js';
import { TransactionParser } from '../transactions/TransactionParser.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.replace('0x', '')];
}
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.replace('0x', '');
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 Buffer.from(result.bytecode, 'base64');
}
}
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 = Buffer.isBuffer(data) ? this.bufferToHex(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(BufferHelper.bufferToUint8Array(Buffer.from(result.revert, 'base64')));
}
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 getBlockWitness(height = -1, trusted, limit, page) {
const params = [height.toString()];
if (trusted !== undefined && trusted !== null)
params.push(trusted);
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 = Buffer.from(info.mldsaPublicKey, 'hex');
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(),
targetHash: this.bufferToHex(params.targetHash),
salt: this.bufferToHex(params.salt),
mldsaPublicKey: this.bufferToHex(params.mldsaPublicKey),
signature: this.bufferToHex(params.signature),
graffiti: params.graffiti ? this.bufferToHex(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 _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: input.txId.toString('base64'),
outputIndex: input.outputIndex,
scriptSig: input.scriptSig.toString('base64'),
witnesses: input.witnesses.map((w) => w.toString('base64')),
coinbase: input.coinbase ? input.coinbase.toString('base64') : undefined,
flags: input.flags,
};
}),
outputs: transaction.outputs.map((output) => {
return {
index: output.index,
to: output.to,
value: output.value.toString(),
scriptPubKey: output.scriptPubKey?.toString('base64') || undefined,
flags: output.flags || TransactionOutputFlags.hasTo,
};
}),
};
}
bufferToHex(buffer) {
return buffer.toString('hex');
}
bigintToBase64(bigint) {
return Buffer.from(BufferHelper.pointerToUint8Array(bigint)).toString('base64');
}
}