UNPKG

opnet

Version:

The perfect library for building Bitcoin-based applications.

535 lines (534 loc) 20.5 kB
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); }