@aboutcircles/sdk-runner 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +47319 -35808
- package/dist/safe-4337-runner.d.ts +183 -0
- package/dist/safe-4337-runner.d.ts.map +1 -0
- package/dist/safe-4337-runner.js +378 -0
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Address, TransactionRequest } from '@aboutcircles/sdk-types';
|
|
2
|
+
import type { ContractRunner, BatchRun } from './runner';
|
|
3
|
+
import type { PublicClient, TransactionReceipt, Chain } from 'viem';
|
|
4
|
+
import type { EIP1193Provider } from 'viem';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration options for Safe 4337 paymaster
|
|
7
|
+
*/
|
|
8
|
+
export interface Safe4337PaymasterOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Whether transactions should be sponsored (gasless)
|
|
11
|
+
*/
|
|
12
|
+
isSponsored: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Pimlico paymaster URL for sponsored transactions
|
|
15
|
+
* @example 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY'
|
|
16
|
+
*/
|
|
17
|
+
paymasterUrl: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Configuration options for Safe 4337 custom contracts
|
|
21
|
+
*/
|
|
22
|
+
export interface Safe4337CustomContracts {
|
|
23
|
+
/**
|
|
24
|
+
* Address of the Safe 4337 module contract
|
|
25
|
+
* @example '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
|
|
26
|
+
*/
|
|
27
|
+
safe4337ModuleAddress: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional entry point address (defaults to standard ERC-4337 entry point)
|
|
30
|
+
*/
|
|
31
|
+
entryPointAddress?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Safe 4337 browser contract runner implementation using Safe4337Pack from relay-kit
|
|
35
|
+
* Executes account abstraction transactions with optional paymaster sponsorship
|
|
36
|
+
*
|
|
37
|
+
* This runner enables gasless transactions through ERC-4337 account abstraction
|
|
38
|
+
* and Pimlico bundler/paymaster infrastructure.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* import { createPublicClient, http } from 'viem';
|
|
43
|
+
* import { gnosis } from 'viem/chains';
|
|
44
|
+
* import { Safe4337Runner } from '@aboutcircles/sdk-runner';
|
|
45
|
+
*
|
|
46
|
+
* const publicClient = createPublicClient({
|
|
47
|
+
* chain: gnosis,
|
|
48
|
+
* transport: http('https://rpc.gnosischain.com')
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* const runner = new Safe4337Runner(
|
|
52
|
+
* publicClient,
|
|
53
|
+
* window.ethereum,
|
|
54
|
+
* '0xYourSafeAddress...',
|
|
55
|
+
* 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY',
|
|
56
|
+
* {
|
|
57
|
+
* isSponsored: true,
|
|
58
|
+
* paymasterUrl: 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY'
|
|
59
|
+
* },
|
|
60
|
+
* {
|
|
61
|
+
* safe4337ModuleAddress: '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
|
|
62
|
+
* }
|
|
63
|
+
* );
|
|
64
|
+
*
|
|
65
|
+
* await runner.init();
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare class Safe4337Runner implements ContractRunner {
|
|
69
|
+
address?: Address;
|
|
70
|
+
publicClient: PublicClient;
|
|
71
|
+
private eip1193Provider;
|
|
72
|
+
private safeAddress?;
|
|
73
|
+
private bundlerUrl;
|
|
74
|
+
private paymasterOptions?;
|
|
75
|
+
private customContracts?;
|
|
76
|
+
private safe4337?;
|
|
77
|
+
private safeModulesVersion;
|
|
78
|
+
/**
|
|
79
|
+
* Creates a new Safe4337Runner
|
|
80
|
+
* @param publicClient - The viem public client for reading blockchain state
|
|
81
|
+
* @param eip1193Provider - The EIP-1193 provider from the browser (e.g., window.ethereum)
|
|
82
|
+
* @param safeAddress - The address of the Safe wallet (optional, can be set in init)
|
|
83
|
+
* @param bundlerUrl - Pimlico bundler URL for ERC-4337 transactions
|
|
84
|
+
* @param paymasterOptions - Optional paymaster configuration for sponsored transactions
|
|
85
|
+
* @param customContracts - Optional custom contract addresses
|
|
86
|
+
* @param safeModulesVersion - Version of Safe modules (default: '0.3.0')
|
|
87
|
+
*/
|
|
88
|
+
constructor(publicClient: PublicClient, eip1193Provider: EIP1193Provider, safeAddress: Address | undefined, bundlerUrl: string, paymasterOptions?: Safe4337PaymasterOptions, customContracts?: Safe4337CustomContracts, safeModulesVersion?: string);
|
|
89
|
+
/**
|
|
90
|
+
* Create and initialize a Safe4337Runner in one step
|
|
91
|
+
* @param rpcUrl - The RPC URL to connect to
|
|
92
|
+
* @param eip1193Provider - The EIP-1193 provider from the browser (e.g., window.ethereum)
|
|
93
|
+
* @param safeAddress - The address of the Safe wallet
|
|
94
|
+
* @param chain - The viem chain configuration (e.g., gnosis from 'viem/chains')
|
|
95
|
+
* @param bundlerUrl - Pimlico bundler URL for ERC-4337 transactions
|
|
96
|
+
* @param paymasterOptions - Optional paymaster configuration for sponsored transactions
|
|
97
|
+
* @param customContracts - Optional custom contract addresses
|
|
98
|
+
* @param safeModulesVersion - Version of Safe modules (default: '0.3.0')
|
|
99
|
+
* @returns An initialized Safe4337Runner instance
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* import { gnosis } from 'viem/chains';
|
|
104
|
+
* import { Safe4337Runner } from '@aboutcircles/sdk-runner';
|
|
105
|
+
*
|
|
106
|
+
* const runner = await Safe4337Runner.create(
|
|
107
|
+
* 'https://rpc.gnosischain.com',
|
|
108
|
+
* window.ethereum,
|
|
109
|
+
* '0xYourSafeAddress...',
|
|
110
|
+
* gnosis,
|
|
111
|
+
* 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY',
|
|
112
|
+
* {
|
|
113
|
+
* isSponsored: true,
|
|
114
|
+
* paymasterUrl: 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY'
|
|
115
|
+
* },
|
|
116
|
+
* {
|
|
117
|
+
* safe4337ModuleAddress: '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
|
|
118
|
+
* }
|
|
119
|
+
* );
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
static create(rpcUrl: string, eip1193Provider: EIP1193Provider, safeAddress: Address, chain: Chain, bundlerUrl: string, paymasterOptions?: Safe4337PaymasterOptions, customContracts?: Safe4337CustomContracts, safeModulesVersion?: string): Promise<Safe4337Runner>;
|
|
123
|
+
/**
|
|
124
|
+
* Initialize the runner with a Safe address
|
|
125
|
+
* Sets up the Safe4337Pack with bundler and optional paymaster
|
|
126
|
+
* @param safeAddress - The address of the Safe wallet (optional if provided in constructor)
|
|
127
|
+
* @throws {RunnerError} If no Safe address is provided or initialization fails
|
|
128
|
+
*/
|
|
129
|
+
init(safeAddress?: Address): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Ensures the Safe4337Pack is initialized
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
private ensureSafe4337;
|
|
135
|
+
/**
|
|
136
|
+
* Estimate gas for a transaction
|
|
137
|
+
*/
|
|
138
|
+
estimateGas: (tx: TransactionRequest) => Promise<bigint>;
|
|
139
|
+
/**
|
|
140
|
+
* Call a contract (read-only operation)
|
|
141
|
+
*/
|
|
142
|
+
call: (tx: TransactionRequest) => Promise<string>;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve an ENS name to an address
|
|
145
|
+
*/
|
|
146
|
+
resolveName: (name: string) => Promise<string | null>;
|
|
147
|
+
/**
|
|
148
|
+
* Send one or more transactions through Safe 4337 and wait for confirmation
|
|
149
|
+
* All transactions are batched and executed atomically as a UserOperation
|
|
150
|
+
*
|
|
151
|
+
* The transaction will be sponsored if paymaster options were configured
|
|
152
|
+
*
|
|
153
|
+
* @throws {RunnerError} If transaction reverts or execution fails
|
|
154
|
+
*/
|
|
155
|
+
sendTransaction: (txs: TransactionRequest[]) => Promise<TransactionReceipt>;
|
|
156
|
+
/**
|
|
157
|
+
* Create a batch transaction runner
|
|
158
|
+
* @returns A Safe4337BatchRun instance for batching multiple transactions
|
|
159
|
+
*/
|
|
160
|
+
sendBatchTransaction: () => Safe4337BatchRun;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Batch transaction runner for Safe 4337 operations
|
|
164
|
+
* Allows multiple transactions to be batched and executed together as a UserOperation
|
|
165
|
+
*/
|
|
166
|
+
export declare class Safe4337BatchRun implements BatchRun {
|
|
167
|
+
private readonly safe4337;
|
|
168
|
+
private readonly publicClient;
|
|
169
|
+
private readonly transactions;
|
|
170
|
+
constructor(safe4337: any, publicClient: PublicClient);
|
|
171
|
+
/**
|
|
172
|
+
* Add a transaction to the batch
|
|
173
|
+
*/
|
|
174
|
+
addTransaction(tx: TransactionRequest): void;
|
|
175
|
+
/**
|
|
176
|
+
* Execute all batched transactions as a sponsored UserOperation
|
|
177
|
+
* Waits for the UserOperation to be included and confirmed on-chain
|
|
178
|
+
*
|
|
179
|
+
* @throws {RunnerError} If transaction reverts or execution fails
|
|
180
|
+
*/
|
|
181
|
+
run(): Promise<TransactionReceipt>;
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=safe-4337-runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-4337-runner.d.ts","sourceRoot":"","sources":["../src/safe-4337-runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { createPublicClient, http } from 'viem';
|
|
2
|
+
import { RunnerError } from './errors';
|
|
3
|
+
// Use require for Safe to ensure compatibility with bun's CJS/ESM interop
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
5
|
+
const Safe4337PackModule = require('@safe-global/relay-kit');
|
|
6
|
+
const Safe4337Pack = Safe4337PackModule.Safe4337Pack || Safe4337PackModule.default || Safe4337PackModule;
|
|
7
|
+
/**
|
|
8
|
+
* Safe 4337 browser contract runner implementation using Safe4337Pack from relay-kit
|
|
9
|
+
* Executes account abstraction transactions with optional paymaster sponsorship
|
|
10
|
+
*
|
|
11
|
+
* This runner enables gasless transactions through ERC-4337 account abstraction
|
|
12
|
+
* and Pimlico bundler/paymaster infrastructure.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { createPublicClient, http } from 'viem';
|
|
17
|
+
* import { gnosis } from 'viem/chains';
|
|
18
|
+
* import { Safe4337Runner } from '@aboutcircles/sdk-runner';
|
|
19
|
+
*
|
|
20
|
+
* const publicClient = createPublicClient({
|
|
21
|
+
* chain: gnosis,
|
|
22
|
+
* transport: http('https://rpc.gnosischain.com')
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* const runner = new Safe4337Runner(
|
|
26
|
+
* publicClient,
|
|
27
|
+
* window.ethereum,
|
|
28
|
+
* '0xYourSafeAddress...',
|
|
29
|
+
* 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY',
|
|
30
|
+
* {
|
|
31
|
+
* isSponsored: true,
|
|
32
|
+
* paymasterUrl: 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY'
|
|
33
|
+
* },
|
|
34
|
+
* {
|
|
35
|
+
* safe4337ModuleAddress: '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
|
|
36
|
+
* }
|
|
37
|
+
* );
|
|
38
|
+
*
|
|
39
|
+
* await runner.init();
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export class Safe4337Runner {
|
|
43
|
+
address;
|
|
44
|
+
publicClient;
|
|
45
|
+
eip1193Provider;
|
|
46
|
+
safeAddress;
|
|
47
|
+
bundlerUrl;
|
|
48
|
+
paymasterOptions;
|
|
49
|
+
customContracts;
|
|
50
|
+
safe4337;
|
|
51
|
+
safeModulesVersion;
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new Safe4337Runner
|
|
54
|
+
* @param publicClient - The viem public client for reading blockchain state
|
|
55
|
+
* @param eip1193Provider - The EIP-1193 provider from the browser (e.g., window.ethereum)
|
|
56
|
+
* @param safeAddress - The address of the Safe wallet (optional, can be set in init)
|
|
57
|
+
* @param bundlerUrl - Pimlico bundler URL for ERC-4337 transactions
|
|
58
|
+
* @param paymasterOptions - Optional paymaster configuration for sponsored transactions
|
|
59
|
+
* @param customContracts - Optional custom contract addresses
|
|
60
|
+
* @param safeModulesVersion - Version of Safe modules (default: '0.3.0')
|
|
61
|
+
*/
|
|
62
|
+
constructor(publicClient, eip1193Provider, safeAddress, bundlerUrl, paymasterOptions, customContracts, safeModulesVersion = '0.3.0') {
|
|
63
|
+
this.publicClient = publicClient;
|
|
64
|
+
this.eip1193Provider = eip1193Provider;
|
|
65
|
+
this.safeAddress = safeAddress;
|
|
66
|
+
this.bundlerUrl = bundlerUrl;
|
|
67
|
+
this.paymasterOptions = paymasterOptions;
|
|
68
|
+
this.customContracts = customContracts;
|
|
69
|
+
this.safeModulesVersion = safeModulesVersion;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Create and initialize a Safe4337Runner in one step
|
|
73
|
+
* @param rpcUrl - The RPC URL to connect to
|
|
74
|
+
* @param eip1193Provider - The EIP-1193 provider from the browser (e.g., window.ethereum)
|
|
75
|
+
* @param safeAddress - The address of the Safe wallet
|
|
76
|
+
* @param chain - The viem chain configuration (e.g., gnosis from 'viem/chains')
|
|
77
|
+
* @param bundlerUrl - Pimlico bundler URL for ERC-4337 transactions
|
|
78
|
+
* @param paymasterOptions - Optional paymaster configuration for sponsored transactions
|
|
79
|
+
* @param customContracts - Optional custom contract addresses
|
|
80
|
+
* @param safeModulesVersion - Version of Safe modules (default: '0.3.0')
|
|
81
|
+
* @returns An initialized Safe4337Runner instance
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { gnosis } from 'viem/chains';
|
|
86
|
+
* import { Safe4337Runner } from '@aboutcircles/sdk-runner';
|
|
87
|
+
*
|
|
88
|
+
* const runner = await Safe4337Runner.create(
|
|
89
|
+
* 'https://rpc.gnosischain.com',
|
|
90
|
+
* window.ethereum,
|
|
91
|
+
* '0xYourSafeAddress...',
|
|
92
|
+
* gnosis,
|
|
93
|
+
* 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY',
|
|
94
|
+
* {
|
|
95
|
+
* isSponsored: true,
|
|
96
|
+
* paymasterUrl: 'https://api.pimlico.io/v2/100/rpc?apikey=YOUR_API_KEY'
|
|
97
|
+
* },
|
|
98
|
+
* {
|
|
99
|
+
* safe4337ModuleAddress: '0x75cf11467937ce3F2f357CE24ffc3DBF8fD5c226'
|
|
100
|
+
* }
|
|
101
|
+
* );
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
static async create(rpcUrl, eip1193Provider, safeAddress, chain, bundlerUrl, paymasterOptions, customContracts, safeModulesVersion = '0.3.0') {
|
|
105
|
+
const publicClient = createPublicClient({
|
|
106
|
+
chain,
|
|
107
|
+
transport: http(rpcUrl),
|
|
108
|
+
});
|
|
109
|
+
const runner = new Safe4337Runner(publicClient, eip1193Provider, safeAddress, bundlerUrl, paymasterOptions, customContracts, safeModulesVersion);
|
|
110
|
+
await runner.init();
|
|
111
|
+
return runner;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Initialize the runner with a Safe address
|
|
115
|
+
* Sets up the Safe4337Pack with bundler and optional paymaster
|
|
116
|
+
* @param safeAddress - The address of the Safe wallet (optional if provided in constructor)
|
|
117
|
+
* @throws {RunnerError} If no Safe address is provided or initialization fails
|
|
118
|
+
*/
|
|
119
|
+
async init(safeAddress) {
|
|
120
|
+
// Use provided address or the one from constructor
|
|
121
|
+
const targetSafeAddress = safeAddress || this.safeAddress;
|
|
122
|
+
if (!targetSafeAddress) {
|
|
123
|
+
throw RunnerError.initializationFailed('Safe4337Runner', new Error('Safe address must be provided either in constructor or init()'));
|
|
124
|
+
}
|
|
125
|
+
if (!this.eip1193Provider) {
|
|
126
|
+
throw RunnerError.initializationFailed('Safe4337Runner', new Error('No EIP-1193 provider available. Make sure you are in a browser environment with a Web3 wallet extension.'));
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
// Get the current signer address from the provider
|
|
130
|
+
const accounts = await this.eip1193Provider.request({
|
|
131
|
+
method: 'eth_requestAccounts'
|
|
132
|
+
});
|
|
133
|
+
const signerAddress = accounts[0];
|
|
134
|
+
if (!signerAddress) {
|
|
135
|
+
throw RunnerError.missingSigner();
|
|
136
|
+
}
|
|
137
|
+
this.safeAddress = targetSafeAddress;
|
|
138
|
+
this.address = targetSafeAddress;
|
|
139
|
+
// Build initialization options
|
|
140
|
+
const initOptions = {
|
|
141
|
+
provider: this.eip1193Provider,
|
|
142
|
+
signer: signerAddress,
|
|
143
|
+
safeModulesVersion: this.safeModulesVersion,
|
|
144
|
+
bundlerUrl: this.bundlerUrl,
|
|
145
|
+
options: {
|
|
146
|
+
safeAddress: targetSafeAddress,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
// Add custom contracts if provided
|
|
150
|
+
if (this.customContracts) {
|
|
151
|
+
initOptions.customContracts = this.customContracts;
|
|
152
|
+
}
|
|
153
|
+
// Add paymaster options if provided
|
|
154
|
+
if (this.paymasterOptions) {
|
|
155
|
+
initOptions.paymasterOptions = this.paymasterOptions;
|
|
156
|
+
}
|
|
157
|
+
// Initialize Safe4337Pack
|
|
158
|
+
this.safe4337 = await Safe4337Pack.init(initOptions);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw RunnerError.initializationFailed('Safe4337Runner', error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Ensures the Safe4337Pack is initialized
|
|
166
|
+
* @private
|
|
167
|
+
*/
|
|
168
|
+
ensureSafe4337() {
|
|
169
|
+
if (!this.safe4337) {
|
|
170
|
+
throw RunnerError.initializationFailed('Safe4337Runner', new Error('Safe4337Runner not initialized. Call init() first.'));
|
|
171
|
+
}
|
|
172
|
+
return this.safe4337;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Estimate gas for a transaction
|
|
176
|
+
*/
|
|
177
|
+
estimateGas = async (tx) => {
|
|
178
|
+
const estimate = await this.publicClient.estimateGas({
|
|
179
|
+
// @ts-expect-error - Address type is compatible with viem's 0x${string}
|
|
180
|
+
account: this.address,
|
|
181
|
+
// @ts-expect-error - Address type is compatible with viem's 0x${string}
|
|
182
|
+
to: tx.to,
|
|
183
|
+
data: tx.data,
|
|
184
|
+
value: tx.value,
|
|
185
|
+
});
|
|
186
|
+
return estimate;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Call a contract (read-only operation)
|
|
190
|
+
*/
|
|
191
|
+
call = async (tx) => {
|
|
192
|
+
const result = await this.publicClient.call({
|
|
193
|
+
// @ts-expect-error - Address type is compatible with viem's 0x${string}
|
|
194
|
+
account: tx.from || this.address,
|
|
195
|
+
// @ts-expect-error - Address type is compatible with viem's 0x${string}
|
|
196
|
+
to: tx.to,
|
|
197
|
+
data: tx.data,
|
|
198
|
+
value: tx.value,
|
|
199
|
+
gas: tx.gas,
|
|
200
|
+
gasPrice: tx.gasPrice,
|
|
201
|
+
});
|
|
202
|
+
return result.data || '0x';
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Resolve an ENS name to an address
|
|
206
|
+
*/
|
|
207
|
+
resolveName = async (name) => {
|
|
208
|
+
try {
|
|
209
|
+
const address = await this.publicClient.getEnsAddress({
|
|
210
|
+
name,
|
|
211
|
+
});
|
|
212
|
+
return address;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// ENS resolution failed or not supported
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Send one or more transactions through Safe 4337 and wait for confirmation
|
|
221
|
+
* All transactions are batched and executed atomically as a UserOperation
|
|
222
|
+
*
|
|
223
|
+
* The transaction will be sponsored if paymaster options were configured
|
|
224
|
+
*
|
|
225
|
+
* @throws {RunnerError} If transaction reverts or execution fails
|
|
226
|
+
*/
|
|
227
|
+
sendTransaction = async (txs) => {
|
|
228
|
+
const safe4337 = this.ensureSafe4337();
|
|
229
|
+
if (txs.length === 0) {
|
|
230
|
+
throw RunnerError.executionFailed('No transactions provided');
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
// Convert to Safe4337 transaction format
|
|
234
|
+
const transactions = txs.map((tx) => ({
|
|
235
|
+
to: tx.to,
|
|
236
|
+
data: tx.data ?? '0x',
|
|
237
|
+
value: (tx.value?.toString() ?? '0'),
|
|
238
|
+
}));
|
|
239
|
+
// Create the SafeOperation with all the transactions
|
|
240
|
+
const safeOperation = await safe4337.createTransaction({
|
|
241
|
+
transactions,
|
|
242
|
+
});
|
|
243
|
+
// Sign the SafeOperation
|
|
244
|
+
const signedSafeOperation = await safe4337.signSafeOperation(safeOperation);
|
|
245
|
+
// Execute and submit to bundler
|
|
246
|
+
const userOperationHash = await safe4337.executeTransaction({
|
|
247
|
+
executable: signedSafeOperation,
|
|
248
|
+
});
|
|
249
|
+
if (!userOperationHash) {
|
|
250
|
+
throw RunnerError.executionFailed('No UserOperation hash returned from Safe 4337 execution');
|
|
251
|
+
}
|
|
252
|
+
// Poll for the user operation receipt
|
|
253
|
+
let userOperationReceipt = null;
|
|
254
|
+
const maxAttempts = 60; // 2 minutes max wait (60 * 2 seconds)
|
|
255
|
+
let attempts = 0;
|
|
256
|
+
while (!userOperationReceipt && attempts < maxAttempts) {
|
|
257
|
+
// Wait 2 seconds before checking the status again
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
259
|
+
userOperationReceipt = await safe4337.getUserOperationReceipt(userOperationHash);
|
|
260
|
+
attempts++;
|
|
261
|
+
}
|
|
262
|
+
if (!userOperationReceipt) {
|
|
263
|
+
throw RunnerError.timeout(userOperationHash, maxAttempts * 2000);
|
|
264
|
+
}
|
|
265
|
+
// Extract transaction hash from UserOperation receipt
|
|
266
|
+
const txHash = userOperationReceipt.receipt?.transactionHash;
|
|
267
|
+
if (!txHash) {
|
|
268
|
+
throw RunnerError.executionFailed('No transaction hash in UserOperation receipt');
|
|
269
|
+
}
|
|
270
|
+
// Get the full transaction receipt
|
|
271
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({
|
|
272
|
+
hash: txHash,
|
|
273
|
+
});
|
|
274
|
+
// Check transaction status and throw if reverted
|
|
275
|
+
if (receipt.status === 'reverted') {
|
|
276
|
+
throw RunnerError.transactionReverted(receipt.transactionHash, receipt.blockNumber, receipt.gasUsed);
|
|
277
|
+
}
|
|
278
|
+
return receipt;
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (error instanceof RunnerError) {
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
throw RunnerError.executionFailed('Safe 4337 transaction failed', error);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/**
|
|
288
|
+
* Create a batch transaction runner
|
|
289
|
+
* @returns A Safe4337BatchRun instance for batching multiple transactions
|
|
290
|
+
*/
|
|
291
|
+
sendBatchTransaction = () => {
|
|
292
|
+
const safe4337 = this.ensureSafe4337();
|
|
293
|
+
return new Safe4337BatchRun(safe4337, this.publicClient);
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Batch transaction runner for Safe 4337 operations
|
|
298
|
+
* Allows multiple transactions to be batched and executed together as a UserOperation
|
|
299
|
+
*/
|
|
300
|
+
export class Safe4337BatchRun {
|
|
301
|
+
safe4337;
|
|
302
|
+
publicClient;
|
|
303
|
+
transactions = [];
|
|
304
|
+
constructor(safe4337, publicClient) {
|
|
305
|
+
this.safe4337 = safe4337;
|
|
306
|
+
this.publicClient = publicClient;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Add a transaction to the batch
|
|
310
|
+
*/
|
|
311
|
+
addTransaction(tx) {
|
|
312
|
+
this.transactions.push(tx);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Execute all batched transactions as a sponsored UserOperation
|
|
316
|
+
* Waits for the UserOperation to be included and confirmed on-chain
|
|
317
|
+
*
|
|
318
|
+
* @throws {RunnerError} If transaction reverts or execution fails
|
|
319
|
+
*/
|
|
320
|
+
async run() {
|
|
321
|
+
if (this.transactions.length === 0) {
|
|
322
|
+
throw RunnerError.executionFailed('No transactions in batch');
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
// Convert to Safe4337 transaction format
|
|
326
|
+
const transactions = this.transactions.map((tx) => ({
|
|
327
|
+
to: tx.to,
|
|
328
|
+
data: tx.data ?? '0x',
|
|
329
|
+
value: (tx.value?.toString() ?? '0'),
|
|
330
|
+
}));
|
|
331
|
+
// Create the SafeOperation with all the transactions
|
|
332
|
+
const safeOperation = await this.safe4337.createTransaction({
|
|
333
|
+
transactions,
|
|
334
|
+
});
|
|
335
|
+
// Sign the SafeOperation
|
|
336
|
+
const signedSafeOperation = await this.safe4337.signSafeOperation(safeOperation);
|
|
337
|
+
// Execute and submit to bundler
|
|
338
|
+
const userOperationHash = await this.safe4337.executeTransaction({
|
|
339
|
+
executable: signedSafeOperation,
|
|
340
|
+
});
|
|
341
|
+
if (!userOperationHash) {
|
|
342
|
+
throw RunnerError.executionFailed('No UserOperation hash returned from Safe 4337 execution');
|
|
343
|
+
}
|
|
344
|
+
// Poll for the user operation receipt
|
|
345
|
+
let userOperationReceipt = null;
|
|
346
|
+
const maxAttempts = 60; // 2 minutes max wait
|
|
347
|
+
let attempts = 0;
|
|
348
|
+
while (!userOperationReceipt && attempts < maxAttempts) {
|
|
349
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
350
|
+
userOperationReceipt = await this.safe4337.getUserOperationReceipt(userOperationHash);
|
|
351
|
+
attempts++;
|
|
352
|
+
}
|
|
353
|
+
if (!userOperationReceipt) {
|
|
354
|
+
throw RunnerError.timeout(userOperationHash, maxAttempts * 2000);
|
|
355
|
+
}
|
|
356
|
+
// Extract transaction hash from UserOperation receipt
|
|
357
|
+
const txHash = userOperationReceipt.receipt?.transactionHash;
|
|
358
|
+
if (!txHash) {
|
|
359
|
+
throw RunnerError.executionFailed('No transaction hash in UserOperation receipt');
|
|
360
|
+
}
|
|
361
|
+
// Get the full transaction receipt
|
|
362
|
+
const receipt = await this.publicClient.waitForTransactionReceipt({
|
|
363
|
+
hash: txHash,
|
|
364
|
+
});
|
|
365
|
+
// Check transaction status and throw if reverted
|
|
366
|
+
if (receipt.status === 'reverted') {
|
|
367
|
+
throw RunnerError.transactionReverted(receipt.transactionHash, receipt.blockNumber, receipt.gasUsed);
|
|
368
|
+
}
|
|
369
|
+
return receipt;
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (error instanceof RunnerError) {
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
throw RunnerError.executionFailed('Safe 4337 batch transaction failed', error);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|