Skip to content

Commit 47cc68a

Browse files
committed
feat: give state machine ability to sign and send transactions and to have several actions in one state
1 parent be62dbc commit 47cc68a

File tree

4 files changed

+178
-63
lines changed

4 files changed

+178
-63
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const signWithLitActionCode = `(async () => {
2+
const signature = await Lit.Actions.signAndCombineEcdsa({
3+
toSign,
4+
publicKey,
5+
sigName,
6+
});
7+
8+
Lit.Actions.setResponse({ response: signature });
9+
})();`;

packages/automation/src/lib/state-machine.ts

Lines changed: 144 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,7 @@
11
import { ethers } from 'ethers';
22

3-
import {
4-
// createSiweMessageWithRecaps,
5-
// generateAuthSig,
6-
LitActionResource,
7-
// LitPKPResource,
8-
} from '@lit-protocol/auth-helpers';
9-
import {
10-
LIT_ABILITY,
11-
LIT_EVM_CHAINS,
12-
LIT_RPC,
13-
LIT_NETWORK,
14-
} from '@lit-protocol/constants';
3+
import { LitActionResource } from '@lit-protocol/auth-helpers';
4+
import { LIT_ABILITY, LIT_RPC } from '@lit-protocol/constants';
155
import { LitContracts } from '@lit-protocol/contracts-sdk';
166
import { EthWalletProvider } from '@lit-protocol/lit-auth-client';
177
import { LitNodeClient } from '@lit-protocol/lit-node-client';
@@ -23,25 +13,15 @@ import {
2313
Listener,
2414
TimerListener,
2515
} from './listeners';
16+
import { signWithLitActionCode } from './litActions';
2617
import { State, StateParams } from './states';
2718
import { Check, Transition } from './transitions';
2819
import { getChain } from './utils/chain';
2920
import { getBalanceTransitionCheck, getERC20Balance } from './utils/erc20';
3021

3122
import type {
32-
Address,
33-
BalanceTransitionDefinition,
34-
BaseBalanceTransitionDefinition,
3523
BaseStateMachineParams,
36-
ERC20BalanceTransitionDefinition,
37-
EvmContractEventTransitionDefinition,
38-
IntervalTransitionDefinition,
39-
NativeBalanceTransitionDefinition,
40-
OnEvmChainEvent,
41-
StateDefinition,
4224
StateMachineDefinition,
43-
TimerTransitionDefinition,
44-
TransitionDefinition,
4525
TransitionParams,
4626
} from './types';
4727

@@ -51,11 +31,58 @@ const ethPrivateKey = process.env['ETHEREUM_PRIVATE_KEY'];
5131
if (!ethPrivateKey) {
5232
throw new Error('ethPrivateKey not defined');
5333
}
54-
const yellowstoneSigner = new ethers.Wallet(
34+
const yellowstoneLitSigner = new ethers.Wallet(
5535
ethPrivateKey,
5636
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
5737
);
5838

39+
// TODO improve and move
40+
interface ExecuteLitAction {
41+
litNodeClient: LitNodeClient;
42+
pkpPublicKey: string;
43+
machineSigner: ethers.Wallet;
44+
ipfsId?: string;
45+
code: string;
46+
jsParams: Record<string, any>;
47+
}
48+
49+
async function executeLitAction({
50+
litNodeClient,
51+
pkpPublicKey,
52+
machineSigner,
53+
ipfsId,
54+
code,
55+
jsParams,
56+
}: ExecuteLitAction) {
57+
const pkpSessionSigs = await litNodeClient.getPkpSessionSigs({
58+
pkpPublicKey,
59+
capabilityAuthSigs: [],
60+
authMethods: [
61+
await EthWalletProvider.authenticate({
62+
signer: machineSigner,
63+
litNodeClient: litNodeClient,
64+
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
65+
}),
66+
],
67+
resourceAbilityRequests: [
68+
{
69+
resource: new LitActionResource('*'),
70+
ability: LIT_ABILITY.LitActionExecution,
71+
},
72+
],
73+
});
74+
75+
// Run a LitAction
76+
const executeJsResponse = await litNodeClient.executeJs({
77+
ipfsId,
78+
code,
79+
jsParams,
80+
sessionSigs: pkpSessionSigs,
81+
});
82+
83+
return executeJsResponse;
84+
}
85+
5986
/**
6087
* A StateMachine class that manages states and transitions between them.
6188
*/
@@ -83,6 +110,7 @@ export class StateMachine {
83110
static fromDefinition(machineConfig: StateMachineDefinition): StateMachine {
84111
const { litNodeClient, litContracts = {} } = machineConfig;
85112

113+
// Create litNodeClient and litContracts instances
86114
const litNodeClientInstance =
87115
'connect' in litNodeClient
88116
? litNodeClient
@@ -91,7 +119,7 @@ export class StateMachine {
91119
'connect' in litContracts
92120
? litContracts
93121
: new LitContracts({
94-
signer: yellowstoneSigner,
122+
signer: yellowstoneLitSigner,
95123
...litContracts,
96124
});
97125

@@ -110,17 +138,19 @@ export class StateMachine {
110138
});
111139

112140
machineConfig.states.forEach((state) => {
113-
const { litAction } = state;
141+
const { litAction, transaction } = state;
114142

115143
const stateConfig: StateParams = {
116144
key: state.key,
117145
};
118146

147+
const onEnterFunctions = [] as (() => Promise<void>)[];
148+
119149
if (litAction) {
120150
let pkpPublicKey: string = litAction.pkpPublicKey;
121151

122-
stateConfig.onEnter = async () => {
123-
const yellowstoneSigner = new ethers.Wallet(
152+
onEnterFunctions.push(async () => {
153+
const yellowstoneMachineSigner = new ethers.Wallet(
124154
litAction.pkpOwnerKey,
125155
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
126156
);
@@ -134,37 +164,99 @@ export class StateMachine {
134164
console.log(`Minted PKP: ${pkp}`);
135165
}
136166

137-
const pkpSessionSigs = await litNodeClientInstance.getPkpSessionSigs({
167+
const litActionResponse = await executeLitAction({
168+
litNodeClient: litNodeClientInstance,
138169
pkpPublicKey,
139-
capabilityAuthSigs: [],
140-
authMethods: [
141-
await EthWalletProvider.authenticate({
142-
signer: yellowstoneSigner,
143-
litNodeClient: litNodeClientInstance,
144-
expiration: new Date(Date.now() + 1000 * 60 * 10).toISOString(), // 10 minutes
145-
}),
146-
],
147-
resourceAbilityRequests: [
148-
{
149-
resource: new LitActionResource('*'),
150-
ability: LIT_ABILITY.LitActionExecution,
151-
},
152-
],
153-
});
154-
155-
// Run a LitAction
156-
const executeJsResponse = await litNodeClientInstance.executeJs({
157-
sessionSigs: pkpSessionSigs,
170+
machineSigner: yellowstoneMachineSigner,
158171
ipfsId: litAction.ipfsId,
159172
code: litAction.code,
160173
jsParams: litAction.jsParams,
161174
});
162175

163-
// TODO send user this result with a webhook maybe
164-
console.log(`============ executeJsResponse:`, executeJsResponse);
165-
};
176+
// TODO send user this result with a webhook and log
177+
console.log(`============ litActionResponse:`, litActionResponse);
178+
});
179+
}
180+
181+
if (transaction) {
182+
onEnterFunctions.push(async () => {
183+
const yellowstoneMachineSigner = new ethers.Wallet(
184+
transaction.pkpOwnerKey,
185+
new ethers.providers.JsonRpcProvider(LIT_RPC.CHRONICLE_YELLOWSTONE)
186+
);
187+
188+
const chain = getChain(transaction);
189+
const chainProvider = new ethers.providers.JsonRpcProvider(
190+
chain.rpcUrls[0],
191+
chain.chainId
192+
);
193+
194+
const contract = new ethers.Contract(
195+
transaction.contractAddress,
196+
transaction.contractABI,
197+
chainProvider
198+
);
199+
200+
const txData = await contract.populateTransaction[transaction.method](
201+
...(transaction.params || [])
202+
);
203+
const gasLimit = await chainProvider.estimateGas({
204+
to: transaction.contractAddress,
205+
data: txData.data,
206+
from: transaction.pkpEthAddress,
207+
});
208+
const gasPrice = await chainProvider.getGasPrice();
209+
const nonce = await chainProvider.getTransactionCount(
210+
transaction.pkpEthAddress
211+
);
212+
213+
const rawTx = {
214+
chainId: chain.chainId,
215+
data: txData.data,
216+
gasLimit: gasLimit.toHexString(),
217+
gasPrice: gasPrice.toHexString(),
218+
nonce,
219+
to: transaction.contractAddress,
220+
};
221+
const rawTxHash = ethers.utils.keccak256(
222+
ethers.utils.serializeTransaction(rawTx)
223+
);
224+
225+
// Sign with the PKP in a LitAction
226+
const litActionResponse = await executeLitAction({
227+
litNodeClient: litNodeClientInstance,
228+
pkpPublicKey: transaction.pkpPublicKey,
229+
machineSigner: yellowstoneMachineSigner,
230+
code: signWithLitActionCode,
231+
jsParams: {
232+
toSign: ethers.utils.arrayify(rawTxHash),
233+
publicKey: transaction.pkpPublicKey,
234+
sigName: 'signedTransaction',
235+
},
236+
});
237+
238+
const signature = litActionResponse.response as string;
239+
const jsonSignature = JSON.parse(signature);
240+
jsonSignature.r = '0x' + jsonSignature.r.substring(2);
241+
jsonSignature.s = '0x' + jsonSignature.s;
242+
const hexSignature = ethers.utils.joinSignature(jsonSignature);
243+
244+
const signedTx = ethers.utils.serializeTransaction(
245+
rawTx,
246+
hexSignature
247+
);
248+
249+
const receipt = await chainProvider.sendTransaction(signedTx);
250+
251+
// TODO send user this result with a webhook and log
252+
console.log('Transaction Receipt:', receipt);
253+
});
166254
}
167255

256+
stateConfig.onEnter = async () => {
257+
await Promise.all(onEnterFunctions.map((onEnter) => onEnter()));
258+
};
259+
168260
stateMachine.addState(stateConfig);
169261
});
170262

@@ -430,6 +522,7 @@ export class StateMachine {
430522
this.isRunning && (await this.enterState(stateKey));
431523
} catch (e) {
432524
this.currentState = undefined;
525+
console.error(e);
433526
throw new Error(`Could not enter state ${stateKey}`);
434527
}
435528
}

packages/automation/src/lib/types.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,34 @@ import { BaseTransitionParams } from './transitions';
77

88
export type Address = `0x${string}`;
99

10-
export interface LitActionStateDefinition {
10+
export interface OnEvmChain {
11+
evmChainId: number;
12+
}
13+
14+
export interface UsesPkp {
1115
pkpOwnerKey: string;
1216
pkpPublicKey: string;
17+
pkpEthAddress: Address;
18+
}
19+
20+
export interface LitActionStateDefinition extends UsesPkp {
1321
ipfsId?: string; // TODO separate into another without code
1422
code: string;
1523
jsParams: Record<string, any>;
1624
}
1725

26+
export interface TransactionStateDefinition extends UsesPkp, OnEvmChain {
27+
contractAddress: Address;
28+
contractABI: ethers.ContractInterface;
29+
method: string;
30+
value?: string;
31+
params?: any[];
32+
}
33+
1834
export interface StateDefinition {
1935
key: string;
2036
litAction?: LitActionStateDefinition;
21-
}
22-
23-
export interface OnEvmChainEvent {
24-
evmChainId: number;
37+
transaction?: TransactionStateDefinition;
2538
}
2639

2740
export interface IntervalTransitionDefinition {
@@ -30,7 +43,7 @@ export interface IntervalTransitionDefinition {
3043

3144
export interface BaseBalanceTransitionDefinition
3245
extends IntervalTransitionDefinition,
33-
OnEvmChainEvent {
46+
OnEvmChain {
3447
address: Address;
3548
comparator: '>' | '>=' | '=' | '!=' | '<=' | '<';
3649
amount: string;
@@ -44,7 +57,7 @@ export interface NativeBalanceTransitionDefinition
4457
export interface ERC20BalanceTransitionDefinition
4558
extends BaseBalanceTransitionDefinition {
4659
type: 'ERC20';
47-
tokenAddress: string;
60+
tokenAddress: Address;
4861
tokenDecimals: number;
4962
}
5063

@@ -60,9 +73,9 @@ export interface TimerTransitionDefinition
6073
until: number;
6174
}
6275

63-
export interface EvmContractEventTransitionDefinition extends OnEvmChainEvent {
64-
contractAddress: string;
65-
abi: ethers.ContractInterface;
76+
export interface EvmContractEventTransitionDefinition extends OnEvmChain {
77+
contractAddress: Address;
78+
abi: ethers.ContractInterface; // TODO rename a contractABI
6679
eventName: string;
6780
eventParams?: any;
6881
}

packages/automation/src/lib/utils/chain.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { LIT_EVM_CHAINS } from '@lit-protocol/constants';
22

3-
import { OnEvmChainEvent } from '../types';
3+
import { OnEvmChain } from '../types';
44

5-
export function getChain(event: OnEvmChainEvent) {
5+
export function getChain(event: OnEvmChain) {
66
const chain = Object.values(LIT_EVM_CHAINS).find(
77
(chain) => chain.chainId === event.evmChainId
88
);

0 commit comments

Comments
 (0)