opnet
Version:
The perfect library for building Bitcoin-based applications.
535 lines (534 loc) • 20.5 kB
JavaScript
import { ABICoder, ABIDataTypes, Address, AddressTypes, AddressVerificator, BinaryReader, BinaryWriter, } from '@btc-vision/transaction';
import { BitcoinAbiTypes } from '../abi/BitcoinAbiTypes.js';
import { BitcoinInterface } from '../abi/BitcoinInterface.js';
import { OPNetEvent } from './OPNetEvent.js';
import { AbiTypeToStr } from './TypeToStr.js';
const internal = Symbol.for('_btc_internal');
const bitcoinAbiCoder = new ABICoder();
export class IBaseContract {
address;
network;
interface;
provider;
from;
[internal];
events = new Map();
gasParameters;
fetchGasParametersAfter = 1000 * 10;
currentTxDetails;
simulatedHeight = undefined;
accessList;
_rlAddress;
constructor(address, abi, provider, network, from) {
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();
}
get p2opOrTweaked() {
if (typeof this.address !== 'string') {
return this.address.p2op(this.network);
}
return this.address;
}
get contractAddress() {
if (typeof this.address === 'string') {
if (!this._rlAddress) {
this._rlAddress = this.provider.getPublicKeyInfo(this.address);
}
return this._rlAddress;
}
return Promise.resolve(this.address);
}
setSender(sender) {
this.from = sender;
}
decodeEvents(events) {
const decodedEvents = [];
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;
}
decodeEvent(event) {
const eventData = this.events.get(event.type);
if (!eventData || eventData.values.length === 0) {
return new OPNetEvent(event.type, event.data);
}
const binaryReader = new BinaryReader(event.data);
const out = this.decodeOutput(eventData.values, binaryReader);
const decodedEvent = new OPNetEvent(event.type, event.data);
decodedEvent.setDecoded(out);
return decodedEvent;
}
encodeCalldata(functionName, args) {
for (const element of this.interface.abi) {
if (element.name === functionName) {
const data = this.encodeFunctionData(element, args);
return Buffer.from(data.getBuffer());
}
}
throw new Error(`Function not found: ${functionName}`);
}
async currentGasParameters() {
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;
}
setTransactionDetails(tx) {
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;
}
setAccessList(accessList) {
this.accessList = accessList;
}
setSimulatedHeight(height) {
this.simulatedHeight = height;
}
getFunction(name) {
const key = name;
return this[key];
}
defineInternalFunctions() {
for (const element of this.interface.abi) {
switch (element.type) {
case BitcoinAbiTypes.Function: {
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.`);
}
}
}
getSelector(element) {
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;
}
encodeFunctionData(element, args) {
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;
}
encodeInput(writer, abi, value) {
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)) {
throw new Error(`Expected value to be of type Address (${name}) was ${typeof value}`);
}
writer.writeAddress(value);
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);
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);
break;
}
case ABIDataTypes.ARRAY_OF_UINT256: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU256Array(value);
break;
}
case ABIDataTypes.ARRAY_OF_UINT32: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU32Array(value);
break;
}
case ABIDataTypes.ARRAY_OF_STRING: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeStringArray(value);
break;
}
case ABIDataTypes.ARRAY_OF_BYTES: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeBytesArray(value);
break;
}
case ABIDataTypes.ARRAY_OF_UINT64: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU64Array(value);
break;
}
case ABIDataTypes.ARRAY_OF_UINT8: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU8Array(value);
break;
}
case ABIDataTypes.ARRAY_OF_UINT16: {
if (!(value instanceof Array)) {
throw new Error(`Expected value to be of type Array (${name})`);
}
writer.writeU16Array(value);
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);
break;
}
default: {
throw new Error(`Unsupported type: ${type} (${name})`);
}
}
}
decodeOutput(abi, reader) {
const result = [];
const obj = {};
for (let i = 0; i < abi.length; i++) {
const type = abi[i].type;
const name = abi[i].name;
let decodedResult;
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,
};
}
estimateGas(gas, gasParameters) {
const gasPerSat = gasParameters.gasPerSat;
const exactGas = (gas * gasPerSat) / 1000000000000n;
const finalGas = (exactGas * 100n) / (100n - 30n);
return this.max(finalGas, 297n);
}
max(a, b) {
return a > b ? a : b;
}
callFunction(element) {
return async (...args) => {
const address = await this.contractAddress;
const txDetails = this.currentTxDetails;
const accessList = 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 = 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;
};
}
}
export class BaseContract extends IBaseContract {
constructor(address, abi, provider, network, sender) {
super(address, abi, provider, network, sender);
return this.proxify();
}
proxify() {
return new Proxy(this, {
get: (target, prop, receiver) => {
if (typeof prop === 'symbol' || prop in target) {
return Reflect.get(target, prop, receiver);
}
try {
return this.getFunction(prop);
}
catch (error) {
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);
},
});
}
}
function contractBase() {
return BaseContract;
}
export function getContract(address, abi, provider, network, sender) {
const base = contractBase();
return new base(address, abi, provider, network, sender);
}