Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new automation package #726

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"@nx/web": "17.3.0",
"@solana/web3.js": "^1.95.3",
"@types/depd": "^1.1.36",
"@types/events": "^3.0.3",
"@types/jest": "27.4.1",
"@types/node": "18.19.18",
"@types/secp256k1": "^4.0.6",
Expand Down
10 changes: 10 additions & 0 deletions packages/automation/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"presets": [
[
"@nx/web/babel",
{
"useBuiltIns": "usage"
}
]
]
}
18 changes: 18 additions & 0 deletions packages/automation/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
3 changes: 3 additions & 0 deletions packages/automation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Quick Start

This submodule is used to automate different actions using the Lit Protocol network or other useful events providing listening and responding abilities based on state machines.
16 changes: 16 additions & 0 deletions packages/automation/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable */
export default {
displayName: 'types',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
transform: {
'^.+\\.[t]s$': 'ts-jest',
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/packages/automation',
setupFilesAfterEnv: ['../../jest.setup.js'],
};
32 changes: 32 additions & 0 deletions packages/automation/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@lit-protocol/automation",
"type": "commonjs",
"license": "MIT",
"homepage": "https://github.com/Lit-Protocol/js-sdk",
"repository": {
"type": "git",
"url": "https://github.com/LIT-Protocol/js-sdk"
},
"keywords": [
"library"
],
"bugs": {
"url": "https://github.com/LIT-Protocol/js-sdk/issues"
},
"publishConfig": {
"access": "public",
"directory": "../../dist/packages/automation"
},
"tags": [
"universal"
],
"buildOptions": {
"genReact": false
},
"scripts": {
"generate-lit-actions": "yarn node ./esbuild.config.js"
},
"version": "7.0.0",
"main": "./dist/src/index.js",
"typings": "./dist/src/index.d.ts"
}
37 changes: 37 additions & 0 deletions packages/automation/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "automation",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/automation/src",
"projectType": "library",
"targets": {
"build": {
"cache": false,
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/automation",
"main": "packages/automation/src/index.ts",
"tsConfig": "packages/automation/tsconfig.lib.json",
"assets": ["packages/automation/*.md"],
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": ["^build"]
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/automation/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/packages/automation"],
"options": {
"jestConfig": "packages/automation/jest.config.ts",
"passWithNoTests": true
}
}
},
"tags": []
}
4 changes: 4 additions & 0 deletions packages/automation/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lib/listeners';
export * from './lib/states';
export * from './lib/transitions';
export * from './lib/state-machine';
36 changes: 36 additions & 0 deletions packages/automation/src/lib/listeners/constant.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ConstantListener } from './constant';

describe('ConstantListener', () => {
let constantListener: ConstantListener<number>;
const valueToEmit = 42;

beforeEach(() => {
constantListener = new ConstantListener(valueToEmit);
});

it('should emit the constant value immediately when started', async () => {
const callback = jest.fn();
constantListener.onStateChange(callback);

await constantListener.start();

// Advance event loop
await new Promise((resolve) => setTimeout(resolve, 0));

expect(callback).toHaveBeenCalledWith(valueToEmit);
});

it('should not emit any value after being stopped', async () => {
const callback = jest.fn();
constantListener.onStateChange(callback);

await constantListener.start();
await constantListener.stop();

// Advance event loop
await new Promise((resolve) => setTimeout(resolve, 0));

// Ensure no additional calls were made after stop
expect(callback).toHaveBeenCalledTimes(1);
});
});
17 changes: 17 additions & 0 deletions packages/automation/src/lib/listeners/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Listener } from './listener';

/**
* A simple listener that emits a constant value immediately when started
*/
export class ConstantListener<T> extends Listener<T> {
constructor(private value: T) {
super({
start: async () => {
// Emit value on next tick simulating a state change and respecting event architecture
setTimeout(() => {
this.emit(this.value);
}, 0);
},
});
}
}
56 changes: 56 additions & 0 deletions packages/automation/src/lib/listeners/evm-block.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { ethers } from 'ethers';

import { EVMBlockListener } from './evm-block';

jest.mock('ethers');

describe('EVMBlockListener', () => {
let evmBlockListener: EVMBlockListener;
let providerMock: jest.Mocked<ethers.providers.JsonRpcProvider>;

beforeEach(() => {
providerMock = {
on: jest.fn(),
removeAllListeners: jest.fn(),
getBlock: jest.fn().mockResolvedValue({ number: 123, hash: '0xabc' }),
} as unknown as jest.Mocked<ethers.providers.JsonRpcProvider>;

(
ethers.providers.JsonRpcProvider as unknown as jest.Mock
).mockImplementation(() => providerMock);

evmBlockListener = new EVMBlockListener('http://example-rpc-url.com');
});

afterEach(async () => {
await evmBlockListener.stop();
jest.clearAllMocks();
});

it('should start listening to block events', async () => {
await evmBlockListener.start();

expect(providerMock.on).toHaveBeenCalledWith('block', expect.any(Function));
});

it('should emit block data on block event', async () => {
const callback = jest.fn();
evmBlockListener.onStateChange(callback);

await evmBlockListener.start();

// Simulate block event
const blockEventCallback = providerMock.on.mock.calls[0][1];
await blockEventCallback(123);

expect(providerMock.getBlock).toHaveBeenCalledWith(123);
expect(callback).toHaveBeenCalledWith({ number: 123, hash: '0xabc' });
});

it('should stop listening to block events', async () => {
await evmBlockListener.start();
await evmBlockListener.stop();

expect(providerMock.removeAllListeners).toHaveBeenCalledWith('block');
});
});
27 changes: 27 additions & 0 deletions packages/automation/src/lib/listeners/evm-block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ethers } from 'ethers';

import { LIT_EVM_CHAINS } from '@lit-protocol/constants';

import { Listener } from './listener';

export type BlockData = ethers.providers.Block;

export class EVMBlockListener extends Listener<BlockData> {
constructor(rpcUrl: string = LIT_EVM_CHAINS['ethereum'].rpcUrls[0]) {
const provider = new ethers.providers.JsonRpcProvider(rpcUrl);

super({
start: async () => {
provider.on('block', async (blockNumber) => {
const block = await provider.getBlock(blockNumber);
if (block) {
this.emit(block);
}
});
},
stop: async () => {
provider.removeAllListeners('block');
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ethers } from 'ethers';

import {
EVMContractEventListener,
ContractInfo,
EventInfo,
} from './evm-contract-event';

jest.mock('ethers');

describe('EVMContractEventListener', () => {
let evmContractEventListener: EVMContractEventListener;
let contractMock: jest.Mocked<ethers.Contract>;
const rpcUrl = 'http://example-rpc-url.com';
const contractInfo: ContractInfo = {
address: '0x123',
abi: [],
};
const eventInfo: EventInfo = {
name: 'TestEvent',
};

beforeEach(() => {
contractMock = {
on: jest.fn(),
removeAllListeners: jest.fn(),
filters: {
TestEvent: jest.fn().mockReturnValue({}),
},
} as unknown as jest.Mocked<ethers.Contract>;

(ethers.Contract as unknown as jest.Mock).mockImplementation(
() => contractMock
);

evmContractEventListener = new EVMContractEventListener(
rpcUrl,
contractInfo,
eventInfo
);
});

afterEach(async () => {
await evmContractEventListener.stop();
jest.clearAllMocks();
});

it('should start listening to contract events', async () => {
await evmContractEventListener.start();

expect(contractMock.on).toHaveBeenCalledWith({}, expect.any(Function));
});

it('should emit event data on contract event', async () => {
const callback = jest.fn();
evmContractEventListener.onStateChange(callback);

await evmContractEventListener.start();

// Simulate contract event
const eventCallback = contractMock.on.mock.calls[0][1];
const mockEvent = { blockNumber: 123, transactionHash: '0xabc' };
eventCallback('arg1', 'arg2', mockEvent);

expect(callback).toHaveBeenCalledWith({
event: mockEvent,
args: ['arg1', 'arg2'],
blockNumber: 123,
transactionHash: '0xabc',
});
});

it('should stop listening to contract events', async () => {
await evmContractEventListener.start();
await evmContractEventListener.stop();

expect(contractMock.removeAllListeners).toHaveBeenCalledWith(
eventInfo.name
);
});
});
Loading
Loading