-
Notifications
You must be signed in to change notification settings - Fork 88
feat: new event listener automation package #726
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
Merged
Merged
Changes from 22 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
327964e
feat: add automation package building blocks, including state machine…
FedericoAmura 40bbafe
feat: add ids in state machines
FedericoAmura 1f76cc6
feat: state machine creation using declarative config
FedericoAmura 720cce3
feat: native and erc20 balances monitoring transitions declarative in…
FedericoAmura be62dbc
feat: add lit action run state machine capability as a state
FedericoAmura 47cc68a
feat: give state machine ability to sign and send transactions and to…
FedericoAmura 4b39424
feat: add machine PKP and private key handling and refactor LitAction…
FedericoAmura 6a1b481
feat: include transitions definitions inside states definitions
FedericoAmura 3e37a56
fix: missing change, transitions property in StateDefinition
FedericoAmura 751b1dd
feat: refactor types
FedericoAmura dcf0f07
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura 06a6da5
feat: add state machine context to dynamically share information betw…
FedericoAmura 3889f78
feat: generalize context usage to states variables
FedericoAmura c4786f9
feat: types improvement
FedericoAmura 7598448
fix: queue events in transition to avoid multiple racing calls to che…
FedericoAmura 3e12d72
fix: remove unnecessary context functions in machine
FedericoAmura 2c26270
feat: error handling while in automation execution
FedericoAmura fbe2cce
feat:
FedericoAmura 74d59c2
feat: remove unnecessary things and restore state machine context man…
FedericoAmura cdf0715
feat: update README.md
FedericoAmura a82feb2
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura a829079
chore: fmt
FedericoAmura c554d2c
fix: readme timer example
FedericoAmura 8ec1b83
feat: improve comments
FedericoAmura a677568
feat: improve state machine context methods types
FedericoAmura 00435e9
fix: replace jsParams anys for unknowns
FedericoAmura 2728414
feat: refactor state functions into Actions
FedericoAmura a9ee7ae
feat: update readme with new Actions concept, removed from State
FedericoAmura 0b55dd4
fix: remove some anys in types
FedericoAmura 0e6ac5c
feat: replace states function properties with a generic actions array
FedericoAmura ae7cade
feat: update readme with actions in states
FedericoAmura 48e47e0
feat: improve types
FedericoAmura ddf1261
feat: improve types
FedericoAmura 407c97e
feat: allow actions in transitions triggered when a match is found
FedericoAmura 8cebae7
feat: readme descriptions update
FedericoAmura 32ac3e9
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura c4c8731
fix: log all context path check
FedericoAmura 40f2600
fix: make contextUpdates optional
FedericoAmura fc570e3
feat: throw if lit action response is not successful
FedericoAmura c313106
feat: allow transaction definitions with data instead of populating a…
FedericoAmura 0f4df3b
feat: increase support for evmChainIds in different types using ether…
FedericoAmura 528bc01
feat: rename automation package to event-listener
FedericoAmura bc14aa5
Merge branch 'master' into feature/lit-4031-new-listener-sdk
Ansonhkg 947367b
typo
zach-is-my-name 026103a
feat: add machine drawings in README.md to clarify explanation
FedericoAmura 331dc2d
feat: add reference to the open source machine running server
FedericoAmura 8723f8b
Merge pull request #746 from zach-is-my-name/app/upstream/feature/lit…
Ansonhkg d4bf15a
fix: rename event-listener repo reference
FedericoAmura File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "presets": [ | ||
| [ | ||
| "@nx/web/babel", | ||
| { | ||
| "useBuiltIns": "usage" | ||
| } | ||
| ] | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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": {} | ||
| } | ||
| ] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,347 @@ | ||
| # @lit-protocol/automation | ||
|
|
||
| A TypeScript library for creating and managing state machines that can automate complex workflows involving the Lit Protocol network, blockchain events, and other triggers. | ||
|
|
||
| ## Overview | ||
|
|
||
| The automation package provides a flexible state machine implementation that allows you to: | ||
|
|
||
| - Create automated workflows that respond to blockchain events | ||
| - Execute Lit Actions based on custom triggers | ||
| - Mint PKPs and Capacity Delegation Tokens | ||
| - Monitor token balances and prices | ||
| - Bridge tokens across chains automatically using PKPs | ||
| - Automate PKP (Programmable Key Pair) operations overall | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| npm install @lit-protocol/automation | ||
| # or | ||
| yarn add @lit-protocol/automation | ||
| ``` | ||
|
|
||
| ## Core Concepts | ||
|
|
||
| ### State Machine | ||
|
|
||
| A state machine consists of states, and transitions between those states which are triggered based on a collection of Listeners. | ||
|
|
||
| ### States | ||
|
|
||
| States represent different phases of your automation. Each state can: | ||
|
|
||
| - Execute code when entered and/or exited | ||
| - Configure PKPs and Capacity Credits for the machine | ||
| - Run Lit Actions | ||
| - Send blockchain transactions | ||
| - Run custom code | ||
|
|
||
| ### Transitions | ||
|
|
||
| Transitions define how the machine moves between states. They can be triggered automatically or by any combination of: | ||
|
|
||
| - Blockchain events | ||
| - Token balance changes | ||
| - Timers and intervals | ||
| - HTTP requests (polling) | ||
| - Custom conditions | ||
|
|
||
| ### Listeners | ||
|
|
||
| Listeners monitor various events and feed data to transitions: | ||
|
|
||
| - EVMBlockListener: Monitors new blocks | ||
| - EVMContractEventListener: Monitors EVM smart contract events | ||
| - TimerListener: Triggers based on time | ||
| - FetchListener: Polls an HTTP endpoint at regular intervals | ||
| - IntervalListener: Runs a function at regular intervals | ||
|
|
||
| ## Basic Example | ||
|
|
||
| Here's a simple example that mints a PKP, a Capacity Delegation NFT and then runs a Lit Action every hour: | ||
|
|
||
| ```typescript | ||
| async function runLitActionInterval() { | ||
| const stateMachine = StateMachine.fromDefinition({ | ||
| privateKey: '0xPRIVATE_KEY_WITH_LIT_TOKENS', | ||
| litNodeClient: { | ||
| litNetwork: 'datil-test', | ||
| }, | ||
| litContracts: { | ||
| network: 'datil-test', | ||
| }, | ||
| states: [ | ||
| { | ||
| key: 'setPKP', | ||
| usePkp: { | ||
| mint: true, | ||
| }, | ||
| transitions: [{ toState: 'setCapacityNFT' }], | ||
| }, | ||
| { | ||
| key: 'setCapacityNFT', | ||
| useCapacityNFT: { | ||
| mint: true, | ||
| daysUntilUTCMidnightExpiration: 10, | ||
| requestPerSecond: 1, | ||
| }, | ||
| transitions: [{ toState: 'runLitAction' }], | ||
| }, | ||
| { | ||
| key: 'runLitAction', | ||
| litAction: { | ||
| code: `(async () => { | ||
| if (magicNumber >= 42) { | ||
| LitActions.setResponse({ response:"The number is greater than or equal to 42!" }); | ||
| } else { | ||
| LitActions.setResponse({ response: "The number is less than 42!" }); | ||
| } | ||
| })();`, | ||
| jsParams: { | ||
| magicNumber: Math.floor(Math.random() * 100), | ||
| }, | ||
| }, | ||
| transitions: [{ toState: 'cooldown' }], | ||
| }, | ||
| { | ||
| key: 'cooldown', | ||
| transitions: [ | ||
| { | ||
| toState: 'runLitAction', | ||
| timer: { | ||
| until: 1 * 60 * 60 * 1000, | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| // Start the machine at the desired state | ||
| await stateMachine.startMachine('setPKP'); | ||
| } | ||
|
|
||
| runLitActionInterval().catch(console.error); | ||
| ``` | ||
|
|
||
| ## Functional interface | ||
|
|
||
| There care cases where such a declarative interface won't be enough for your use case. When that happens, the machines can also accept generic states, transitions and listeners where it is possible to write any logic. | ||
|
|
||
| Here is an example that listens to Ethereum blocks looking one whose numbers ends in 0 | ||
FedericoAmura marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ```typescript | ||
| async function monitorEthereumBlocksWithHashEndingWithZero() { | ||
| const litNodeClient = new LitNodeClient({ | ||
| litNetwork: 'datil-dev', | ||
| }); | ||
| const litContracts = new LitContracts({ | ||
| network: 'datil-dev', | ||
| }); | ||
| const stateMachine = new StateMachine({ | ||
| // When the machine doesn't mint nor use Lit, these values do not matter | ||
| privateKey: 'NOT_USED', | ||
| litNodeClient, | ||
| litContracts, | ||
| }); | ||
| // const stateMachine = StateMachine.fromDefinition({...}) also works to extend a base definition | ||
|
|
||
| // Add each state individually | ||
| stateMachine.addState({ | ||
| key: 'listenBlocks', | ||
| onEnter: async () => | ||
| console.log('Waiting for a block with a hash ending in 0'), | ||
| onExit: async () => console.log('Found a block whose hash ends in 0!'), | ||
| }); | ||
| stateMachine.addState({ | ||
| key: 'autoAdvancingState', | ||
| }); | ||
|
|
||
| // Then add transitions between states | ||
| stateMachine.addTransition({ | ||
| // Because this transition does not have any listeners, it will be triggered automatically when the machine enters fromState | ||
| fromState: 'autoAdvancingState', | ||
| toState: 'listenBlocks', | ||
| }); | ||
| stateMachine.addTransition({ | ||
| fromState: 'listenBlocks', | ||
| toState: 'autoAdvancingState', | ||
| // listeners are the ones that will produce the values that the transition will monitor | ||
| listeners: [new EVMBlockListener(LIT_EVM_CHAINS.ethereum.rpcUrls[0])], | ||
| // check is the function that will evaluate all values produced by listeners and define if there is a match or not | ||
| check: async (values): Promise<boolean> => { | ||
| // values are the results of all listeners | ||
| const blockData = values[0] as BlockData; | ||
| if (!blockData) return false; | ||
| console.log(`New block: ${blockData.number} (${blockData.hash})`); | ||
| return blockData.hash.endsWith('0'); | ||
| }, | ||
| // when check finds a match (returns true) this function gets executed and the machine moves to toState | ||
| onMatch: async (values) => { | ||
| // values are the results of all listeners | ||
| console.log('We have matching values here'); | ||
| }, | ||
| onMismatch: undefined, // when check returns false (there is a mismatch) this function gets executed but the machine does not change state | ||
| onError: undefined, | ||
| }); | ||
|
|
||
| await stateMachine.startMachine('listenBlocks'); | ||
| } | ||
| monitorEthereumBlocksWithHashEndingWithZero().catch(console.error); | ||
| ``` | ||
|
|
||
| Last machine could have been implemented with just the `listenBlocks` state and a `listenBlocks` -> `listenBlocks` transition, but the machine realizes that the state does not change and therefore does not exit nor enter the state, however it runs the transition `onMatch` function. | ||
|
|
||
| ## Context | ||
|
|
||
| Each State Machine has its own information repository called `context`. | ||
|
|
||
| When using the defined states in the declarative interface, some values are already populated and then used later | ||
|
|
||
| - `StateDefinition.usePkp` populates `context.activePkp` with the minted PKP data | ||
| - `StateDefinition.useCapacityNFT` populates `context.activeCapacityTokenId` with the minted Capacity Token Id | ||
| - `StateDefinition.litAction` populates `context.lastLitActionResponse` with the lit action response | ||
| - `StateDefinition.transaction` populates `context.lastTransactionReceipt` with the transaction receipt | ||
|
|
||
| Several places in the machine definition can read values from the context. Instead of passing a literal value, pass an object with the `contextPath` property, like in the following example. | ||
|
|
||
| The machine context can be accessed using its `getFromContext`, `setToContext` or `pushToContext` methods to read or write. | ||
|
|
||
| ### Advance example | ||
|
|
||
| By leveraging the State Machine context and the ability of Lit PKPs to sign transaction of a variety of chains, it is possible to implement a Token Bridge that composes multiple chains and even offchain interaction if needed among other uses cases. | ||
FedericoAmura marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| In this example, when a State Machine PKP receives USDC in Base Sepolia, it will send the same amount to the sender but in Ethereum Sepolia | ||
|
|
||
| ```typescript | ||
| async function bridgeBaseSepoliaUSDCToEthereumSepolia() { | ||
| const evmSourceNetwork = LIT_EVM_CHAINS.baseSepolia; | ||
| const evmDestinationNetwork = LIT_EVM_CHAINS.sepolia; | ||
| const pkp = { | ||
| tokenId: '0x123...', | ||
| publicKey: '456...', | ||
| ethAddress: '0x789...', | ||
| } as PKPInfo; // Minted Previously | ||
| const capacityTokenId = '123456'; // Minted previously | ||
| // Because the pkp and the capacity token nft were minted previously, this private key only needs to be an authorized signer of the pkp. It can be empty, without funds of any kind | ||
| const ethPrivateKey = '0xTHE_PKP_AUTHORIZED_SIGNER_PRIVATE_KEY'; | ||
|
|
||
| const stateMachine = StateMachine.fromDefinition({ | ||
| privateKey: ethPrivateKey, // Used only for authorization here, minting was done previously | ||
| context: { | ||
| // We can prepopulate the context, for example setting the pkp here instead of using state.usePkp later | ||
| // activePkp: pkp, | ||
| }, | ||
| litNodeClient: { | ||
| litNetwork: 'datil', | ||
| }, | ||
| litContracts: { | ||
| network: 'datil', | ||
| }, | ||
| states: [ | ||
| { | ||
| key: 'setPKP', | ||
| usePkp: { | ||
| pkp, // Configure the pkp passed. Not minting a new one | ||
| }, | ||
| transitions: [{ toState: 'setCapacityNFT' }], | ||
| }, | ||
| { | ||
| key: 'setCapacityNFT', | ||
| useCapacityNFT: { | ||
| capacityTokenId: capacityTokenId, // Configure the capacity token to use. Not minting a new one | ||
| }, | ||
| transitions: [{ toState: 'waitForFunds' }], | ||
| }, | ||
| { | ||
| key: 'waitForFunds', | ||
| // Waits for our emitting PKP to have some USDC and native balance in destination chain | ||
| transitions: [ | ||
| { | ||
| toState: 'waitForTransfer', | ||
| balances: [ | ||
| { | ||
| address: pkp.ethAddress as Address, | ||
| evmChainId: evmDestinationNetwork.chainId, | ||
| type: 'native' as const, | ||
| comparator: '>=' as const, | ||
| amount: '0.001', | ||
| }, | ||
| { | ||
| address: pkp.ethAddress as Address, | ||
| evmChainId: evmDestinationNetwork.chainId, | ||
| type: 'ERC20' as const, | ||
| tokenAddress: USDC_ETH_SEPOLIA_ADDRESS, | ||
| tokenDecimals: 6, | ||
| comparator: '>=' as const, | ||
| amount: '20', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| key: 'waitForTransfer', | ||
| context: { | ||
| log: { | ||
| atEnter: true, | ||
| atExit: true, | ||
| }, | ||
| }, | ||
| transitions: [ | ||
| // Waits to receive an USDC transfer in our listening chain | ||
| { | ||
| toState: 'transferFunds', | ||
| evmContractEvent: { | ||
| evmChainId: evmSourceNetwork.chainId, | ||
| contractAddress: USDC_BASE_SEPOLIA_ADDRESS, | ||
| contractABI: USDC_ABI, | ||
| eventName: 'Transfer', | ||
| // Filter events using params for just listening the pkp.ethAddress as destination | ||
| eventParams: [null, pkp.ethAddress], | ||
| contextUpdates: [ | ||
| // The transition can perform some updates to the context | ||
| { | ||
| contextPath: 'transfer.sender', // The context path to update | ||
| dataPath: 'event.args[0]', // The value from the event to save in the context | ||
| }, | ||
| { | ||
| contextPath: 'transfer.amount', | ||
| dataPath: 'event.args[2]', | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| key: 'transferFunds', | ||
| // Sends a transaction to transfer some USDC in destination chain | ||
| transaction: { | ||
| evmChainId: evmDestinationNetwork.chainId, | ||
| contractAddress: USDC_ETH_SEPOLIA_ADDRESS, | ||
| contractABI: [ | ||
| 'function transfer(address to, uint256 amount) public returns (bool)', | ||
| ], | ||
| method: 'transfer', | ||
| params: [ | ||
| // Params can be hardcoded values such as ['0x123...', '100'] or values from the state machine context | ||
| { | ||
| contextPath: 'transfer.sender', | ||
| }, | ||
| { | ||
| contextPath: 'transfer.amount', | ||
| }, | ||
| ], | ||
| }, | ||
| // Going back to waitForFunds to suspend machine if we need more sepolia eth or sepolia USDC | ||
| transitions: [{ toState: 'waitForFunds' }], | ||
| }, | ||
| ], | ||
| }); | ||
|
|
||
| await stateMachine.startMachine('setPKP'); | ||
| } | ||
| bridgeBaseSepoliaUSDCToEthereumSepolia().catch(console.error); | ||
| ``` | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.