opnet
Version:
The perfect library for building Bitcoin-based applications.
852 lines (742 loc) • 28.8 kB
text/typescript
import { Network } from '@btc-vision/bitcoin';
import {
ABICoder,
ABIDataTypes,
Address,
AddressMap,
AddressTypes,
AddressVerificator,
BinaryReader,
BinaryWriter,
NetEvent,
} from '@btc-vision/transaction';
import { BitcoinAbiTypes } from '../abi/BitcoinAbiTypes.js';
import { BitcoinInterface } from '../abi/BitcoinInterface.js';
import { BaseContractProperties } from '../abi/interfaces/BaseContractProperties.js';
import { BitcoinAbiValue } from '../abi/interfaces/BitcoinAbiValue.js';
import {
BitcoinInterfaceAbi,
EventBaseData,
FunctionBaseData,
} from '../abi/interfaces/BitcoinInterfaceAbi.js';
import { BlockGasParameters } from '../block/BlockGasParameters.js';
import { DecodedCallResult } from '../common/CommonTypes.js';
import { AbstractRpcProvider } from '../providers/AbstractRpcProvider.js';
import { ContractEvents } from '../transactions/interfaces/ITransactionReceipt.js';
import { CallResult } from './CallResult.js';
import { IAccessList } from './interfaces/IAccessList.js';
import { IContract } from './interfaces/IContract.js';
import { ParsedSimulatedTransaction } from './interfaces/SimulatedTransaction.js';
import { OPNetEvent } from './OPNetEvent.js';
import { AbiTypeToStr } from './TypeToStr.js';
const internal = Symbol.for('_btc_internal');
const bitcoinAbiCoder = new ABICoder();
export type ContractDecodedObjectResult = { [key: string]: DecodedCallResult };
export type DecodedOutput = { values: Array<DecodedCallResult>; obj: ContractDecodedObjectResult };
/**
* Represents the base contract class.
* @category Contracts
* @abstract
*/
export abstract class IBaseContract<T extends BaseContractProperties> implements IContract {
/**
* The address of the contract.
*/
public readonly address: string | Address;
/**
* The address of the contract.
*/
public readonly network: Network;
/**
* The interface of the contract.
*/
public readonly interface!: BitcoinInterface;
/**
* A generic provider for the contract.
*/
public readonly provider: AbstractRpcProvider;
/**
* Who is sending the transaction.
*/
public from?: Address;
/**
* The internal functions of the contract.
* @protected
*/
readonly [internal]: keyof T | undefined;
private events: Map<string, EventBaseData> = new Map();
private gasParameters:
| {
cachedAt: number;
params: Promise<BlockGasParameters>;
}
| undefined;
private readonly fetchGasParametersAfter: number = 1000 * 10;
private currentTxDetails: ParsedSimulatedTransaction | undefined;
private simulatedHeight: bigint | undefined = undefined;
private accessList: IAccessList | undefined;
private _rlAddress: Promise<Address> | undefined;
protected constructor(
address: string | Address,
abi: BitcoinInterface | BitcoinInterfaceAbi,
provider: AbstractRpcProvider,
network: Network,
from?: Address,
) {
if (typeof address === 'string') {
const type = AddressVerificator.detectAddressType(address, network);
if (type !== AddressTypes.P2OP && type !== AddressTypes.P2PK) {
throw new Error(
`Oops! The address provided is not a valid P2OP or P2PK address ${address}.`,
);
}
}
this.address = address;
this.provider = provider;
this.interface = BitcoinInterface.from(abi);
this.network = network;
this.from = from;
Object.defineProperty(this, internal, { value: {} });
this.defineInternalFunctions();
}
/**
* The P2OP address of the contract.
* @returns {string} The P2OP address of the contract.
*/
public get p2opOrTweaked(): string {
if (typeof this.address !== 'string') {
return this.address.p2op(this.network);
}
return this.address;
}
/**
* Gets the contract address as an Address object.
* @return {Promise<Address>} The contract address as an Address object.
*/
public get contractAddress(): Promise<Address> {
if (typeof this.address === 'string') {
if (!this._rlAddress) {
this._rlAddress = this.provider.getPublicKeyInfo(this.address);
}
return this._rlAddress;
}
return Promise.resolve(this.address);
}
/**
* Sets the sender of the transaction.
* @param {Address} sender The sender of the transaction.
*/
public setSender(sender: Address): void {
this.from = sender;
}
/**
* Decodes the events from the contract.
* @param {NetEvent[] | ContractEvents} events The events to decode.
* @returns {OPNetEvent[]} The decoded events.
*/
public decodeEvents(
events: NetEvent[] | ContractEvents,
): OPNetEvent<ContractDecodedObjectResult>[] {
const decodedEvents: OPNetEvent<ContractDecodedObjectResult>[] = [];
if (!Array.isArray(events)) {
const tempEvents = events;
events = tempEvents[this.p2opOrTweaked];
if (
!Array.isArray(events) &&
typeof this.address === 'string' &&
this.address.startsWith('0x')
) {
const addy = Address.fromString(this.address);
const p2op = addy.p2op(this.network);
events = tempEvents[p2op];
}
if (!Array.isArray(events)) {
return [];
}
}
for (const event of events) {
decodedEvents.push(this.decodeEvent(event));
}
return decodedEvents;
}
/**
* Decodes a single event.
* @param {NetEvent} event The event to decode.
* @returns {OPNetEvent} The decoded event.
*/
public decodeEvent(event: NetEvent): OPNetEvent {
const eventData = this.events.get(event.type);
if (!eventData || eventData.values.length === 0) {
return new OPNetEvent(event.type, event.data);
}
const binaryReader: BinaryReader = new BinaryReader(event.data);
const out: DecodedOutput = this.decodeOutput(eventData.values, binaryReader);
const decodedEvent = new OPNetEvent(event.type, event.data);
decodedEvent.setDecoded(out);
return decodedEvent;
}
/**
* Encodes the calldata for a function.
* @param {string} functionName The name of the function.
* @param {unknown[]} args The arguments for the function.
* @returns {Buffer} The encoded calldata.
*/
public encodeCalldata(functionName: string, args: unknown[]): Buffer {
for (const element of this.interface.abi) {
if (element.name === functionName) {
const data = this.encodeFunctionData(element as FunctionBaseData, args);
return Buffer.from(data.getBuffer());
}
}
throw new Error(`Function not found: ${functionName}`);
}
public async currentGasParameters(): Promise<BlockGasParameters> {
if (
this.gasParameters &&
this.gasParameters.cachedAt + this.fetchGasParametersAfter > Date.now()
) {
return this.gasParameters.params;
}
this.gasParameters = {
cachedAt: Date.now(),
params: this.provider.gasParameters(),
};
return await this.gasParameters.params;
}
public setTransactionDetails(tx: ParsedSimulatedTransaction): void {
for (let i = 0; i < tx.outputs.length; i++) {
const input = tx.outputs[i];
if (input.index === 0 || input.index === 1) {
throw new Error(`Outputs 0 and 1 are reserved for the contract internal use.`);
}
}
this.currentTxDetails = tx;
}
public setAccessList(accessList: IAccessList): void {
this.accessList = accessList;
}
public setSimulatedHeight(height: bigint | undefined): void {
this.simulatedHeight = height;
}
protected getFunction(
name: symbol | string,
):
| CallResult
| undefined
| string
| number
| symbol
| Address
| Network
| (() => Promise<BlockGasParameters>)
| ((functionName: string, args: unknown[]) => Buffer) {
const key = name as keyof Omit<
IBaseContract<T>,
| 'address'
| 'provider'
| 'interface'
| 'decodeEvents'
| 'decodeEvent'
| 'setSender'
| 'setSimulatedHeight'
| 'setTransactionDetails'
| 'setAccessList'
| 'contractAddress'
>;
return this[key];
}
/**
* Defines the internal functions of the contract. These functions are generated for the ABI provided.
* @private
*/
private defineInternalFunctions(): void {
for (const element of this.interface.abi) {
switch (element.type) {
case BitcoinAbiTypes.Function: {
// We will allow overwrites.
//if (this[element.name]) {
// throw new Error(`Duplicate function found in the ABI: ${element.name}.`);
//}
if (this.getFunction(element.name)) {
continue;
}
Object.defineProperty(this, element.name, {
value: this.callFunction(element).bind(this),
});
break;
}
case BitcoinAbiTypes.Event: {
if (this.events.has(element.name)) {
throw new Error(`Duplicate event found in the ABI: ${element.name}.`);
}
this.events.set(element.name, element);
break;
}
default:
throw new Error(`Unsupported type.`);
}
}
}
private getSelector(element: FunctionBaseData): string {
let name = element.name;
name += '(';
if (element.inputs && element.inputs.length) {
for (let i = 0; i < element.inputs.length; i++) {
const input = element.inputs[i];
const str = AbiTypeToStr[input.type];
if (!str) {
throw new Error(`Unsupported type: ${input.type}`);
}
if (i > 0) {
name += ',';
}
name += str;
}
}
name += ')';
return name;
}
private encodeFunctionData(element: FunctionBaseData, args: unknown[]): BinaryWriter {
const writer = new BinaryWriter();
const selectorStr = this.getSelector(element);
const selector = Number('0x' + bitcoinAbiCoder.encodeSelector(selectorStr));
writer.writeSelector(selector);
if (args.length !== (element.inputs?.length ?? 0)) {
throw new Error('Invalid number of arguments provided');
}
if (!element.inputs || (element.inputs && element.inputs.length === 0)) {
return writer;
}
for (let i = 0; i < element.inputs.length; i++) {
this.encodeInput(writer, element.inputs[i], args[i]);
}
return writer;
}
private encodeInput(writer: BinaryWriter, abi: BitcoinAbiValue, value: unknown): void {
const type = abi.type;
const name = abi.name;
switch (type) {
case ABIDataTypes.INT128: {
if (typeof value !== 'bigint') {
throw new Error(`Expected value to be of type bigint (${name})`);
}
writer.writeI128(value);
break;
}
case ABIDataTypes.UINT256: {
if (typeof value !== 'bigint') {
throw new Error(`Expected value to be of type bigint (${name})`);
}
writer.writeU256(value);
break;
}
case ABIDataTypes.BOOL: {
if (typeof value !== 'boolean') {
throw new Error(`Expected value to be of type boolean (${name})`);
}
writer.writeBoolean(value);
break;
}
case ABIDataTypes.STRING: {
if (typeof value !== 'string') {
throw new Error(`Expected value to be of type string (${name})`);
}
writer.writeStringWithLength(value);
break;
}
case ABIDataTypes.ADDRESS: {
if (!value) throw new Error(`Expected value to be of type Address (${name})`);
if (!('equals' in (value as Address))) {
throw new Error(
`Expected value to be of type Address (${name}) was ${typeof value}`,
);
}
writer.writeAddress(value as Address);
break;
}
case ABIDataTypes.UINT8: {
if (typeof value !== 'number') {
throw new Error(`Expected value to be of type number (${name})`);
}
writer.writeU8(value);
break;
}
case ABIDataTypes.UINT16: {
if (typeof value !== 'number') {
throw new Error(`Expected value to be of type number (${name})`);
}
writer.writeU16(value);
break;
}
case ABIDataTypes.UINT32: {
if (typeof value !== 'number') {
throw new Error(`Expected value to be of type number (${name})`);
}
writer.writeU32(value);
break;
}
case ABIDataTypes.BYTES32: {
if (!(value instanceof Uint8Array)) {
throw new Error(`Expected value to be of type Uint8Array (${name})`);
}
writer.writeBytes(value);
break;
}
case ABIDataTypes.BYTES4: {
if (!(value instanceof Uint8Array)) {
throw new Error(`Expected value to be of type Uint8Array (${name})`);
}
writer.writeBytes(value);
break;
}
case ABIDataTypes.ADDRESS_UINT256_TUPLE: {
writer.writeAddressValueTuple(value as AddressMap<bigint>);
break;
}
case ABIDataTypes.BYTES: {
if (!(value instanceof Uint8Array)) {
throw new Error(`Expected value to be of type Uint8Array (${name})`);
}
writer.writeBytesWithLength(value);
break;
}
case ABIDataTypes.UINT64: {
if (typeof value !== 'bigint') {
throw new Error(`Expected value to be of type bigint (${name})`);
}
writer.writeU64(value);
break;
}
case ABIDataTypes.ARRAY_OF_ADDRESSES: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeAddressArray(value as Address[]);
break;
}
case ABIDataTypes.ARRAY_OF_UINT256: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU256Array(value as bigint[]);
break;
}
case ABIDataTypes.ARRAY_OF_UINT32: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU32Array(value as number[]);
break;
}
case ABIDataTypes.ARRAY_OF_STRING: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeStringArray(value as string[]);
break;
}
case ABIDataTypes.ARRAY_OF_BYTES: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeBytesArray(value as Uint8Array[]);
break;
}
case ABIDataTypes.ARRAY_OF_UINT64: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU64Array(value as bigint[]);
break;
}
case ABIDataTypes.ARRAY_OF_UINT8: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU8Array(value as number[]);
break;
}
case ABIDataTypes.ARRAY_OF_UINT16: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU16Array(value as number[]);
break;
}
case ABIDataTypes.UINT128: {
if (typeof value !== 'bigint') {
throw new Error(`Expected value to be of type bigint (${name})`);
}
writer.writeU128(value);
break;
}
case ABIDataTypes.ARRAY_OF_UINT128: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU128Array(value as bigint[]);
break;
}
default: {
throw new Error(`Unsupported type: ${type} (${name})`);
}
}
}
private decodeOutput(abi: BitcoinAbiValue[], reader: BinaryReader): DecodedOutput {
const result: Array<DecodedCallResult> = [];
const obj: ContractDecodedObjectResult = {};
for (let i = 0; i < abi.length; i++) {
const type = abi[i].type;
const name = abi[i].name;
let decodedResult: DecodedCallResult;
switch (type) {
case ABIDataTypes.INT128:
decodedResult = reader.readI128();
break;
case ABIDataTypes.UINT256:
decodedResult = reader.readU256();
break;
case ABIDataTypes.BOOL:
decodedResult = reader.readBoolean();
break;
case ABIDataTypes.STRING:
decodedResult = reader.readStringWithLength();
break;
case ABIDataTypes.ADDRESS:
decodedResult = reader.readAddress();
break;
case ABIDataTypes.UINT8:
decodedResult = reader.readU8();
break;
case ABIDataTypes.UINT16:
decodedResult = reader.readU16();
break;
case ABIDataTypes.UINT32:
decodedResult = reader.readU32();
break;
case ABIDataTypes.BYTES32:
decodedResult = reader.readBytes(32);
break;
case ABIDataTypes.BYTES4:
decodedResult = reader.readBytes(4);
break;
case ABIDataTypes.ADDRESS_UINT256_TUPLE:
decodedResult = reader.readAddressValueTuple();
break;
case ABIDataTypes.BYTES: {
decodedResult = reader.readBytesWithLength();
break;
}
case ABIDataTypes.UINT64: {
decodedResult = reader.readU64();
break;
}
case ABIDataTypes.ARRAY_OF_ADDRESSES: {
decodedResult = reader.readAddressArray();
break;
}
case ABIDataTypes.ARRAY_OF_UINT256: {
decodedResult = reader.readU256Array();
break;
}
case ABIDataTypes.ARRAY_OF_UINT32: {
decodedResult = reader.readU32Array();
break;
}
case ABIDataTypes.ARRAY_OF_STRING: {
decodedResult = reader.readStringArray();
break;
}
case ABIDataTypes.ARRAY_OF_BYTES: {
decodedResult = reader.readBytesArray();
break;
}
case ABIDataTypes.ARRAY_OF_UINT64: {
decodedResult = reader.readU64Array();
break;
}
case ABIDataTypes.ARRAY_OF_UINT8: {
decodedResult = reader.readU8Array();
break;
}
case ABIDataTypes.ARRAY_OF_UINT16: {
decodedResult = reader.readU16Array();
break;
}
case ABIDataTypes.UINT128: {
decodedResult = reader.readU128();
break;
}
case ABIDataTypes.ARRAY_OF_UINT128: {
decodedResult = reader.readU128Array();
break;
}
default: {
throw new Error(`Unsupported type: ${type} (${name})`);
}
}
result.push(decodedResult);
obj[name] = decodedResult;
}
return {
values: result,
obj: obj,
};
}
private estimateGas(gas: bigint, gasParameters: BlockGasParameters): bigint {
const gasPerSat = gasParameters.gasPerSat;
const exactGas = (gas * gasPerSat) / 1000000000000n;
// Add 25% extra gas
const finalGas = (exactGas * 100n) / (100n - 30n);
return this.max(finalGas, 297n);
}
private max(a: bigint, b: bigint): bigint {
return a > b ? a : b;
}
private callFunction(element: FunctionBaseData): (...args: unknown[]) => Promise<CallResult> {
return async (...args: unknown[]): Promise<CallResult> => {
const address = await this.contractAddress;
const txDetails: ParsedSimulatedTransaction | undefined = this.currentTxDetails;
const accessList: IAccessList | undefined = this.accessList;
this.currentTxDetails = undefined;
this.accessList = undefined;
const data = this.encodeFunctionData(element, args);
const buffer = Buffer.from(data.getBuffer());
const response = await this.provider.call(
this.address,
buffer,
this.from,
this.simulatedHeight,
txDetails,
accessList,
);
if ('error' in response) {
throw new Error(`Error in calling function: ${response.error}`);
}
if (response.revert) {
throw new Error(`Execution Reverted: ${response.revert}`);
}
const decoded: DecodedOutput = element.outputs
? this.decodeOutput(element.outputs, response.result)
: { values: [], obj: {} };
response.setTo(this.p2opOrTweaked, address);
response.setDecoded(decoded);
response.setCalldata(buffer);
const gasParameters = await this.currentGasParameters();
const gas = this.estimateGas(response.estimatedGas || 0n, gasParameters);
const gasRefunded = this.estimateGas(response.refundedGas || 0n, gasParameters);
response.setBitcoinFee(gasParameters.bitcoin);
response.setGasEstimation(gas, gasRefunded);
response.setEvents(this.decodeEvents(response.rawEvents));
return response;
};
}
}
/**
* Represents the base contract class.
* @category Contracts
*/
export class BaseContract<T extends BaseContractProperties> extends IBaseContract<T> {
constructor(
address: string | Address,
abi: BitcoinInterface | BitcoinInterfaceAbi,
provider: AbstractRpcProvider,
network: Network,
sender?: Address,
) {
super(address, abi, provider, network, sender);
return this.proxify();
}
/**
* Proxifies the contract to allow for type checking.
* @private
*/
private proxify(): T & BaseContract<T> {
return new Proxy(this, {
get: (target: BaseContract<T>, prop: typeof internal, receiver) => {
if (typeof prop === 'symbol' || prop in target) {
return Reflect.get(target, prop, receiver);
}
// Undefined properties should return undefined
try {
return this.getFunction(prop);
} catch (error: unknown) {
if (!(error instanceof Error)) {
throw new Error(
`Something went wrong when trying to get the function: ${error}`,
);
} else {
throw error;
}
}
},
has: (target, prop) => {
if (typeof prop === 'symbol' || prop in target) {
return Reflect.has(target, prop);
}
return target.interface.hasFunction(prop);
},
}) as BaseContract<T> & T;
}
}
/**
* Creates a new contract class.
*/
function contractBase<T extends BaseContractProperties>(): new (
address: string | Address,
abi: BitcoinInterface | BitcoinInterfaceAbi,
provider: AbstractRpcProvider,
network: Network,
sender?: Address,
) => BaseContract<T> & Omit<T, keyof BaseContract<T>> {
return BaseContract as new (
address: string | Address,
abi: BitcoinInterface | BitcoinInterfaceAbi,
provider: AbstractRpcProvider,
network: Network,
sender?: Address,
) => BaseContract<T> & Omit<T, keyof BaseContract<T>>;
}
/**
* Creates a new contract instance.
* @param {string | Address} address The address of the contract.
* @param {BitcoinInterface | BitcoinInterfaceAbi} abi The ABI of the contract.
* @param {AbstractRpcProvider} provider The provider for the contract.
* @param {Address} [sender] Who is sending the transaction.
* @param {Network} [network] The network of the contract.
* @returns {BaseContract<T> & Omit<T, keyof BaseContract<T>>} The contract instance.
* @template T The properties of the contract.
* @category Contracts
*
* @example
* const contractAddress: string | Address = 'bcrt1p9p97ftxmx25ehgltlfn2j8wmgxnm0gwjlm6p2wveytxc5pzgtspqx93dy5';
* const senderAddress: Address = new Address([
* 40, 11, 228, 172, 219, 50, 169, 155, 163, 235, 250, 102, 169, 29, 219, 65, 167, 183, 161, 210,
* 254, 244, 21, 57, 153, 34, 205, 138, 4, 72, 92, 2,
* ]);
* const provider: JSONRpcProvider = new JSONRpcProvider('https://regtest.opnet.org');
* const contract: IOP_20Contract = getContract<IOP_20Contract>(
* contractAddress,
* OP_20_ABI,
* provider,
* networks.regtest,
* senderAddress,
* );
*
* const balanceExample = await contract.balanceOf(
* senderAddress
* );
*
* if ('error' in balanceExample) throw new Error('Error in fetching balance');
* console.log('Balance:', balanceExample.decoded);
*/
export function getContract<T extends BaseContractProperties>(
address: string | Address,
abi: BitcoinInterface | BitcoinInterfaceAbi,
provider: AbstractRpcProvider,
network: Network,
sender?: Address,
): BaseContract<T> & Omit<T, keyof BaseContract<T>> {
const base = contractBase<T>();
return new base(address, abi, provider, network, sender);
}