diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f37c99 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +# Editor config +# http://EditorConfig.org + +# This EditorConfig overrides any parent EditorConfigs +root = true + +# Default rules applied to all file types +[*] + +# No trailing spaces, newline at EOF +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf + +# 2 space indentation +indent_style = space +indent_size = 2 + +# JavaScript-specific settings +[*.{js,ts}] +quote_type = double +continuation_indent_size = 2 +curly_bracket_next_line = false +indent_brace_style = BSD +spaces_around_operators = true +spaces_around_brackets = none diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b3ee998 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,29 @@ +module.exports = { + parser: "@typescript-eslint/parser", + parserOptions: { + project: "tsconfig.json", + tsconfigRootDir : __dirname, + sourceType: "module", + }, + plugins: [ + "@typescript-eslint/eslint-plugin" + ], + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + ], + root: true, + env: { + node: true, + jest: true, + }, + ignorePatterns: [ + ".eslintrc.js" + ], + rules: { + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + }, +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38bb787 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# compiled output +/dist +/node_modules +package-lock.json + +# Logs +logs +*.log +npm-debug.log* +pnpm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5302b4b --- /dev/null +++ b/.npmignore @@ -0,0 +1,69 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +#ide +.vscode +.idea + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# NPM +src +examples +gulpfile.js +.prettierrc +.travis.yml diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1521c8b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..3856d88 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/README.md b/README.md index c70c3d4..508d487 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,159 @@ -# nestjs-web3 -NestJS module for working with web3 applications (EVM) +# Nesthers + + +## Description + +The Nesthers Module is a convenient integration of the [ethers.js](https://github.com/ethers-io/ethers.js) library into NestJS applications. This module aims to simplify the interaction with Ethereum blockchain features such as smart contracts, wallets, events, and more, providing a seamless and developer-friendly experience within the NestJS framework. + + +## Installation + +Install the module via npm: + +```sh +npm install nesthers +``` + +Install the module via yarn: + +```sh +yarn add nesthers +``` + + +## Features +- **Connection Configuration:** Configure the Ethereum provider to connect to your preferred network (e.g., mainnet, testnet). +- **Wallet Management:** Manage Ethereum wallets effortlessly for secure transactions. +- **Smart Contract Interaction:** Easily interact with Ethereum smart contracts using the provided service methods. +- **Event Handling:** Streamline the handling of Ethereum events for real-time updates. +- **Block Handling:** Streamline the handling of Ethereum blocks for real-time updates. + + +## Usage +1. Module initialization +```ts +import { Module } from "@nestjs/common"; +import { EthersModule, JsonRpcConnection } from "nesthers"; + +@Module({ + imports: [ + EthersModule.forRoot({ + connection: { + name: "Connection", // is option + instace: new JsonRpcConnection({ + url: "", + }), + wallets: [], // is option + contracts: [], // is option + }, + }), + ], +}) +export class AppModule {} +``` +2. Inject Connection +```ts +import { Injectable } from "@nestjs/common"; +import { InjectConnection, JsonRpcConnection } from "nesthers"; + +@Injectable() +export class AppService { + constructor( + @InjectConnection("Conection") // name is option + private readonly connection: JsonRpcConnection, + ) {} +} +``` +3. WalletBuilder +```ts +import { WalletBuilder, Wallet } from "nesthers"; + +@WalletBuilder({ + privateKey: "0x0...", +}) +export class AliceWallet extends Wallet { + // you can add your own functionality here +} +``` +4. InjectWallet +```ts +import { Injectable } from "@nestjs/common"; +import { InjectWallet } from "nesthers"; +import { AliceWallet } from "./wallets/alice.wallet"; + +@Injectable() +export class AppService { + constructor( + @InjectWallet(AliceWallet.name) // name is require + private readonly alice: AliceWallet, + ) {} +} +``` +5. ContractBuilder +```ts +import { ContractBuilder, Contract } from "nesthers"; + +@ContractBuilder({ + address: "0x0...", + abi: [ + { + "constant": true, + "inputs": [], + "name": "name", + ... + }, + ... + ], +}) +export class TokenContract extends Contract { + // you can add your own functionality here +} +``` +6. InjectContract +```ts +import { Injectable } from "@nestjs/common"; +import { InjectContract } from "nesthers"; +import { TokenContract } from "./contracts/token.contract"; + +@Injectable() +export class AppService { + constructor( + @InjectContract(TokenContract.name) // name is require + private readonly token: TokenContract, + ) {} +} +``` +7. OnBlock +```ts +import { Injectable } from "@nestjs/common"; +import { OnBlock, Arg } from "nesthers"; + +@Injectable() +export class AppService { + @OnBlock({}) + newBlockHandler(@Arg("hash") hash: string) { + console.log(hash); + } +} +``` +8. OnEvent +```ts +import { Injectable } from "@nestjs/common"; +import { OnEvent, Arg } from "nesthers"; + +@Injectable() +export class AppService { + @OnEvent({ + address: "0x0...", + topics: [ /* args */ ], + }) + newEventHandler(@Arg("hash") hash: string) { + console.log(hash); + } +} +``` + + +## License + +nesthers is [MIT licensed](LICENSE). diff --git a/lib/common/block.ts b/lib/common/block.ts new file mode 100644 index 0000000..d8a6d34 --- /dev/null +++ b/lib/common/block.ts @@ -0,0 +1,8 @@ +import { ethers } from "ethers"; +import { BlockOptions } from "../intefaces/block-options.interface"; + +export class Block extends ethers.Block { + constructor(options: BlockOptions) { + super(options.block, options.provider); + } +} diff --git a/lib/common/connection.ts b/lib/common/connection.ts new file mode 100644 index 0000000..418bf01 --- /dev/null +++ b/lib/common/connection.ts @@ -0,0 +1,54 @@ +import { ethers } from "ethers"; +import { + AnkrConnectionOptions, + AlchemyConnectionOptions, + BrowserConnectionOptions, + PocketConnectionOptions, + InfuraConnectionOptions, + SocketConnectionOptions, + JsonRpcConnectionOptions, +} from "../intefaces/connection-options.interface"; + +export class AbstractConnection extends ethers.AbstractProvider {} + +export class AnkrConnection extends ethers.AnkrProvider { + constructor(options: AnkrConnectionOptions) { + super(options.network, options.apiKey); + } +} + +export class AlchemyConnection extends ethers.AlchemyProvider { + constructor(options: AlchemyConnectionOptions) { + super(options.network, options.apiKey); + } +} + +export class BrowserConnection extends ethers.BrowserProvider { + constructor(options: BrowserConnectionOptions) { + super(options.ethereum, options.network); + } +} + +export class PocketConnection extends ethers.PocketProvider { + constructor(options: PocketConnectionOptions) { + super(options.network, options.applicationId, options.applicationSecret); + } +} + +export class InfuraConnection extends ethers.InfuraProvider { + constructor(options: InfuraConnectionOptions) { + super(options.network, options.projectId, options.projectSecret); + } +} + +export class SocketConnection extends ethers.SocketProvider { + constructor(options: SocketConnectionOptions) { + super(options.network); + } +} + +export class JsonRpcConnection extends ethers.JsonRpcProvider { + constructor(options: JsonRpcConnectionOptions) { + super(options.url, options.network, options.options); + } +} diff --git a/lib/common/contract.ts b/lib/common/contract.ts new file mode 100644 index 0000000..3f7dfd2 --- /dev/null +++ b/lib/common/contract.ts @@ -0,0 +1,9 @@ +import { ethers } from "ethers"; +import { ContractOptions } from "../intefaces/contract-options.interface"; +import { AbstractConnection } from "./connection"; + +export class Contract extends ethers.Contract { + constructor(options: ContractOptions, connection?: AbstractConnection) { + super(options.address, options.abi, connection); + } +} diff --git a/lib/common/event.ts b/lib/common/event.ts new file mode 100644 index 0000000..c2953a4 --- /dev/null +++ b/lib/common/event.ts @@ -0,0 +1,8 @@ +import { ethers } from "ethers"; +import { EventOptions } from "../intefaces/event-options.interface"; + +export class Event extends ethers.EventLog { + constructor(options: EventOptions) { + super(options.log, options.iface, options.fragment); + } +} diff --git a/lib/common/listener-factory.ts b/lib/common/listener-factory.ts new file mode 100644 index 0000000..3609b68 --- /dev/null +++ b/lib/common/listener-factory.ts @@ -0,0 +1,17 @@ +import { ListnerType } from "../enums/listener-type.enum"; +import { IListener } from "../intefaces/ilistener.interface"; +import { BlockListener } from "../listeners/block.listener"; +import { EventListener } from "../listeners/event.listener"; + +export class ListnerFactory { + static createListener(type: ListnerType, options: any): IListener { + switch (type) { + case ListnerType.BLOCK: { + return new BlockListener(options); + } + case ListnerType.EVENT: { + return new EventListener(options); + } + } + } +} diff --git a/lib/common/listener.ts b/lib/common/listener.ts new file mode 100644 index 0000000..dc6c263 --- /dev/null +++ b/lib/common/listener.ts @@ -0,0 +1,25 @@ +import { IListener } from "../intefaces/ilistener.interface"; + +export class Listener implements IListener { + private _isListening: boolean = false; + + constructor(protected readonly options: T) {} + + isListening(): boolean { + return this._isListening; + } + + start() { + if (this._isListening) { + throw new Error(""); + } + this._isListening = true; + } + + stop() { + if (!this._isListening) { + throw new Error(""); + } + this._isListening = false; + } +} diff --git a/lib/common/wallet.ts b/lib/common/wallet.ts new file mode 100644 index 0000000..692f3e3 --- /dev/null +++ b/lib/common/wallet.ts @@ -0,0 +1,17 @@ +import { ethers } from "ethers"; +import { WalletOptions } from "../intefaces/wallet-options.interface"; +import { AbstractConnection } from "./connection"; + +export class Wallet extends ethers.Wallet { + constructor(options: WalletOptions, connection?: AbstractConnection) { + if (options.random) { + super(ethers.Wallet.createRandom().privateKey, connection); + } else if (options.privateKey !== "") { + super(options.privateKey, connection); + } else if (options.mnemonic && options.mnemonic.phrase.length > 1) { + super(ethers.Wallet.fromPhrase(options.mnemonic.phrase.join(" ")).privateKey, connection); + } else { + throw new Error(""); + } + } +} diff --git a/lib/decorators/arg.decorator.ts b/lib/decorators/arg.decorator.ts new file mode 100644 index 0000000..2b0179a --- /dev/null +++ b/lib/decorators/arg.decorator.ts @@ -0,0 +1,11 @@ +import { getMetadata, setMetadata } from "../utils/metadata.util"; +import { ArgMetadata } from "../intefaces/arg-metadata.interface"; +import { ETHERS_ARGS_KEY } from "../ethers.constants"; + +export function Arg(key?: keyof T) { + return function (target: any, propertyKey: string | symbol, parameterIndex: number) { + const args = getMetadata[]>(ETHERS_ARGS_KEY, target[propertyKey]) || []; + args.push({ index: parameterIndex, key }); + setMetadata(ETHERS_ARGS_KEY, args, target[propertyKey]); + }; +} diff --git a/lib/decorators/contract-builder.decorator.ts b/lib/decorators/contract-builder.decorator.ts new file mode 100644 index 0000000..1a90724 --- /dev/null +++ b/lib/decorators/contract-builder.decorator.ts @@ -0,0 +1,10 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; +import { ContractBuilderOptions } from "../intefaces/contract-builder-options.interface"; +import { Functional, getFunctionalParams } from "../utils/functional.util"; +import { ETHERS_CONTRACT_BUILDER_OPTIONS } from "../ethers.constants"; + +export function ContractBuilder(options: Functional) { + return applyDecorators( + SetMetadata(ETHERS_CONTRACT_BUILDER_OPTIONS, getFunctionalParams(options)), + ); +} diff --git a/lib/decorators/inject-connection.decorator.ts b/lib/decorators/inject-connection.decorator.ts new file mode 100644 index 0000000..44f38f7 --- /dev/null +++ b/lib/decorators/inject-connection.decorator.ts @@ -0,0 +1,6 @@ +import { Inject } from "@nestjs/common"; +import { getConnectionToken } from "../utils/token.util"; + +export function InjectConnection(name?: string) { + return Inject(getConnectionToken(name)); +} diff --git a/lib/decorators/inject-contract.decorator.ts b/lib/decorators/inject-contract.decorator.ts new file mode 100644 index 0000000..fb8779f --- /dev/null +++ b/lib/decorators/inject-contract.decorator.ts @@ -0,0 +1,6 @@ +import { Inject } from "@nestjs/common"; +import { getContractToken } from "../utils/token.util"; + +export function InjectContract(name: string) { + return Inject(getContractToken(name)); +} diff --git a/lib/decorators/inject-wallet.decorator.ts b/lib/decorators/inject-wallet.decorator.ts new file mode 100644 index 0000000..b4eb5e4 --- /dev/null +++ b/lib/decorators/inject-wallet.decorator.ts @@ -0,0 +1,6 @@ +import { Inject } from "@nestjs/common"; +import { getWalletToken } from "../utils/token.util"; + +export function InjectWallet(name: string) { + return Inject(getWalletToken(name)); +} diff --git a/lib/decorators/on-block.decorator.ts b/lib/decorators/on-block.decorator.ts new file mode 100644 index 0000000..3f4030d --- /dev/null +++ b/lib/decorators/on-block.decorator.ts @@ -0,0 +1,12 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; +import { ListnerType } from "../enums/listener-type.enum"; +import { OnBlockOptions } from "../intefaces/on-block-options.interface"; +import { Functional, getFunctionalParams } from "../utils/functional.util"; +import { ETHERS_LISTENER_TYPE, ETHERS_LISTENER_ON_BLOCK_OPTIONS } from "../ethers.constants"; + +export function OnBlock(options: Functional): MethodDecorator { + return applyDecorators( + SetMetadata(ETHERS_LISTENER_TYPE, ListnerType.BLOCK), + SetMetadata(ETHERS_LISTENER_ON_BLOCK_OPTIONS, getFunctionalParams(options)), + ); +} diff --git a/lib/decorators/on-event.decorator.ts b/lib/decorators/on-event.decorator.ts new file mode 100644 index 0000000..938fdc5 --- /dev/null +++ b/lib/decorators/on-event.decorator.ts @@ -0,0 +1,12 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; +import { ListnerType } from "../enums/listener-type.enum"; +import { OnEventOptions } from "../intefaces/on-event-options.interface"; +import { Functional, getFunctionalParams } from "../utils/functional.util"; +import { ETHERS_LISTENER_TYPE, ETHERS_LISTENER_ON_EVENT_OPTIONS } from "../ethers.constants"; + +export function OnEvent(options: Functional): MethodDecorator { + return applyDecorators( + SetMetadata(ETHERS_LISTENER_TYPE, ListnerType.EVENT), + SetMetadata(ETHERS_LISTENER_ON_EVENT_OPTIONS, getFunctionalParams(options)), + ); +} diff --git a/lib/decorators/wallet-builder.decorator.ts b/lib/decorators/wallet-builder.decorator.ts new file mode 100644 index 0000000..1ac60ba --- /dev/null +++ b/lib/decorators/wallet-builder.decorator.ts @@ -0,0 +1,8 @@ +import { applyDecorators, SetMetadata } from "@nestjs/common"; +import { WalletBuilderOptions } from "../intefaces/wallet-builder-options.interface"; +import { Functional, getFunctionalParams } from "../utils/functional.util"; +import { ETHERS_WALLET_BUILDER_OPTIONS } from "../ethers.constants"; + +export function WalletBuilder(options: Functional) { + return applyDecorators(SetMetadata(ETHERS_WALLET_BUILDER_OPTIONS, getFunctionalParams(options))); +} diff --git a/lib/enums/listener-type.enum.ts b/lib/enums/listener-type.enum.ts new file mode 100644 index 0000000..09c7785 --- /dev/null +++ b/lib/enums/listener-type.enum.ts @@ -0,0 +1,4 @@ +export enum ListnerType { + BLOCK, + EVENT, +} diff --git a/lib/ethers-metadata.accessor.ts b/lib/ethers-metadata.accessor.ts new file mode 100644 index 0000000..b081d70 --- /dev/null +++ b/lib/ethers-metadata.accessor.ts @@ -0,0 +1,44 @@ +import { Injectable } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { ListnerType } from "./enums/listener-type.enum"; +import { OnBlockMetadata } from "./intefaces/on-block-metadata.interface"; +import { OnEventMetadata } from "./intefaces/on-event-metadata.interface"; +import { + ETHERS_LISTENER_TYPE, + ETHERS_LISTENER_ON_BLOCK_OPTIONS, + ETHERS_LISTENER_ON_EVENT_OPTIONS, + ETHERS_ARGS_KEY, +} from "./ethers.constants"; + +@Injectable() +export class EthersMetadataAccessor { + constructor(private readonly reflector: Reflector) {} + + getListenerTypeMetadata(target: any): ListnerType | undefined { + return this.getMetadata(ETHERS_LISTENER_TYPE, target); + } + + getListenerOnBlockMetadata(target: any): OnBlockMetadata | undefined { + const options = this.getMetadata( + ETHERS_LISTENER_ON_BLOCK_OPTIONS, + target, + ); + const args = this.getMetadata(ETHERS_ARGS_KEY, target) || []; + return { options, args }; + } + + getListenerOnEventMetadata(target: any): OnEventMetadata | undefined { + const options = this.getMetadata( + ETHERS_LISTENER_ON_EVENT_OPTIONS, + target, + ); + const args = this.getMetadata(ETHERS_ARGS_KEY, target) || []; + return { options, args }; + } + + private getMetadata(key: string, target: any): T | undefined { + const isObject = typeof target === "object" ? target !== null : typeof target === "function"; + + return isObject ? this.reflector.get(key, target) : undefined; + } +} diff --git a/lib/ethers.constants.ts b/lib/ethers.constants.ts new file mode 100644 index 0000000..a6bfc7e --- /dev/null +++ b/lib/ethers.constants.ts @@ -0,0 +1,7 @@ +export const ETHERS_WALLET_BUILDER_OPTIONS = "ETHERS_WALLET_BUILDER_OPTIONS"; +export const ETHERS_CONTRACT_BUILDER_OPTIONS = "ETHERS_CONTRACT_BUILDER_OPTIONS"; +export const ETHERS_LISTENER_TYPE = "ETHERS_LISTENER_TYPE"; +export const ETHERS_LISTENER_ON_BLOCK_OPTIONS = "ETHERS_LISTENER_ON_BLOCK_OPTIONS"; +export const ETHERS_LISTENER_ON_EVENT_OPTIONS = "ETHERS_LISTENER_ON_EVENT_OPTIONS"; +export const ETHERS_ARGS_KEY = "ETHERS_ARGS_KEY"; +export const ETHERS_CONNECTION = "ETHERS_CONNECTION"; diff --git a/lib/ethers.explorer.ts b/lib/ethers.explorer.ts new file mode 100644 index 0000000..b145da0 --- /dev/null +++ b/lib/ethers.explorer.ts @@ -0,0 +1,107 @@ +import { Injectable, OnModuleInit, Logger } from "@nestjs/common"; +import { DiscoveryService, MetadataScanner } from "@nestjs/core"; +import { InstanceWrapper } from "@nestjs/core/injector/instance-wrapper"; +import { EthersOrchestrator } from "./ethers.orchestrator"; +import { EthersMetadataAccessor } from "./ethers-metadata.accessor"; +import { ListnerType } from "./enums/listener-type.enum"; + +@Injectable() +export class EthersExplorer implements OnModuleInit { + private readonly logger = new Logger(EthersExplorer.name); + + constructor( + private readonly discoveryService: DiscoveryService, + private readonly metadataScanner: MetadataScanner, + private readonly ethersOrchestrator: EthersOrchestrator, + private readonly metadataAccessor: EthersMetadataAccessor, + ) {} + + onModuleInit() { + this.explore(); + } + + explore() { + const instanceWrappers: InstanceWrapper[] = [ + ...this.discoveryService.getControllers(), + ...this.discoveryService.getProviders(), + ]; + + for (const wrapper of instanceWrappers) { + const { instance } = wrapper; + + if (!instance || !Object.getPrototypeOf(instance)) { + continue; + } + + const processMethod = (name: string) => + wrapper.isDependencyTreeStatic() + ? this.lookupListeners(instance, name) + : this.warnForNonStaticProviders(wrapper, instance, name); + + // Remove this after dropping support for NestJS v9.3.2 + if (!Reflect.has(this.metadataScanner, "getAllMethodNames")) { + this.metadataScanner.scanFromPrototype( + instance, + Object.getPrototypeOf(instance), + processMethod, + ); + continue; + } + + this.metadataScanner + .getAllMethodNames(Object.getPrototypeOf(instance)) + .forEach(processMethod); + } + } + + private lookupListeners(instance: Record, key: string) { + const methodRef = instance[key]; + const typeMetadata = this.metadataAccessor.getListenerTypeMetadata(methodRef); + const wrapFunc = this.wrapFunctionInTryCatchBlock(methodRef, instance); + + switch (typeMetadata) { + case ListnerType.BLOCK: { + const onBlockMetadata = this.metadataAccessor.getListenerOnBlockMetadata(methodRef); + return this.ethersOrchestrator.addOnBlock(wrapFunc, onBlockMetadata); + } + case ListnerType.EVENT: { + const onEventMetadata = this.metadataAccessor.getListenerOnEventMetadata(methodRef); + return this.ethersOrchestrator.addOnEvent(wrapFunc, onEventMetadata); + } + } + } + + private warnForNonStaticProviders( + wrapper: InstanceWrapper, + instance: Record, + key: string, + ) { + const methodRef = instance[key]; + const typeMetadata = this.metadataAccessor.getListenerTypeMetadata(methodRef); + + switch (typeMetadata) { + case ListnerType.BLOCK: { + this.logger.warn( + `Cannot register BlockListener "${wrapper.name}@${key}" because it is defined in a non static provider.`, + ); + break; + } + case ListnerType.EVENT: { + this.logger.warn( + `Cannot register EventListener "${wrapper.name}@${key}" because it is defined in a non static provider.`, + ); + break; + } + } + } + + private wrapFunctionInTryCatchBlock(methodRef: Function, instance: object): Function { + return async (...args: unknown[]) => { + try { + await methodRef.call(instance, ...args); + } catch (error) { + this.logger.error(error); + } + }; + } +} diff --git a/lib/ethers.module.ts b/lib/ethers.module.ts new file mode 100644 index 0000000..3d28074 --- /dev/null +++ b/lib/ethers.module.ts @@ -0,0 +1,77 @@ +import { Module, DynamicModule, Provider } from "@nestjs/common"; +import { DiscoveryModule } from "@nestjs/core"; +import { EthersModuleOptions } from "./intefaces/ethers-options.interface"; +import { EthersExplorer } from "./ethers.explorer"; +import { EthersOrchestrator } from "./ethers.orchestrator"; +import { EthersRegistry } from "./ethers.registry"; +import { EthersMetadataAccessor } from "./ethers-metadata.accessor"; +import { getConnectionToken } from "./utils/token.util"; +import { getConnectionProvider } from "./providers/connection.provider"; +import { getWalletProviders } from "./providers/wallet.provider"; +import { getContractProviders } from "./providers/contract.provider"; +import { AbstractConnection } from "./common/connection"; +import { ETHERS_CONNECTION } from "./ethers.constants"; + +@Module({ + imports: [DiscoveryModule], + providers: [EthersMetadataAccessor, EthersOrchestrator], +}) +export class EthersModule { + static forRoot(options: EthersModuleOptions): DynamicModule { + const connection = getConnectionProvider(options.connection); + const wallets = getWalletProviders( + options.wallets || [], + getConnectionToken(options.connection.name), + ); + const contracts = getContractProviders( + options.contracts || [], + getConnectionToken(options.connection.name), + ); + + const innerConnection: Provider = { + provide: ETHERS_CONNECTION, + useValue: options.connection.instance, + }; + + return { + global: true, + module: EthersModule, + providers: [ + connection, + ...wallets, + ...contracts, + innerConnection, + EthersExplorer, + EthersRegistry, + ], + exports: [connection, ...wallets, ...contracts, EthersRegistry], + }; + } + + static forFuture( + options: Omit & { connectionName?: string }, + ): DynamicModule { + const connection = { + provide: ETHERS_CONNECTION, + inject: [getConnectionToken(options.connectionName)], + useFactory: (connection?: AbstractConnection) => { + return connection; + }, + }; + + const wallets = getWalletProviders( + options.wallets || [], + getConnectionToken(options.connectionName), + ); + const contracts = getContractProviders( + options.contracts || [], + getConnectionToken(options.connectionName), + ); + + return { + module: EthersModule, + providers: [connection, ...wallets, ...contracts, EthersExplorer, EthersRegistry], + exports: [connection, ...wallets, ...contracts, EthersRegistry], + }; + } +} diff --git a/lib/ethers.orchestrator.ts b/lib/ethers.orchestrator.ts new file mode 100644 index 0000000..38c1177 --- /dev/null +++ b/lib/ethers.orchestrator.ts @@ -0,0 +1,86 @@ +import { Injectable, Inject, OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common"; +import { v4 as uuidv4 } from "uuid"; +import { EthersRegistry } from "./ethers.registry"; +import { AbstractConnection } from "./common/connection"; +import { OnBlockMetadata } from "./intefaces/on-block-metadata.interface"; +import { OnEventMetadata } from "./intefaces/on-event-metadata.interface"; +import { BlockListener } from "./listeners/block.listener"; +import { EventListener } from "./listeners/event.listener"; +import { wrapFuncFilterArgs } from "./utils/tools.util"; +import { ETHERS_CONNECTION } from "./ethers.constants"; + +type TargetHost = { target: any }; +type RefHost = { ref?: T }; + +type OnBlock = OnBlockMetadata & TargetHost & RefHost; +type OnEvent = OnEventMetadata & TargetHost & RefHost; + +@Injectable() +export class EthersOrchestrator implements OnApplicationBootstrap, OnApplicationShutdown { + private readonly onBlocks: Record = {}; + private readonly onEvents: Record = {}; + + constructor( + private readonly ethersRegistry: EthersRegistry, + @Inject(ETHERS_CONNECTION) private readonly connection: AbstractConnection, + ) {} + + onApplicationBootstrap() { + this.subscribeOnBlocks(); + this.subscribeOnEvents(); + } + + onApplicationShutdown() { + this.unsubscribeOnBlocks(); + this.unsubscribeOnEvents(); + } + + private subscribeOnBlocks() { + for (const key in this.onBlocks) { + const options = this.onBlocks[key]; + options.ref = new BlockListener({ + connection: this.connection, + callback: (data) => wrapFuncFilterArgs(data, options.target, options.args), + }); + this.ethersRegistry.addListener(key, options.ref); + } + } + + private subscribeOnEvents() { + for (const key in this.onEvents) { + const options = this.onEvents[key]; + options.ref = new EventListener({ + connection: this.connection, + filter: options.options, + callback: (data) => wrapFuncFilterArgs(data, options.target, options.args), + }); + this.ethersRegistry.addListener(key, options.ref); + } + } + + private unsubscribeOnBlocks() { + for (const key in this.onBlocks) { + this.ethersRegistry.deleteListener(key); + } + } + + private unsubscribeOnEvents() { + for (const key in this.onEvents) { + this.ethersRegistry.deleteListener(key); + } + } + + addOnBlock(methodRef: any, options: OnBlockMetadata, name: string = uuidv4()) { + this.onBlocks[name] = { + target: methodRef, + ...options, + }; + } + + addOnEvent(methodRef: any, options: OnEventMetadata, name: string = uuidv4()) { + this.onEvents[name] = { + target: methodRef, + ...options, + }; + } +} diff --git a/lib/ethers.registry.ts b/lib/ethers.registry.ts new file mode 100644 index 0000000..ef299d5 --- /dev/null +++ b/lib/ethers.registry.ts @@ -0,0 +1,45 @@ +import { Injectable } from "@nestjs/common"; +import { IListener } from "./intefaces/ilistener.interface"; + +@Injectable() +export class EthersRegistry { + private readonly listeners = new Map(); + + hasListener(name: string): boolean { + return this.listeners.has(name); + } + + getListenerNames(): string[] { + return [...this.listeners.keys()]; + } + + getListener(name: string): T { + const ref = this.listeners.get(name); + if (!ref) { + throw new Error("Listner with this name desn't exist"); + } + return ref as T; + } + + addListener(name: string, listener: T) { + const ref = this.listeners.get(name); + if (ref) { + throw new Error("Listner with this name already exists"); + } + if (!listener.isListening()) { + listener.start(); + } + this.listeners.set(name, listener); + } + + deleteListener(name: string) { + const ref = this.listeners.get(name); + if (!ref) { + throw new Error("Listner with this name desn't exist"); + } + if (ref.isListening()) { + ref.stop(); + } + this.listeners.delete(name); + } +} diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..d6fbbf1 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,45 @@ +export * as ethers from "ethers"; + +// Ethers +export * from "./ethers.module"; +export * from "./ethers.explorer"; +export * from "./ethers.orchestrator"; +export * from "./ethers.registry"; + +// Common +export * from "./common/connection"; +export * from "./common/wallet"; +export * from "./common/contract"; +export * from "./common/block"; +export * from "./common/event"; + +// Decorators +export * from "./decorators/inject-connection.decorator"; +export * from "./decorators/inject-wallet.decorator"; +export * from "./decorators/inject-contract.decorator"; +export * from "./decorators/wallet-builder.decorator"; +export * from "./decorators/contract-builder.decorator"; +export * from "./decorators/on-block.decorator"; +export * from "./decorators/on-event.decorator"; +export * from "./decorators/arg.decorator"; + +// Interfaces +export * from "./intefaces/ethers-options.interface"; +export * from "./intefaces/connection-options.interface"; +export * from "./intefaces/wallet-options.interface"; +export * from "./intefaces/contract-options.interface"; +export * from "./intefaces/block-options.interface"; +export * from "./intefaces/event-options.interface"; +export * from "./intefaces/wallet-builder-options.interface"; +export * from "./intefaces/wallet-builder-metadata.interface"; +export * from "./intefaces/contract-builder-options.interface"; +export * from "./intefaces/contract-builder-metadata.interface"; +export * from "./intefaces/on-block-options.interface"; +export * from "./intefaces/on-block-metadata.interface"; +export * from "./intefaces/on-event-options.interface"; +export * from "./intefaces/on-event-metadata.interface"; +export * from "./intefaces/arg-metadata.interface"; +export * from "./intefaces/ilistener.interface"; + +// Enums +export * from "./enums/listener-type.enum"; diff --git a/lib/intefaces/arg-metadata.interface.ts b/lib/intefaces/arg-metadata.interface.ts new file mode 100644 index 0000000..241166b --- /dev/null +++ b/lib/intefaces/arg-metadata.interface.ts @@ -0,0 +1,4 @@ +export interface ArgMetadata { + index: number; + key?: keyof T; +} diff --git a/lib/intefaces/block-options.interface.ts b/lib/intefaces/block-options.interface.ts new file mode 100644 index 0000000..010df7b --- /dev/null +++ b/lib/intefaces/block-options.interface.ts @@ -0,0 +1,6 @@ +import { ethers } from "ethers"; + +export interface BlockOptions { + block: ethers.BlockParams; + provider: ethers.Provider; +} diff --git a/lib/intefaces/connection-options.interface.ts b/lib/intefaces/connection-options.interface.ts new file mode 100644 index 0000000..589fc82 --- /dev/null +++ b/lib/intefaces/connection-options.interface.ts @@ -0,0 +1,44 @@ +import { ethers } from "ethers"; +import { AbstractConnection } from "../common/connection"; + +export interface ConnectionOptions { + name?: string; + instance: AbstractConnection; +} + +export interface AnkrConnectionOptions { + network?: ethers.Networkish; + apiKey?: null | string; +} + +export interface AlchemyConnectionOptions { + network?: ethers.Networkish; + apiKey?: null | string; +} + +export interface BrowserConnectionOptions { + ethereum: ethers.Eip1193Provider; + network?: ethers.Networkish; +} + +export interface PocketConnectionOptions { + network?: ethers.Networkish; + applicationId?: null | string; + applicationSecret?: null | string; +} + +export interface InfuraConnectionOptions { + network?: ethers.Networkish; + projectId?: null | string; + projectSecret?: null | string; +} + +export interface SocketConnectionOptions { + network?: ethers.Networkish; +} + +export interface JsonRpcConnectionOptions { + url?: string | ethers.FetchRequest; + network?: ethers.Networkish; + options?: ethers.JsonRpcApiProviderOptions; +} diff --git a/lib/intefaces/contract-builder-metadata.interface.ts b/lib/intefaces/contract-builder-metadata.interface.ts new file mode 100644 index 0000000..1f0797b --- /dev/null +++ b/lib/intefaces/contract-builder-metadata.interface.ts @@ -0,0 +1,3 @@ +import { ContractOptions } from "./contract-options.interface"; + +export interface ContractBuilderMetadata extends ContractOptions {} diff --git a/lib/intefaces/contract-builder-options.interface.ts b/lib/intefaces/contract-builder-options.interface.ts new file mode 100644 index 0000000..48bc838 --- /dev/null +++ b/lib/intefaces/contract-builder-options.interface.ts @@ -0,0 +1,3 @@ +import { ContractOptions } from "./contract-options.interface"; + +export interface ContractBuilderOptions extends ContractOptions {} diff --git a/lib/intefaces/contract-options.interface.ts b/lib/intefaces/contract-options.interface.ts new file mode 100644 index 0000000..1306b72 --- /dev/null +++ b/lib/intefaces/contract-options.interface.ts @@ -0,0 +1,6 @@ +import { ethers } from "ethers"; + +export interface ContractOptions { + address: string; + abi: ethers.Interface | ethers.InterfaceAbi; +} diff --git a/lib/intefaces/ethers-options.interface.ts b/lib/intefaces/ethers-options.interface.ts new file mode 100644 index 0000000..e68272d --- /dev/null +++ b/lib/intefaces/ethers-options.interface.ts @@ -0,0 +1,20 @@ +import { ModuleMetadata } from "@nestjs/common"; +import { ConnectionOptions } from "./connection-options.interface"; +import { Wallet } from "../common/wallet"; +import { Contract } from "../common/contract"; + +export interface EthersModuleOptions { + connection: ConnectionOptions; + wallets?: EthersModuleOptionsWallet[]; + contracts?: EthersModuleOptionsContract[]; +} + +export interface EthersModuleOptionsWallet { + name: string; + wallet: typeof Wallet; +} + +export interface EthersModuleOptionsContract { + name: string; + contract: typeof Contract; +} diff --git a/lib/intefaces/event-options.interface.ts b/lib/intefaces/event-options.interface.ts new file mode 100644 index 0000000..072f0f1 --- /dev/null +++ b/lib/intefaces/event-options.interface.ts @@ -0,0 +1,7 @@ +import { ethers } from "ethers"; + +export interface EventOptions { + log: ethers.Log; + iface: ethers.Interface; + fragment: ethers.EventFragment; +} diff --git a/lib/intefaces/ilistener.interface.ts b/lib/intefaces/ilistener.interface.ts new file mode 100644 index 0000000..6139289 --- /dev/null +++ b/lib/intefaces/ilistener.interface.ts @@ -0,0 +1,5 @@ +export interface IListener { + isListening(): boolean; + start(): void; + stop(): void; +} diff --git a/lib/intefaces/on-block-metadata.interface.ts b/lib/intefaces/on-block-metadata.interface.ts new file mode 100644 index 0000000..76a0ecd --- /dev/null +++ b/lib/intefaces/on-block-metadata.interface.ts @@ -0,0 +1,8 @@ +import { OnBlockOptions } from "./on-block-options.interface"; +import { ArgMetadata } from "./arg-metadata.interface"; +import { Block } from "../common/block"; + +export interface OnBlockMetadata { + options: OnBlockOptions; + args: ArgMetadata[]; +} diff --git a/lib/intefaces/on-block-options.interface.ts b/lib/intefaces/on-block-options.interface.ts new file mode 100644 index 0000000..fb45b84 --- /dev/null +++ b/lib/intefaces/on-block-options.interface.ts @@ -0,0 +1 @@ +export interface OnBlockOptions {} diff --git a/lib/intefaces/on-event-metadata.interface.ts b/lib/intefaces/on-event-metadata.interface.ts new file mode 100644 index 0000000..b855568 --- /dev/null +++ b/lib/intefaces/on-event-metadata.interface.ts @@ -0,0 +1,8 @@ +import { OnEventOptions } from "./on-event-options.interface"; +import { ArgMetadata } from "./arg-metadata.interface"; +import { Event } from "../common/event"; + +export interface OnEventMetadata { + options: OnEventOptions; + args: ArgMetadata[]; +} diff --git a/lib/intefaces/on-event-options.interface.ts b/lib/intefaces/on-event-options.interface.ts new file mode 100644 index 0000000..402d01c --- /dev/null +++ b/lib/intefaces/on-event-options.interface.ts @@ -0,0 +1,6 @@ +import { ethers } from "ethers"; + +export interface OnEventOptions { + address: ethers.AddressLike; + topics?: ethers.TopicFilter; +} diff --git a/lib/intefaces/wallet-builder-metadata.interface.ts b/lib/intefaces/wallet-builder-metadata.interface.ts new file mode 100644 index 0000000..495d3b7 --- /dev/null +++ b/lib/intefaces/wallet-builder-metadata.interface.ts @@ -0,0 +1,3 @@ +import { WalletBuilderOptions } from "./wallet-builder-options.interface"; + +export interface WalletBuilderMetadata extends WalletBuilderOptions {} diff --git a/lib/intefaces/wallet-builder-options.interface.ts b/lib/intefaces/wallet-builder-options.interface.ts new file mode 100644 index 0000000..627a032 --- /dev/null +++ b/lib/intefaces/wallet-builder-options.interface.ts @@ -0,0 +1,3 @@ +import { WalletOptions } from "./wallet-options.interface"; + +export interface WalletBuilderOptions extends WalletOptions {} diff --git a/lib/intefaces/wallet-options.interface.ts b/lib/intefaces/wallet-options.interface.ts new file mode 100644 index 0000000..1d7f3bd --- /dev/null +++ b/lib/intefaces/wallet-options.interface.ts @@ -0,0 +1,8 @@ +export interface WalletOptions { + random?: boolean; + privateKey?: string; + mnemonic?: { + phrase: string[]; + derivePath?: string; + }; +} diff --git a/lib/listeners/block.listener.ts b/lib/listeners/block.listener.ts new file mode 100644 index 0000000..8fe2f5f --- /dev/null +++ b/lib/listeners/block.listener.ts @@ -0,0 +1,20 @@ +import { ethers } from "ethers"; +import { Listener } from "../common/listener"; +import { AbstractConnection } from "../common/connection"; + +export interface BlockListenerOptions { + connection: AbstractConnection; + callback: ethers.Listener; +} + +export class BlockListener extends Listener { + start() { + super.start(); + this.options.connection.on("block", this.options.callback); + } + + stop() { + super.stop(); + this.options.connection.removeListener("block", this.options.callback); + } +} diff --git a/lib/listeners/event.listener.ts b/lib/listeners/event.listener.ts new file mode 100644 index 0000000..a0df773 --- /dev/null +++ b/lib/listeners/event.listener.ts @@ -0,0 +1,21 @@ +import { ethers } from "ethers"; +import { Listener } from "../common/listener"; +import { AbstractConnection } from "../common/connection"; + +export interface EventListenerOptions { + connection: AbstractConnection; + filter: ethers.EventFilter; + callback: ethers.Listener; +} + +export class EventListener extends Listener { + start() { + super.start(); + this.options.connection.on(this.options.filter, this.options.callback); + } + + stop() { + super.stop(); + this.options.connection.removeListener(this.options.filter, this.options.callback); + } +} diff --git a/lib/providers/connection.provider.ts b/lib/providers/connection.provider.ts new file mode 100644 index 0000000..199cad8 --- /dev/null +++ b/lib/providers/connection.provider.ts @@ -0,0 +1,10 @@ +import { Provider } from "@nestjs/common"; +import { ConnectionOptions } from "../intefaces/connection-options.interface"; +import { getConnectionToken } from "../utils/token.util"; + +export function getConnectionProvider(options: ConnectionOptions): Provider { + return { + provide: getConnectionToken(options.name), + useFactory: () => options.instance, + }; +} diff --git a/lib/providers/contract.provider.ts b/lib/providers/contract.provider.ts new file mode 100644 index 0000000..51e43f3 --- /dev/null +++ b/lib/providers/contract.provider.ts @@ -0,0 +1,32 @@ +import { Provider } from "@nestjs/common"; +import { AbstractConnection } from "../common/connection"; +import { getContractToken } from "../utils/token.util"; +import { getMetadata } from "../utils/metadata.util"; +import { EthersModuleOptionsContract } from "../intefaces/ethers-options.interface"; +import { ContractBuilderMetadata } from "../intefaces/contract-builder-metadata.interface"; +import { ETHERS_CONTRACT_BUILDER_OPTIONS } from "../ethers.constants"; + +export function getContractProvider( + contract: EthersModuleOptionsContract, + connection?: any, +): Provider { + const metadata = getMetadata( + ETHERS_CONTRACT_BUILDER_OPTIONS, + contract.contract, + ); + + return { + provide: getContractToken(contract.name), + inject: [connection], + useFactory: (connection?: AbstractConnection) => { + return new contract.contract(metadata, connection); + }, + }; +} + +export function getContractProviders( + contracts: EthersModuleOptionsContract[], + connection?: any, +): Provider[] { + return contracts.map(contract => getContractProvider(contract, connection)); +} diff --git a/lib/providers/wallet.provider.ts b/lib/providers/wallet.provider.ts new file mode 100644 index 0000000..18f0cd6 --- /dev/null +++ b/lib/providers/wallet.provider.ts @@ -0,0 +1,26 @@ +import { Provider } from "@nestjs/common"; +import { AbstractConnection } from "../common/connection"; +import { getWalletToken } from "../utils/token.util"; +import { getMetadata } from "../utils/metadata.util"; +import { EthersModuleOptionsWallet } from "../intefaces/ethers-options.interface"; +import { WalletBuilderOptions } from "../intefaces/wallet-builder-options.interface"; +import { ETHERS_WALLET_BUILDER_OPTIONS } from "../ethers.constants"; + +export function getWalletProvider(wallet: EthersModuleOptionsWallet, connection?: any): Provider { + const metadata = getMetadata(ETHERS_WALLET_BUILDER_OPTIONS, wallet.wallet); + + return { + provide: getWalletToken(wallet.name), + inject: [connection], + useFactory: (connection?: AbstractConnection) => { + return new wallet.wallet(metadata, connection); + }, + }; +} + +export function getWalletProviders( + wallets: EthersModuleOptionsWallet[], + connection?: any, +): Provider[] { + return wallets.map(wallet => getWalletProvider(wallet, connection)); +} diff --git a/lib/utils/functional.util.ts b/lib/utils/functional.util.ts new file mode 100644 index 0000000..583d02c --- /dev/null +++ b/lib/utils/functional.util.ts @@ -0,0 +1,20 @@ +export type Functional = { + [K in keyof T]: T[K] | (() => T[K]); +}; + +export type NonFunctional = { + [K in keyof T]: T[K] extends () => infer R ? R : T[K]; +}; + +export const getFunctionalParams = (params: Functional): NonFunctional => { + const newParams: any = {}; + for (const key in params) { + const value = params[key]; + if (typeof value === "function") { + newParams[key] = value(); + } else { + newParams[key] = value; + } + } + return newParams; +}; diff --git a/lib/utils/metadata.util.ts b/lib/utils/metadata.util.ts new file mode 100644 index 0000000..d15a0f4 --- /dev/null +++ b/lib/utils/metadata.util.ts @@ -0,0 +1,8 @@ +export function getMetadata(key: string, target: any): T { + const isObject = typeof target === "object" ? target !== null : typeof target === "function"; + return isObject ? Reflect.getMetadata(key, target) : undefined; +} + +export function setMetadata(key: string, value: any, target: any) { + Reflect.defineMetadata(key, value, target); +} diff --git a/lib/utils/token.util.ts b/lib/utils/token.util.ts new file mode 100644 index 0000000..0bd565e --- /dev/null +++ b/lib/utils/token.util.ts @@ -0,0 +1,11 @@ +export function getConnectionToken(name?: string): string { + return name && name.slice(-10) === "Connection" ? name : `${name ?? ""}Connection`; +} + +export function getWalletToken(name: string): string { + return name.slice(-6) === "Wallet" ? name : `${name}Wallet`; +} + +export function getContractToken(name: string): string { + return name.slice(-8) === "Contract" ? name : `${name}Contract`; +} diff --git a/lib/utils/tools.util.ts b/lib/utils/tools.util.ts new file mode 100644 index 0000000..5818f2f --- /dev/null +++ b/lib/utils/tools.util.ts @@ -0,0 +1,24 @@ +export interface FilterArg { + index: number; + key?: keyof T; +} + +export function wrapFuncFilterArgs(data: T, callback: Function, filterArgs: FilterArg[]) { + const params = []; + + const args = filterArgs.sort((a, b) => { + if (a.index < b.index) return -1; + if (a.index > b.index) return 1; + return 0; + }); + + for (const { key } of args) { + if (key) { + params.push(data[key]); + } else { + params.push(data); + } + } + + callback(...params); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ff3a73f --- /dev/null +++ b/package.json @@ -0,0 +1,73 @@ +{ + "name": "nesthers", + "version": "0.9.0", + "description": "Nesthers is a convenient integration of the ethers.js library for NestJS", + "author": "Nazar Khatsko ", + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "homepage": "https://github.com/nazarkhatsko/nesthers#readme", + "bugs": "https://github.com/nazarkhatsko/nesthers/issues", + "repository": { + "type": "git", + "url": "https://github.com/nazarkhatsko/nesthers" + }, + "keywords": [ + "nesthers", + "ethers", + "ethersjs", + "ethers.js", + "nest", + "nestjs", + "nest.js", + "web3", + "evm", + "solidity", + "ethereum", + "blockchain" + ], + "scripts": { + "build": "rimraf -rf dist && tsc -p tsconfig.json", + "format": "prettier \"**/**/*.ts\" --ignore-path ./.prettierignore --write", + "lint": "eslint \"**/**/*.ts\" --ignore-path ./.eslintignore --fix" + }, + "dependencies": { + "ethers": "^6.8.1", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/schematics": "^10.0.0", + "@nestjs/testing": "^10.0.0", + "@types/express": "^4.17.17", + "@types/jest": "^29.5.2", + "@types/node": "^20.3.1", + "@types/supertest": "^2.0.12", + "@types/uuid": "^9.0.7", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "eslint": "^8.42.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.5.0", + "prettier": "^3.0.0", + "reflect-metadata": "^0.1.13", + "rimraf": "^5.0.5", + "rxjs": "^7.8.1", + "source-map-support": "^0.5.21", + "supertest": "^6.3.3", + "ts-jest": "^29.1.0", + "ts-loader": "^9.4.3", + "ts-node": "^10.9.1", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.1.3" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5728463 --- /dev/null +++ b/renovate.json @@ -0,0 +1,10 @@ +{ + "semanticCommits": true, + "packageRules": [{ + "depTypeList": ["devDependencies"], + "automerge": true + }], + "extends": [ + "config:base" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cd909e3 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "strict": true, + "module": "commonjs", + "declaration": true, + "removeComments": false, + "noLib": false, + "esModuleInterop": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es2017", + "sourceMap": false, + "outDir": "./dist", + "rootDir": "./lib", + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + }, + "include": ["lib"], + "exclude": ["test", "**/*.spec.ts", "node_modules"] +}