import di from 'a-di'; import memd from 'memd'; import alot from 'alot'; import type { TLogsRangeProgress, Web3Client } from '@dequanto/clients/Web3Client'; import type { ITxWriterOptions, TxWriter } from '@dequanto/txs/TxWriter'; import type { IAccount, TAccount } from "@dequanto/models/TAccount"; import type { TAbiItem } from '@dequanto/types/TAbi'; import type { IBlockchainExplorer } from '@dequanto/explorer/IBlockchainExplorer'; import type { TAddress } from '@dequanto/models/TAddress'; import type { ITxBuilderOptions } from '@dequanto/txs/ITxBuilderOptions'; import { $contract } from '@dequanto/utils/$contract'; import { $class } from '@dequanto/utils/$class'; import { $abiParser } from '@dequanto/utils/$abiParser'; import { ContractReader, ContractReaderUtils } from './ContractReader'; import { ContractWriter } from './ContractWriter'; import { ContractStream } from './ContractStream'; import { TxTopicInMemoryProvider } from '@dequanto/txs/receipt/TxTopicInMemoryProvider'; import { BlocksTxIndexer, TBlockListener } from '@dequanto/indexer/BlocksTxIndexer'; import { SubjectStream } from '@dequanto/class/SubjectStream'; import { $logger } from '@dequanto/utils/$logger'; import { $address } from '@dequanto/utils/$address'; import { TEth } from '@dequanto/models/TEth'; import { $abiUtils } from '@dequanto/utils/$abiUtils'; import { RpcTypes } from '@dequanto/rpc/Rpc'; import { ContractStorageReaderBase } from './ContractStorageReaderBase'; import { ContractBaseUtils } from './utils/ContractBaseUtils'; import { FnSignedWrapper } from './wrappers/FnSignedWrapper'; import { ITxLogItem } from '@dequanto/txs/receipt/ITxLogItem'; import { $is } from '@dequanto/utils/$is'; import { FnRequestWrapper } from './wrappers/FnRequestWrapper'; import { WClient } from '@dequanto/clients/ClientPool'; import { Constructor } from '@dequanto/utils/types'; export abstract class ContractBase { private blockNumber?: number; private blockDate?: Date; //private from?: TAddress; /** 1.4 for medium*/ private gasPriorityFee?: number; protected builderConfig?: ITxBuilderOptions; protected writerConfig?: ITxWriterOptions; abstract abi?: TAbiItem[]; abstract Types?: TContractTypes; $meta?: { // Path to the compiled JSON artifact file (exists when the contract was generated from artifact JSON) artifact?: string // Path to the generated TS/JS class filename class?: string // Path to the SOL file (exists when the contract was generated from SOL) source?: string // Contract name name?: string } storage?: ContractStorageReaderBase constructor( public address: TAddress, public client: Web3Client, public explorer: IBlockchainExplorer ) { if ($is.Address(address) && address !== $address.ZERO && $contract.store.has(address) === false) { const Ctor = this.constructor as Constructor; const cloned = new Ctor(null, null, null); const meta = { address: address.toLowerCase() as TAddress, abi: cloned.abi, name: cloned?.$meta?.name ?? Ctor.name }; $contract.store.register(meta); } } public async $getStorageAt(position: number | bigint | TEth.Hex) { let reader = await this.getContractReader(); return reader.getStorageAt(this.address, position); } public $parseInputData(buffer: TEth.BufferLike, value?: string) { return $abiUtils.parseMethodCallData(this.abi, buffer); } public async $executeBatch(values: T): Promise< { -readonly [P in keyof T]: ContractReaderUtils.TIContractReadParamsInferred; } > { let reader = await this.getContractReader(); return reader.executeBatch(values); } async getPastLogs( event: TEventName | TEventName[] , options?: TEventLogOptions> ): Promise, TEventName>[]> { return await this.$getPastLogsParsed(event as string | string[], options) as any; } public $config(builderConfig?: ITxBuilderOptions, writerConfig?: ITxWriterOptions): this { let $contract = $class.curry(this, { builderConfig: { ...(this.builderConfig ?? {}), ...(builderConfig ?? {}) }, writerConfig: { ...(this.builderConfig ?? {}), ...(writerConfig ?? {}), }, }); return $contract; } public $address(this: T, address: TAddress): T { let Ctor = this.constructor as any; let x = new Ctor(address, this.client, this.explorer); return x; } @memd.deco.memoize({ perInstance: true }) public $call() { let abiArr = this.abi; let writer = this.getContractWriter(); let methods = this.abi.filter(abi => abi.type === 'function' && $abiUtils.isReadMethod(abi) === false); let fns = alot(methods).map(abiMethod => { return { name: abiMethod.name, async fn(sender: TAccount, ...args: any[]) { return ContractBaseUtils.$call( writer, abiMethod, abiArr, sender, ...args ); } } }).toDictionary(x => x.name, x => x.fn); let $contract = $class.curry(this, { ...fns }); return $contract as any; } @memd.deco.memoize({ perInstance: true }) public $data(params?: { estimateGas?: boolean getNonce?: boolean from?: TAddress }) { let $top = this; if (params?.from) { $top = $top.$config({ from: params.from }); } let writeMethods = this.abi.filter(abi => abi.type === 'function' && $abiUtils.isReadMethod(abi) === false); let writeFns = alot(writeMethods).map(method => { return { name: method.name, async fn(sender: TAccount, ...args: any[]) { let writer: TxWriter = await $top .$config({ send: 'manual', gasEstimation: false, nonce: params?.getNonce ? void 0 : 0, }) [method.name](sender, ...args); if (params?.getNonce) { await writer.builder.ensureNonce(); } if (params?.estimateGas) { await writer.builder.ensureGas(); } let data = writer.builder.data as any as TEth.Tx; if (!params?.estimateGas) { // remove default values delete data.gasPrice; delete data.maxPriorityFeePerGas; delete data.maxFeePerGas; delete data.gas; } return data; } } }).toDictionary(x => x.name, x => x.fn); let readMethods = this.abi.filter(abi => abi.type === 'function' && $abiUtils.isReadMethod(abi) === true); let readFns = alot(readMethods).map(method => { return { name: method.name, async fn(...args: any[]) { return { to: $top.address, data: $abiUtils.serializeMethodCallData(method, args) }; } } }).toDictionary(x => x.name, x => x.fn); let $contract = $class.curry(this, { ...writeFns, ...readFns }); return $contract as any; } @memd.deco.memoize({ perInstance: true }) public $gas() { let abiArr = this.abi; let writer = this.getContractWriter(); let methods = this.abi.filter(abi => abi.type === 'function' && $abiUtils.isReadMethod(abi) === false); let fns = alot(methods).map(abiMethod => { return { name: abiMethod.name, async fn(sender: IAccount, ...args: any[]) { return ContractBaseUtils.$gas( writer, abiMethod, abiArr, sender, ...args ); } } }).toDictionary(x => x.name, x => x.fn); let $contract = $class.curry(this, { ...fns }); return $contract as any; } public $req() { return FnRequestWrapper.create(this); } public $signed(builderConfig?: ITxBuilderOptions, writerConfig?: ITxWriterOptions) { let instance = this.$signedCreate(); if (builderConfig != null || writerConfig != null) { instance = instance.$config(builderConfig, writerConfig); } return instance; } @memd.deco.memoize({ perInstance: true }) private $signedCreate() { return FnSignedWrapper.create(this); } @memd.deco.memoize({ perInstance: true }) public $receipt(this: T): T { let $top = this; let methods = this.abi.filter(abi => abi.type === 'function' && $abiUtils.isReadMethod(abi) === false); let fns = alot(methods).map(abiMethod => { return { name: abiMethod.name, async fn(sender: IAccount, ...args: any[]) { let tx: TxWriter = await $top[abiMethod.name](sender, ...args); let receipt = await tx.wait(); return tx; } } }).toDictionary(x => x.name, x => x.fn); let $contract = $class.curry(this, { ...fns }); return $contract as any; } public forBlock(mix: number | bigint | undefined | Date): this { if (mix == null) { return this; } if (typeof mix === 'undefined' || typeof mix === 'number' || typeof mix === 'bigint') { return this.forBlockNumber(mix); } return this.forBlockAt(mix); } protected forBlockNumber(blockNumber: number | bigint | undefined): this { let $contract = $class.curry(this, { blockNumber: blockNumber, blockDate: null }) return $contract; } protected forBlockAt(date: Date | undefined): this { let $contract = $class.curry(this, { blockNumber: null, blockDate: date }) return $contract; } protected $read(abi: string | TAbiItem, ...params) { if (this.builderConfig?.send === 'manual') { let req = { address: this.address, abi, params: params, blockNumber: this.blockNumber ?? this.blockDate, options: { from: this.builderConfig?.from } }; return req; } let reader = this.getContractReader(); return reader.readAsync(this.address, abi, ...params); } public $onLog(event: string, cb?) { let stream = this.getContractStream(); let events = stream.on(event); if (cb) { events.subscribe(cb); } return events; } public $onTransaction(options?: BlockWalker.IBlockWalkerOptions): SubjectStream<{ tx: TEth.Tx, block: TEth.Block, calldata: { method, arguments: any[] } }> { options ??= {}; options.logProgress ??= false; type TSubject = { tx: TEth.Tx, block: TEth.Block, calldata: { method, arguments: any[] } }; let stream = new SubjectStream(); let indexer = BlockWalker.onBlock(this.client, options, async (client, block, { txs }) => { txs = txs.filter(x => $address.eq(x.to, this.address)); if (txs.length === 0) { return; } txs.forEach(tx => { try { let calldata = this.$parseInputData(tx.input); let method = options.filter?.method; if (method != null && method !== '*') { if (calldata.name !== method) { return; } } let args = options.filter?.arguments; if (args != null) { for (let i = 0; i < args.length; i++) { let val = args[i]; if (val != null && val != calldata.args[i]) { return; } } } stream.next({ block, tx, calldata: { method: calldata.name, arguments: calldata.args } }); } catch (error) { $logger.log(`Unexpected exception onTx parser: ${error.message}`); stream.error(error); } }) }); indexer.onStarted.pipe(stream.onConnected); return stream; } protected async $write(abi: string | TAbiItem, account: TAccount & { value?: number | string | bigint }, ...params): Promise { let writer = await this.getContractWriter(); return writer.writeAsync(account, abi, params, { abi: this.abi, builderConfig: this.builderConfig, writerConfig: this.writerConfig, }); } protected $getAbiItem(type: 'event' | 'function' | 'string', name: string, argsCount?: number) { return ContractBaseUtils.$getAbiItem(this.abi, type, name, argsCount); } protected $getAbiItemOverload(fnName: string, args: any[]): TAbiItem protected $getAbiItemOverload(abis: (string | TAbiItem)[], args: any[]): TAbiItem protected $getAbiItemOverload(mix: string | (string | TAbiItem)[], args: any[]): TAbiItem { let abis = typeof mix === 'string' ? this.abi.filter(x => x.type === 'function' && x.name === mix) : mix; let $abis = abis .map(methodAbi => { if (typeof methodAbi === 'string') { return $abiParser.parseMethod(methodAbi); } return methodAbi; }) .filter(x => (x.inputs?.length ?? 0) === args.length); if ($abis.length === 0) { throw new Error(`ABI not found in overloads \n${abis.join('\n')}\n by arguments count. Got ${args.length} arguments`); } if ($abis.length === 1) { return $abis[0]; } $abis = filterABIbyArguments($abis, args, 'shallow'); if ($abis.length === 1) { return $abis[0]; } $abis = filterABIbyArguments($abis, args, 'single'); if ($abis.length === 1) { return $abis[0]; } $abis = filterABIbyArguments($abis, args, 'both'); if ($abis.length === 1) { return $abis[0]; } throw new Error(`ABI not found. Got multiple overloads for the argument count ${args.length}. We should pick the ABI by parameters type.`) } protected $extractLogs(tx: TEth.TxReceipt, abiItem: TAbiItem) { let logs = $contract.extractLogsForAbi(tx, abiItem); return logs; } protected $extractLog(log: TEth.Log, mix: string | string[] | TAbiItem | TAbiItem[] | '*') { let abi: TAbiItem | TAbiItem[]; let mixArr = typeof mix === 'string' ? [mix] : ((mix as TAbiItem[] | string[]) ?? []); if (mixArr.length === 0 || (mixArr.length === 1 && mixArr[0] === '*')) { abi = this.abi; } else { abi = mixArr.map(x => typeof x === 'string' ? this.$getAbiItem('event', x) : x); } let parsed = $contract.parseLogWithAbi(log, abi); return parsed; } protected async $getPastLogs(filters: RpcTypes.Filter, options?: { streamed?: boolean blockRangeLimits?: WClient['blockRangeLimits'] onProgress?(info: TLogsRangeProgress) }) { return this.getContractReader().getLogs(filters, options); } public async $getPastLogsParsed(mix: string | TAbiItem | string[] | TAbiItem[], options?: { addresses?: TAddress[] fromBlock?: number | Date toBlock?: number | Date params?: { [key: string]: any } onProgress?(info: TLogsRangeProgress) /** if TRUE the data will be only forwarded via onProgress callback. * And the final array will be undefined. * This will handle big queries to hold huge arrays in memory * */ streamed?: boolean blockRangeLimits?: WClient['blockRangeLimits'] }) { let filters = await this.$getPastLogsFilters(mix, { ...options }); let logs = await this.$getPastLogs(filters, { streamed: options?.streamed, blockRangeLimits: options?.blockRangeLimits, onProgress: async (info) => { if (options?.onProgress == null) { return; } let paged = info.paged.map(log => this.$extractLog(log, mix)); await options.onProgress({ ...info, logs: paged, paged }); } }); return logs?.map(log => this.$extractLog(log, mix)) as any; } protected async $getPastLogsFilters(mix: string | TAbiItem | string[] | TAbiItem[], options: { addresses?: TAddress[] topic?: string fromBlock?: number | Date toBlock?: number | Date params?: { [key: string]: any } }): Promise { let abi: TAbiItem | '*' | TAbiItem[]; if (mix === '*') { abi = '*' } else if (typeof mix === 'string') { abi = this.$getAbiItem('event', mix); } else if (Array.isArray(mix) === false) { abi = mix; } else if (mix.length === 1 && typeof mix[0] === 'string' && mix[0] === '*') { abi = '*'; } else { abi = mix.map(x => { if (typeof x === 'string') { return this.$getAbiItem('event', x); } return x; }); } return this.getContractReader().getLogsFilter( abi, { ...(options ?? {}), address: options?.addresses ?? this.address } ); } private getContractReader() { let reader = this.getContractReaderInner(); if (this.blockDate != null) { reader.forBlockAt(this.blockDate); } if (this.blockNumber != null) { reader.forBlockNumber(this.blockNumber); } let from = this.builderConfig?.from; if (from != null) { reader.withAddress(from); } return reader; } @memd.deco.memoize({ perInstance: true }) private getContractReaderInner() { let reader = new ContractReader(this.client, { name: this.constructor.name }); return reader; } @memd.deco.memoize({ perInstance: true }) protected getContractWriter() { if (this.abi != null) { // Updates the singleton instance let logParser = di.resolve(TxTopicInMemoryProvider); logParser.register(this.abi); } let writer = new ContractWriter(this.address, this.client); return writer; } @memd.deco.memoize({ perInstance: true }) private getContractStream() { let stream = new ContractStream(this.address, this.abi, this.client); return stream; } } namespace BlockWalker { export interface IBlockWalkerOptions { name?: string, persistence?: boolean, logProgress?: boolean //mempool?: boolean, fromBlock?: number filter?: { method?: string, arguments?: any[] } } const indexers = {} as { [key: string]: BlocksTxIndexer }; export function onBlock(client: Web3Client, options: IBlockWalkerOptions, cb: TBlockListener) { let key = `${client.platform}_${options?.name ?? ''}_${options?.persistence ?? false}`; let current = indexers[key]; if (current) { current.onBlock(cb); return current; } let indexer = new BlocksTxIndexer(client.platform, { name: options.name, persistence: options.persistence, loadTransactions: true, client: client, logProgress: options.logProgress, }); indexers[key] = indexer; indexer.onBlock(cb); indexer.start(); return indexer; } } export type TContractTypes = { Events: TEventsBase } type TEventsBase = { [name: string]: { outputParams: Record, outputArgs: any[], } } export type TEventLogOptions = { addresses?: TAddress[] fromBlock?: number | Date toBlock?: number | Date params?: TParams streamed?: boolean onProgress?(info: TLogsRangeProgress) blockRangeLimits?: WClient['blockRangeLimits'] } type TEventParams = Partial; // Tuple check type detects how an argument satisfies the "tuple" // shallow: checks only that the argument is an object type // single: checks that all tuple components (keys) are present in the argument // both: checks that all tuple components are present in the argument AND all argument keys exist in the tuple components function filterABIbyArguments(abis: TAbiItem[], args: any[], tupleCheck: 'shallow' | 'single' | 'both') { return abis.filter(abi => { for (let i = 0; i < args.length; i++) { let item = abi.inputs[i]; let arg = args[i]; if (item.type === 'address' && $address.isValid(arg)) { continue; } if (item.type === 'bool' && typeof arg === 'boolean') { continue; } if (item.type === 'string' && typeof arg === 'string') { continue; } if (item.type === 'bytes32' && $is.HexBytes32(arg)) { continue; } if (item.type === 'bytes' && $is.HexBytes32(arg)) { continue; } if (/^u?int/.test(item.type)) { if (typeof arg === 'number' || typeof arg === 'bigint') { continue; } if (typeof arg === 'string' && /^\d+$/.test(arg)) { continue; } } if (/tuple/.test(item.type)) { arg ??= {}; if (tupleCheck === 'shallow') { if (typeof arg === 'object') { continue; } } if (tupleCheck === 'single' || tupleCheck === 'both') { // E.g. Solidity structs: // 1. struct FooV1 { uint256 a; uint256 b; } // 2. struct FooV2 { uint256 a; uint256 b; uint256 c; } // If we call the 2. overloaded function the single check will match both structs // therefore we need later to call with tupleCheck === 'both' to detect correct overload const components = item.components ?? []; const satisfies = components.every(x => x.name in arg) ?? false; if (!satisfies) { return false; } if (tupleCheck === 'single') { continue; } } if (tupleCheck === 'both') { const components = item.components ?? []; for (let key in arg) { const found = components.find(x => x.name === key); if (!found) { return false; } } continue; } } return false; } return true; }); }