Skip to content
Merged
Show file tree
Hide file tree
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 Nov 27, 2024
40bbafe
feat: add ids in state machines
FedericoAmura Nov 27, 2024
1f76cc6
feat: state machine creation using declarative config
FedericoAmura Nov 29, 2024
720cce3
feat: native and erc20 balances monitoring transitions declarative in…
FedericoAmura Dec 3, 2024
be62dbc
feat: add lit action run state machine capability as a state
FedericoAmura Dec 3, 2024
47cc68a
feat: give state machine ability to sign and send transactions and to…
FedericoAmura Dec 5, 2024
4b39424
feat: add machine PKP and private key handling and refactor LitAction…
FedericoAmura Dec 6, 2024
6a1b481
feat: include transitions definitions inside states definitions
FedericoAmura Dec 6, 2024
3e37a56
fix: missing change, transitions property in StateDefinition
FedericoAmura Dec 6, 2024
751b1dd
feat: refactor types
FedericoAmura Dec 6, 2024
dcf0f07
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura Dec 9, 2024
06a6da5
feat: add state machine context to dynamically share information betw…
FedericoAmura Dec 11, 2024
3889f78
feat: generalize context usage to states variables
FedericoAmura Dec 12, 2024
c4786f9
feat: types improvement
FedericoAmura Dec 12, 2024
7598448
fix: queue events in transition to avoid multiple racing calls to che…
FedericoAmura Dec 12, 2024
3e12d72
fix: remove unnecessary context functions in machine
FedericoAmura Dec 12, 2024
2c26270
feat: error handling while in automation execution
FedericoAmura Dec 12, 2024
fbe2cce
feat:
FedericoAmura Dec 13, 2024
74d59c2
feat: remove unnecessary things and restore state machine context man…
FedericoAmura Dec 13, 2024
cdf0715
feat: update README.md
FedericoAmura Dec 13, 2024
a82feb2
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura Dec 13, 2024
a829079
chore: fmt
FedericoAmura Dec 13, 2024
c554d2c
fix: readme timer example
FedericoAmura Dec 16, 2024
8ec1b83
feat: improve comments
FedericoAmura Dec 16, 2024
a677568
feat: improve state machine context methods types
FedericoAmura Dec 16, 2024
00435e9
fix: replace jsParams anys for unknowns
FedericoAmura Dec 16, 2024
2728414
feat: refactor state functions into Actions
FedericoAmura Dec 16, 2024
a9ee7ae
feat: update readme with new Actions concept, removed from State
FedericoAmura Dec 16, 2024
0b55dd4
fix: remove some anys in types
FedericoAmura Dec 16, 2024
0e6ac5c
feat: replace states function properties with a generic actions array
FedericoAmura Dec 17, 2024
ae7cade
feat: update readme with actions in states
FedericoAmura Dec 17, 2024
48e47e0
feat: improve types
FedericoAmura Dec 17, 2024
ddf1261
feat: improve types
FedericoAmura Dec 17, 2024
407c97e
feat: allow actions in transitions triggered when a match is found
FedericoAmura Dec 17, 2024
8cebae7
feat: readme descriptions update
FedericoAmura Dec 18, 2024
32ac3e9
Merge branch 'refs/heads/master' into feature/lit-4031-new-listener-sdk
FedericoAmura Dec 18, 2024
c4c8731
fix: log all context path check
FedericoAmura Dec 18, 2024
40f2600
fix: make contextUpdates optional
FedericoAmura Dec 18, 2024
fc570e3
feat: throw if lit action response is not successful
FedericoAmura Dec 18, 2024
c313106
feat: allow transaction definitions with data instead of populating a…
FedericoAmura Dec 18, 2024
0f4df3b
feat: increase support for evmChainIds in different types using ether…
FedericoAmura Dec 19, 2024
528bc01
feat: rename automation package to event-listener
FedericoAmura Dec 20, 2024
bc14aa5
Merge branch 'master' into feature/lit-4031-new-listener-sdk
Ansonhkg Dec 21, 2024
947367b
typo
zach-is-my-name Dec 22, 2024
026103a
feat: add machine drawings in README.md to clarify explanation
FedericoAmura Dec 23, 2024
331dc2d
feat: add reference to the open source machine running server
FedericoAmura Dec 23, 2024
8723f8b
Merge pull request #746 from zach-is-my-name/app/upstream/feature/lit…
Ansonhkg Dec 23, 2024
d4bf15a
fix: rename event-listener repo reference
FedericoAmura Dec 23, 2024
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 @@ -85,6 +85,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": {}
}
]
}
347 changes: 347 additions & 0 deletions packages/automation/README.md
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

```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.

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);
```
Loading
Loading