opnet
Version:
The perfect library for building Bitcoin-based applications.
1,156 lines (1,001 loc) • 47 kB
text/typescript
import { Network } from '@btc-vision/bitcoin';
import {
Address,
AddressMap,
AddressTypes,
AddressVerificator,
BufferHelper,
ChallengeSolution,
IP2WSHAddress,
MLDSASecurityLevel,
RawChallenge,
} from '@btc-vision/transaction';
import '../serialize/BigInt.js';
import { decodeRevertData } from '../utils/RevertDecoder.js';
import { Block } from '../block/Block.js';
import { BlockGasParameters, IBlockGasParametersInput } from '../block/BlockGasParameters.js';
import { parseBlockWitnesses } from '../block/BlockWitness.js';
import { IBlock } from '../block/interfaces/IBlock.js';
import { BlockWitnesses, RawBlockWitnessAPI } from '../block/interfaces/IBlockWitness.js';
import { BigNumberish, BlockTag } from '../common/CommonTypes.js';
import { CallResult } from '../contracts/CallResult.js';
import { ContractData } from '../contracts/ContractData.js';
import { TransactionOutputFlags } from '../contracts/enums/TransactionFlags.js';
import { IAccessList } from '../contracts/interfaces/IAccessList.js';
import { ICallRequestError, ICallResult } from '../contracts/interfaces/ICallResult.js';
import { IRawContract } from '../contracts/interfaces/IRawContract.js';
import {
ParsedSimulatedTransaction,
SimulatedTransaction,
} from '../contracts/interfaces/SimulatedTransaction.js';
import { Epoch } from '../epoch/Epoch.js';
import { EpochWithSubmissions } from '../epoch/EpochSubmission.js';
import { EpochTemplate } from '../epoch/EpochTemplate.js';
import { EpochSubmissionParams } from '../epoch/interfaces/EpochSubmissionParams.js';
import {
RawEpoch,
RawEpochTemplate,
RawEpochWithSubmissions,
RawSubmittedEpoch,
} from '../epoch/interfaces/IEpoch.js';
import { SubmittedEpoch } from '../epoch/SubmittedEpoch.js';
import { OPNetTransactionTypes } from '../interfaces/opnet/OPNetTransactionTypes.js';
import { IStorageValue } from '../storage/interfaces/IStorageValue.js';
import { StoredValue } from '../storage/StoredValue.js';
import { BroadcastedTransaction } from '../transactions/interfaces/BroadcastedTransaction.js';
import { ITransaction } from '../transactions/interfaces/ITransaction.js';
import { ITransactionReceipt } from '../transactions/interfaces/ITransactionReceipt.js';
import { TransactionReceipt } from '../transactions/metadata/TransactionReceipt.js';
import { TransactionBase } from '../transactions/Transaction.js';
import { TransactionParser } from '../transactions/TransactionParser.js';
import { UTXOsManager } from '../utxos/UTXOsManager.js';
import { JsonRpcPayload } from './interfaces/JSONRpc.js';
import { JSONRpcMethods } from './interfaces/JSONRpcMethods.js';
import {
JSONRpc2ResponseResult,
JsonRpcCallResult,
JsonRpcError,
JsonRpcResult,
JSONRpcResultError,
} from './interfaces/JSONRpcResult.js';
import { AddressesInfo, IPublicKeyInfoResult } from './interfaces/PublicKeyInfo.js';
import { ReorgInformation } from './interfaces/ReorgInformation.js';
interface ChallengeCache {
readonly challenge: ChallengeSolution;
readonly expireAt: number;
}
/**
* @description This class is used to provide an abstract RPC provider.
* @abstract
* @class AbstractRpcProvider
* @category Providers
*/
export abstract class AbstractRpcProvider {
private nextId: number = 0;
private chainId: bigint | undefined;
private gasCache: BlockGasParameters | undefined;
private lastFetchedGas: number = 0;
private challengeCache: ChallengeCache | undefined;
private csvCache: AddressMap<IP2WSHAddress> = new AddressMap<IP2WSHAddress>();
protected constructor(public readonly network: Network) {}
private _utxoManager: UTXOsManager = new UTXOsManager(this);
/**
* Get the UTXO manager.
*/
public get utxoManager(): UTXOsManager {
return this._utxoManager;
}
/**
* Get the CSV1 address for a given address.
* @description This method is used to get the CSV1 address for a given address.
* @param {Address} address The address to get the CSV1 address for
* @returns {IP2WSHAddress} The CSV1 address
* @example const csv1Address = provider.getCSV1ForAddress(Address.fromString('bcrt1q...'));
*/
public getCSV1ForAddress(address: Address): IP2WSHAddress {
const cached = this.csvCache.get(address);
if (cached) return cached;
const csv = address.toCSV(1, this.network);
this.csvCache.set(address, csv);
return csv;
}
/**
* Get the public key information.
* @description This method is used to get the public key information.
* @param {string | Address} addressRaw The address or addresses to get the public key information of
* @param isContract
* @returns {Promise<Address>} The public keys information
* @example await getPublicKeyInfo('bcrt1qfqsr3m7vjxheghcvw4ks0fryqxfq8qzjf8fxes');
* @throws {Error} If the address is invalid
*/
public async getPublicKeyInfo(
addressRaw: string | Address,
isContract: boolean,
): Promise<Address> {
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;
}
}
/**
* Verify an address.
* @param {string | Address} addr The address to verify
* @param {Network} network The network to verify the address against
* @returns {AddressTypes} The address type, return null if the address is invalid
*/
public validateAddress(addr: string | Address, network: Network): AddressTypes | null {
let validationResult: AddressTypes | null = 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;
}
/**
* Get the latest block number.
* @description This method is used to get the latest block number.
* @returns {Promise<number>} The latest block number
* @example await getBlockNumber();
*/
public async getBlockNumber(): Promise<bigint> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.BLOCK_BY_NUMBER,
[],
);
const rawBlockNumber: JsonRpcResult = await this.callPayloadSingle(payload);
const result: string = rawBlockNumber.result as string;
return BigInt(result);
}
/**
* Get block by checksum.
* @param {string} checksum The block checksum
* @param {boolean} prefetchTxs Whether to prefetch transactions
* @description This method is used to get a block by its checksum.
* @returns {Promise<Block>} The requested block
* @throws {Error} If the block is not found
* @example await getBlockByChecksum('0xabcdef123456...');
*/
public async getBlockByChecksum(
checksum: string,
prefetchTxs: boolean = false,
): Promise<Block> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_BLOCK_BY_CHECKSUM,
[checksum, prefetchTxs],
);
const block: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in block) {
throw new Error(
`Error fetching block by checksum: ${block.error?.message || 'Unknown error'}`,
);
}
const result: IBlock = block.result as IBlock;
return new Block(result, this.network);
}
/**
* Get the latest challenge to use in a transaction.
* @description This method is used to get the latest challenge along with epoch winner and verification data.
* @returns {Promise<ChallengeSolution>} The challenge and epoch data
* @example const challenge = await getChallenge();
* @throws {Error} If no challenge found or OPNet is not active
*/
public async getChallenge(): Promise<ChallengeSolution> {
// Check if we have a cached preimage that hasn't expired
if (this.challengeCache && Date.now() < this.challengeCache.expireAt) {
return this.challengeCache.challenge;
}
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.TRANSACTION_PREIMAGE,
[],
);
const rawChallenge: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawChallenge) {
throw new Error(
`Error fetching preimage: ${rawChallenge.error?.message || 'Unknown error'}`,
);
}
const result: RawChallenge = rawChallenge.result as RawChallenge;
if (!result || !result.solution) {
throw new Error(
'No challenge found. OPNet is probably not active yet on this blockchain.',
);
}
// Check if the solution is all zeros (invalid)
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;
}
/**
* Get block by number or hash.
* @param {BlockTag} blockNumberOrHash The block number or hash
* @param {boolean} prefetchTxs Whether to prefetch transactions
* @description This method is used to get a block by its number or hash.
* @returns {Promise<Block>} The requested block
* @throws {Error} If the block is not found
* @example await getBlock(123456);
*/
public async getBlock(
blockNumberOrHash: BlockTag,
prefetchTxs: boolean = false,
): Promise<Block> {
const method =
typeof blockNumberOrHash === 'string'
? JSONRpcMethods.GET_BLOCK_BY_HASH
: JSONRpcMethods.GET_BLOCK_BY_NUMBER;
const payload: JsonRpcPayload = this.buildJsonRpcPayload(method, [
blockNumberOrHash,
prefetchTxs,
]);
const block: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in block) {
throw new Error(`Error fetching block: ${block.error?.message || 'Unknown error'}`);
}
const result: IBlock = block.result as IBlock;
return new Block(result, this.network);
}
/**
* Get multiple blocks by number or hash.
* @param {BlockTag[]} blockNumbers The block numbers or hashes
* @param {boolean} prefetchTxs Whether to prefetch transactions
* @description This method is used to get multiple blocks by their numbers or hashes.
* @returns {Promise<Block[]>} The requested blocks
*/
public async getBlocks(
blockNumbers: BlockTag[],
prefetchTxs: boolean = false,
): Promise<Block[]> {
const payloads: JsonRpcPayload[] = blockNumbers.map((blockNumber) => {
return this.buildJsonRpcPayload(JSONRpcMethods.GET_BLOCK_BY_NUMBER, [
blockNumber,
prefetchTxs,
]);
});
const blocks: JsonRpcCallResult = await this.callMultiplePayloads(payloads);
if ('error' in blocks) {
const error = blocks.error as JSONRpcResultError<JSONRpcMethods.BLOCK_BY_NUMBER>;
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: IBlock = block.result as IBlock;
return new Block(result, this.network);
});
}
/**
* Get block by hash. This is the same method as getBlock.
* @param {string} blockHash The block hash
* @description This method is used to get a block by its hash. Note that this method is the same as getBlock.
* @returns {Promise<Block>} The requested block
* @throws {Error} If the block is not found
*/
public async getBlockByHash(blockHash: string): Promise<Block> {
return await this.getBlock(blockHash);
}
/**
* Get the bitcoin balance of an address.
* @param {string} address The address to get the balance of
* @param {boolean} filterOrdinals Whether to filter ordinals or not
* @description This method is used to get the balance of a bitcoin address.
* @returns {Promise<bigint>} The balance of the address
* @example await getBalance('bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq');
*/
public async getBalance(
address: string | Address,
filterOrdinals: boolean = true,
): Promise<bigint> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.GET_BALANCE, [
address,
filterOrdinals,
]);
const rawBalance: JsonRpcResult = await this.callPayloadSingle(payload);
const result: string = rawBalance.result as string;
if (!result || (result && !result.startsWith('0x'))) {
throw new Error(`Invalid balance returned from provider: ${result}`);
}
return BigInt(result);
}
/**
* Get the bitcoin balances of multiple addresses.
* @param {string[]} addressesLike The addresses to get the balances of
* @param {boolean} filterOrdinals Whether to filter ordinals or not
* @description This method is used to get the balance of a bitcoin address.
* @returns {Record<string, bigint>} The balance of the address
* @example await getBalances(['bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq']);
*/
public async getBalances(
addressesLike: string[],
filterOrdinals: boolean = true,
): Promise<Record<string, bigint>> {
const payloads: JsonRpcPayload[] = addressesLike.map((address: string) => {
return this.buildJsonRpcPayload(JSONRpcMethods.GET_BALANCE, [address, filterOrdinals]);
});
const balances: JsonRpcCallResult = await this.callMultiplePayloads(payloads);
if ('error' in balances) {
const error = balances.error as JSONRpcResultError<JSONRpcMethods.GET_BALANCE>;
throw new Error(`Error fetching block: ${error.message}`);
}
const resultBalance: Record<string, bigint> = {};
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 as string;
if (!result || (result && !result.startsWith('0x'))) {
throw new Error(`Invalid balance returned from provider: ${result}`);
}
resultBalance[address] = BigInt(result);
}
return resultBalance;
}
/**
* Get a transaction by its hash or hash id.
* @description This method is used to get a transaction by its hash or hash id.
* @param {string} txHash The transaction hash
* @returns {Promise<TransactionBase<OPNetTransactionTypes>>} The requested transaction
* @example await getTransaction('63e77ba9fa4262b3d4d0d9d97fa8a7359534606c3f3af096284662e3f619f374');
* @throws {Error} If something went wrong while fetching the transaction
*/
public async getTransaction(txHash: string): Promise<TransactionBase<OPNetTransactionTypes>> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_TRANSACTION_BY_HASH,
[txHash],
);
const rawTransaction: JsonRpcResult = await this.callPayloadSingle(payload);
const result: ITransaction = rawTransaction.result as ITransaction;
if ('error' in rawTransaction) {
throw new Error(
`Error fetching transaction: ${rawTransaction.error?.message || 'Unknown error'}`,
);
}
return TransactionParser.parseTransaction(result, this.network);
}
/**
* Get a transaction receipt by its hash.
* @description This method is used to get a transaction receipt by its hash.
* @param {string} txHash The transaction hash
* @returns {Promise<TransactionReceipt>} The requested transaction receipt
* @example await getTransactionReceipt('63e77ba9fa4262b3d4d0d9d97fa8a7359534606c3f3af096284662e3f619f374');
* @throws {Error} Something went wrong while fetching the transaction receipt
*/
public async getTransactionReceipt(txHash: string): Promise<TransactionReceipt> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_TRANSACTION_RECEIPT,
[txHash],
);
const rawTransaction: JsonRpcResult = await this.callPayloadSingle(payload);
return new TransactionReceipt(rawTransaction.result as ITransactionReceipt, this.network);
}
/**
* Get the current connected network type.
* @description This method is used to get the current connected network type.
* @returns {Network} The current connected network type
* @throws {Error} If the chain id is invalid
*/
public getNetwork(): Network {
return this.network;
}
/**
* Get the chain id.
* @description This method is used to get the chain id.
* @returns {Promise<bigint>} The chain id
* @throws {Error} If something went wrong while fetching the chain id
*/
public async getChainId(): Promise<bigint> {
if (this.chainId !== undefined) return this.chainId;
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.CHAIN_ID, []);
const rawChainId: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawChainId) {
throw new Error(`Something went wrong while fetching: ${rawChainId.error}`);
}
const chainId = rawChainId.result as string;
this.chainId = BigInt(chainId);
return this.chainId;
}
/**
* Get the contract code of an address.
* @description This method is used to get the contract code of an address.
* @param {string | Address} address The address of the contract
* @param {boolean} [onlyBytecode] Whether to return only the bytecode
* @returns {Promise<ContractData | Buffer>} The contract data or bytecode
* @example await getCode('tb1pth90usc4f528aqphpjrfkkdm4vy8hxnt5gps6aau2nva6pxeshtqqzlt3a');
* @throws {Error} If something went wrong while fetching the contract code
*/
public async getCode(
address: string | Address,
onlyBytecode: boolean = false,
): Promise<ContractData | Buffer> {
const addressStr: string = address.toString();
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.GET_CODE, [
addressStr,
onlyBytecode,
]);
const rawCode: JsonRpcResult = await this.callPayloadSingle(payload);
if (rawCode.error) {
throw new Error(
`${rawCode.error.code}: Something went wrong while fetching: ${rawCode.error.message}`,
);
}
const result: IRawContract | { bytecode: string } = rawCode.result as
| IRawContract
| { bytecode: string };
if ('contractAddress' in result) {
return new ContractData(result);
} else {
return Buffer.from(result.bytecode, 'base64');
}
}
/**
* Get the storage at a specific address and pointer.
* @description This method is used to get the storage at a specific address and pointer.
* @param {string | Address} address The address to get the storage from
* @param {BigNumberish} rawPointer The pointer to get the storage from as base64 or bigint
* @param {boolean} proofs Whether to send proofs or not
* @param {BigNumberish} [height] The height to get the storage from
* @returns {Promise<StoredValue>} The storage value
* @example await getStorageAt('tb1pth90usc4f528aqphpjrfkkdm4vy8hxnt5gps6aau2nva6pxeshtqqzlt3a', 'EXLK/QhEQMI5d9DrthLvozT+UcDQ7WuSPaz7g8GV3AQ=');
* @throws {Error} If something went wrong while fetching the storage
*/
public async getStorageAt(
address: string | Address,
rawPointer: bigint | string,
proofs: boolean = true,
height?: BigNumberish,
): Promise<StoredValue> {
const addressStr: string = address.toString();
const pointer: string =
typeof rawPointer === 'string' ? rawPointer : this.bigintToBase64(rawPointer);
const params: [string, string, boolean, string?] = [addressStr, pointer, proofs];
if (height) {
params.push(height.toString());
}
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_STORAGE_AT,
params,
);
const rawStorage: JsonRpcResult = await this.callPayloadSingle(payload);
const result: IStorageValue = rawStorage.result as IStorageValue;
return new StoredValue(result);
}
/**
* Call a contract function with a given calldata.
* @description This method is used to call a contract function with a given calldata.
* @param {string | Address} to The address of the contract
* @param {Buffer} data The calldata of the contract function
* @param {string | Address} [from] The address to call the contract from
* @param {BigNumberish} [height] The height to call the contract from
* @param {ParsedSimulatedTransaction} [simulatedTransaction] UTXOs to simulate the transaction
* @param {IAccessList} [accessList] The access list of previous simulation to use for this call
* @returns {Promise<CallResult>} The result of the contract function call
* @example await call('tb1pth90usc4f528aqphpjrfkkdm4vy8hxnt5gps6aau2nva6pxeshtqqzlt3a', Buffer.from('0x12345678'));
* @throws {Error} If something went wrong while calling the contract
*/
public async call(
to: string | Address,
data: Buffer | string,
from?: Address,
height?: BigNumberish,
simulatedTransaction?: ParsedSimulatedTransaction,
accessList?: IAccessList,
): Promise<CallResult | ICallRequestError> {
const toStr: string = to.toString();
const fromStr: string | undefined = from ? from.toHex() : undefined;
const fromLegacyStr: string | undefined = from ? from.tweakedToHex() : undefined;
let dataStr: string = Buffer.isBuffer(data) ? this.bufferToHex(data) : data;
if (dataStr.startsWith('0x')) {
dataStr = dataStr.slice(2);
}
const params: [
string,
string,
string?,
string?,
string?,
SimulatedTransaction?,
IAccessList?,
] = [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: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.CALL, params);
const rawCall: JsonRpcResult = await this.callPayloadSingle(payload);
const result: ICallResult = (rawCall.result as ICallResult) || rawCall;
if (!rawCall.result) {
return {
error: (result as unknown as { error: { message: string } }).error.message,
};
}
if ('error' in result) {
return result;
}
if (result.revert) {
let decodedError: string;
try {
decodedError = decodeRevertData(
BufferHelper.bufferToUint8Array(Buffer.from(result.revert, 'base64')),
);
} catch {
decodedError = result.revert;
}
return {
error: decodedError,
};
}
return new CallResult(result, this);
}
/**
* Get the next block gas parameters.
* @description This method is used to get the next block gas parameters. Such as base gas, gas limit, and gas price.
* @returns {Promise<BlockGasParameters>} The gas parameters of the next block
* @example await provider.gasParameters();
* @throws {Error} If something went wrong while calling the contract
*/
public async gasParameters(): Promise<BlockGasParameters> {
if (!this.gasCache || Date.now() - this.lastFetchedGas > 10000) {
this.lastFetchedGas = Date.now();
this.gasCache = await this._gasParameters();
}
return this.gasCache;
}
/**
* Send a raw transaction.
* @description This method is used to send a raw transaction.
* @param {string} tx The raw transaction to send as hex string
* @param {boolean} [psbt] Whether the transaction is a PSBT or not
* @returns {Promise<BroadcastedTransaction>} The result of the transaction
* @example await sendRawTransaction('02000000000101ad897689f66c98daae5fdc3606235c1ad7a17b9e0a6aaa0ea9e58ecc1198ad2a0100000000ffffffff01a154c39400000000160014482038efcc91af945f0c756d07a46401920380520247304402201c1f8718dec637ddb41b42abc44dcbf35a94c6be6a9de8c1db48c9fa6e456b7e022032a4b3286808372a7ac2c5094d6341b4d61b17663f4ccd1c1d92efa85c7dada80121020373626d317ae8788ce3280b491068610d840c23ecb64c14075bbb9f670af52c00000000', false);
* @throws {Error} If something went wrong while sending the transaction
*/
public async sendRawTransaction(tx: string, psbt: boolean): Promise<BroadcastedTransaction> {
// verify if tx is a valid hex string
if (!/^[0-9A-Fa-f]+$/.test(tx)) {
throw new Error('sendRawTransaction: Invalid hex string');
}
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.BROADCAST_TRANSACTION,
[tx, psbt],
);
const rawTx: JsonRpcResult = await this.callPayloadSingle(payload);
return rawTx.result as BroadcastedTransaction;
}
/**
* Bulk send transactions.
* @description This method is used to send multiple transactions at the same time.
* @param {string[]} txs The raw transactions to send as hex string
* @returns {Promise<BroadcastedTransaction[]>} The result of the transaction
* @throws {Error} If something went wrong while sending the transaction
*/
public async sendRawTransactions(txs: string[]): Promise<BroadcastedTransaction[]> {
const payloads: JsonRpcPayload[] = txs.map((tx) => {
return this.buildJsonRpcPayload(JSONRpcMethods.BROADCAST_TRANSACTION, [tx, false]);
});
const rawTxs: JsonRpcCallResult = await this.callMultiplePayloads(payloads);
if ('error' in rawTxs) {
throw new Error(`Error sending transactions: ${rawTxs.error}`);
}
return rawTxs.map((rawTx) => {
return rawTx.result as BroadcastedTransaction;
});
}
/**
* Get block witnesses.
* @description This method is used to get the witnesses of a block. This proves that the actions executed inside a block are valid and confirmed by the network. If the minimum number of witnesses are not met, the block is considered as potentially invalid.
* @param {BlockTag} height The block number or hash, use -1 for latest block
* @param {boolean} [trusted] Whether to trust the witnesses or not
* @param {number} [limit] The maximum number of witnesses to return
* @param {number} [page] The page number of the witnesses
* @returns {Promise<BlockWitnesses>} The witnesses of the block
* @example await getBlockWitness(123456n);
* @throws {Error} If something went wrong while fetching the witnesses
*/
public async getBlockWitness(
height: BigNumberish = -1,
trusted?: boolean,
limit?: number,
page?: number,
): Promise<BlockWitnesses> {
const params: [BigNumberish, boolean?, number?, number?] = [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: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.BLOCK_WITNESS,
params,
);
const rawWitnesses: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawWitnesses) {
throw new Error(
`Error fetching block witnesses: ${rawWitnesses.error?.message || 'Unknown error'}`,
);
}
const result = rawWitnesses.result as Array<{
blockNumber: string;
witnesses: RawBlockWitnessAPI[];
}>;
return parseBlockWitnesses(result);
}
/**
* Get reorgs that happened between two blocks.
* @description This method is used to get the reorgs that happened between two blocks.
* @param {BigNumberish} [fromBlock] The block number to start from
* @param {BigNumberish} [toBlock] The block number to end at
* @returns {Promise<ReorgInformation>} The reorg information
* @example await getReorg(123456n, 123457n);
* @throws {Error} If something went wrong while fetching the reorg information
*/
public async getReorg(
fromBlock?: BigNumberish,
toBlock?: BigNumberish,
): Promise<ReorgInformation[]> {
const params: [string?, string?] = [];
if (fromBlock !== undefined && fromBlock !== null) params.push(fromBlock.toString());
if (toBlock !== undefined && toBlock !== null) params.push(toBlock.toString());
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.REORG, params);
const rawReorg: JsonRpcResult = await this.callPayloadSingle(payload);
const result: ReorgInformation[] = rawReorg.result as ReorgInformation[];
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;
}
/**
* Requests to the OPNET node
* @param {JsonRpcPayload | JsonRpcPayload[]} payload The method to call
* @returns {Promise<JSONRpc2Result<T extends JSONRpcMethods>>} The result of the request
*/
public abstract _send(payload: JsonRpcPayload | JsonRpcPayload[]): Promise<JsonRpcCallResult>;
/**
* Send a single payload. This method is used to send a single payload.
* @param {JsonRpcPayload} payload The payload to send
* @returns {Promise<JsonRpcResult>} The result of the payload
* @throws {Error} If no data is returned
* @private
*/
public async callPayloadSingle(payload: JsonRpcPayload): Promise<JsonRpcResult> {
const rawData: JsonRpcCallResult = 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 as JSONRpc2ResponseResult<JSONRpcMethods>;
}
/**
* Send multiple payloads. This method is used to send multiple payloads.
* @param {JsonRpcPayload[]} payloads The payloads to send
* @returns {Promise<JsonRpcResult>} The result of the payloads
*/
public async callMultiplePayloads(payloads: JsonRpcPayload[]): Promise<JsonRpcCallResult> {
const rawData: JsonRpcCallResult[] = (await this._send(
payloads,
)) as unknown as JsonRpcCallResult[];
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;
}
/**
* Build a JSON RPC payload. This method is used to build a JSON RPC payload.
* @param {JSONRpcMethods} method The method to call
* @param {unknown[]} params The parameters to send
* @returns {JsonRpcPayload} The JSON RPC payload
*/
public buildJsonRpcPayload<T extends JSONRpcMethods>(
method: T,
params: unknown[],
): JsonRpcPayload {
return {
method: method,
params: params,
id: this.nextId++,
jsonrpc: '2.0',
};
}
/**
* Get the raw public key information from the API.
* @description Returns the raw API response without transforming to Address objects.
* @param {string | string[] | Address | Address[]} addresses The address or addresses to get the public key information of
* @returns {Promise<IPublicKeyInfoResult>} The raw public keys information from the API
* @example await getPublicKeysInfoRaw(['addressA', 'addressB']);
* @throws {Error} If the address is invalid or API call fails
*/
public async getPublicKeysInfoRaw(
addresses: string | string[] | Address | Address[],
): Promise<IPublicKeyInfoResult> {
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: JsonRpcPayload = this.buildJsonRpcPayload(method, [addressArray]);
const data: JsonRpcResult = await this.callPayloadSingle(payload);
if (data.error) {
const errorData = (data as JsonRpcError).error;
const errorMessage = typeof errorData === 'string' ? errorData : errorData.message;
throw new Error(errorMessage);
}
return data.result as IPublicKeyInfoResult;
}
/**
* Get the public key information and transform to Address objects.
* @description Fetches public key information from the API and converts results to Address instances
* containing both ML-DSA (quantum-resistant) and classical key data when available.
*
* The method resolves various address formats (p2tr, p2pkh, p2wpkh, p2op, raw public keys) to their
* corresponding public key information and constructs Address objects with the appropriate key hierarchy.
*
* For addresses with ML-DSA keys registered, the returned Address will have:
* - Primary content: SHA256 hash of the ML-DSA public key (mldsaHashedPublicKey)
* - Legacy key: The classical tweaked/original public key for Bitcoin address derivation
* - Original ML-DSA public key and security level attached to the Address instance
*
* For addresses without ML-DSA keys, the Address will use the tweaked x-only public key as primary content.
*
* @param {string | string[] | Address | Address[]} addresses The address(es) to look up. Accepts p2tr addresses,
* p2op addresses, raw public keys (32-byte x-only, 33-byte compressed, or 65-byte uncompressed), or existing Address instances.
* @param {boolean} [isContract=false] When true, uses tweakedPubkey as the legacy key since contracts
* don't have original untweaked keys. When false, prefers originalPubKey when available.
* @param {boolean} [logErrors=false] When true, logs errors to console for addresses that fail lookup
* instead of silently skipping them.
* @returns {Promise<AddressesInfo>} Map of input keys to Address instances. Keys that failed lookup are omitted.
* @example
* // Single address lookup
* const info = await provider.getPublicKeysInfo('bc1p...');
*
* @example
* // Multiple addresses with error logging
* const info = await provider.getPublicKeysInfo(['bc1p...', 'bc1q...'], false, true);
*
* @example
* // Contract address lookup
* const info = await provider.getPublicKeysInfo(contractAddress, true);
*
* @throws {Error} If any provided address fails validation before the API call
*/
public async getPublicKeysInfo(
addresses: string | string[] | Address | Address[],
isContract: boolean = false,
logErrors: boolean = false,
): Promise<AddressesInfo> {
const result = await this.getPublicKeysInfoRaw(addresses);
const response: AddressesInfo = {};
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 as MLDSASecurityLevel;
}
response[pubKey] = address;
}
return response;
}
/**
* Get the latest epoch.
* @description This method is used to get the latest epoch in the OPNet protocol.
* @returns {Promise<Epoch>} The latest epoch
* @example await getLatestEpoch();
* @throws {Error} If something went wrong while fetching the epoch
*/
public async getLatestEpoch(includeSubmissions: boolean): Promise<Epoch> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.LATEST_EPOCH, []);
const rawEpoch: JsonRpcResult = await this.callPayloadSingle(payload);
const result: RawEpoch = rawEpoch.result as RawEpoch;
return new Epoch(result);
}
/**
* Get an epoch by its number.
* @description This method is used to get an epoch by its number.
* @param {BigNumberish} epochNumber The epoch number (-1 for latest)
* @param {boolean} [includeSubmissions] Whether to include submissions in the response
* @returns {Promise<Epoch | EpochWithSubmissions>} The requested epoch
* @example await getEpochByNumber(123n);
* @throws {Error} If something went wrong while fetching the epoch
*/
public async getEpochByNumber(
epochNumber: BigNumberish,
includeSubmissions: boolean = false,
): Promise<Epoch | EpochWithSubmissions> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_EPOCH_BY_NUMBER,
[epochNumber.toString(), includeSubmissions],
);
const rawEpoch: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawEpoch) {
throw new Error(`Error fetching epoch: ${rawEpoch.error?.message || 'Unknown error'}`);
}
const result: RawEpochWithSubmissions = rawEpoch.result as RawEpochWithSubmissions;
return includeSubmissions || result.submissions
? new EpochWithSubmissions(result)
: new Epoch(result);
}
/**
* Get an epoch by its hash.
* @description This method is used to get an epoch by its hash.
* @param {string} epochHash The epoch hash
* @param {boolean} [includeSubmissions] Whether to include submissions in the response
* @returns {Promise<Epoch | EpochWithSubmissions>} The requested epoch
* @example await getEpochByHash('0x1234567890abcdef...');
* @throws {Error} If something went wrong while fetching the epoch
*/
public async getEpochByHash(
epochHash: string,
includeSubmissions: boolean = false,
): Promise<Epoch | EpochWithSubmissions> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.GET_EPOCH_BY_HASH, [
epochHash,
includeSubmissions,
]);
const rawEpoch: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawEpoch) {
throw new Error(`Error fetching epoch: ${rawEpoch.error?.message || 'Unknown error'}`);
}
const result: RawEpochWithSubmissions = rawEpoch.result as RawEpochWithSubmissions;
return includeSubmissions || result.submissions
? new EpochWithSubmissions(result)
: new Epoch(result);
}
/**
* Get the current epoch mining template.
* @description This method is used to get the current epoch mining template with target hash and requirements.
* @returns {Promise<EpochTemplate>} The epoch template
* @example await getEpochTemplate();
* @throws {Error} If something went wrong while fetching the template
*/
public async getEpochTemplate(): Promise<EpochTemplate> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(
JSONRpcMethods.GET_EPOCH_TEMPLATE,
[],
);
const rawTemplate: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawTemplate) {
throw new Error(
`Error fetching epoch template: ${rawTemplate.error?.message || 'Unknown error'}`,
);
}
const result: RawEpochTemplate = rawTemplate.result as RawEpochTemplate;
return new EpochTemplate(result);
}
/**
* Submit a new epoch solution.
* @description This method is used to submit a SHA-1 collision solution for epoch mining.
* @param {EpochSubmissionParams} params The parameters for the epoch submission
* @returns {Promise<SubmittedEpoch>} The submission result
* @example await submitEpoch({
* epochNumber: 123n,
* targetHash: Buffer.from('00000000000000000000000000000000', 'hex'),
* salt: Buffer.from('0a0a0a0a0a0a00a', 'hex'),
* mldsaPublicKey: Address.dead(),
* graffiti: Buffer.from('Hello, world!'),
* signature: Buffer.from('1234567890abcdef', 'hex'),
* });
* @throws {Error} If something went wrong while submitting the epoch
*/
public async submitEpoch(params: EpochSubmissionParams): Promise<SubmittedEpoch> {
const payload: JsonRpcPayload = 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: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawSubmission) {
throw new Error(
`Error submitting epoch: ${rawSubmission.error?.message || 'Unknown error'}`,
);
}
const result: RawSubmittedEpoch = rawSubmission.result as RawSubmittedEpoch;
return new SubmittedEpoch(result);
}
protected abstract providerUrl(url: string): string;
private async _gasParameters(): Promise<BlockGasParameters> {
const payload: JsonRpcPayload = this.buildJsonRpcPayload(JSONRpcMethods.GAS, []);
const rawCall: JsonRpcResult = await this.callPayloadSingle(payload);
if ('error' in rawCall) {
throw new Error(`Error fetching gas parameters: ${rawCall.error}`);
}
const result: IBlockGasParametersInput = rawCall.result as IBlockGasParametersInput;
return new BlockGasParameters(result);
}
private parseSimulatedTransaction(
transaction: ParsedSimulatedTransaction,
): SimulatedTransaction {
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,
};
}),
};
}
private bufferToHex(buffer: Buffer): string {
return buffer.toString('hex');
}
private bigintToBase64(bigint: bigint): string {
return Buffer.from(BufferHelper.pointerToUint8Array(bigint)).toString('base64');
}
}