@aboutcircles/sdk-runner 0.1.0

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.
@@ -0,0 +1,237 @@
1
+ import { createPublicClient, http } from 'viem';
2
+ import { OperationType } from '@safe-global/safe-core-sdk-types';
3
+ import { RunnerError } from './errors';
4
+ // Use require for Safe to ensure compatibility with bun's CJS/ESM interop
5
+ // Safe Protocol Kit v5 uses CommonJS exports, so we use require() for proper interop
6
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
7
+ const SafeModule = require('@safe-global/protocol-kit');
8
+ const Safe = SafeModule.default || SafeModule;
9
+ /**
10
+ * Batch transaction runner for Safe
11
+ * Allows multiple transactions to be batched and executed together
12
+ */
13
+ export class SafeBatchRun {
14
+ safe;
15
+ publicClient;
16
+ transactions = [];
17
+ constructor(safe, publicClient) {
18
+ this.safe = safe;
19
+ this.publicClient = publicClient;
20
+ }
21
+ /**
22
+ * Add a transaction to the batch
23
+ */
24
+ addTransaction(tx) {
25
+ this.transactions.push(tx);
26
+ }
27
+ /**
28
+ * Get the Safe transaction data for all batched transactions
29
+ */
30
+ async getSafeTransaction() {
31
+ const metaTransactions = this.transactions.map((tx) => ({
32
+ operation: OperationType.Call,
33
+ to: tx.to,
34
+ value: (tx.value?.toString() ?? '0'),
35
+ data: tx.data ?? '0x',
36
+ }));
37
+ const safeTransaction = await this.safe.createTransaction({
38
+ transactions: metaTransactions,
39
+ });
40
+ return safeTransaction;
41
+ }
42
+ /**
43
+ * Execute all batched transactions and wait for confirmation
44
+ * @throws {RunnerError} If transaction reverts or execution fails
45
+ */
46
+ async run() {
47
+ const safeTransaction = await this.getSafeTransaction();
48
+ const txResult = await this.safe.executeTransaction(safeTransaction);
49
+ if (!txResult.hash) {
50
+ throw RunnerError.executionFailed('No transaction hash returned from Safe execution');
51
+ }
52
+ // Wait for transaction receipt
53
+ const receipt = await this.publicClient.waitForTransactionReceipt({
54
+ hash: txResult.hash,
55
+ });
56
+ // Check transaction status and throw if reverted
57
+ if (receipt.status === 'reverted') {
58
+ throw RunnerError.transactionReverted(receipt.transactionHash, receipt.blockNumber, receipt.gasUsed);
59
+ }
60
+ // Return viem's TransactionReceipt directly
61
+ return receipt;
62
+ }
63
+ }
64
+ /**
65
+ * Safe contract runner implementation using Safe Protocol Kit
66
+ * Executes transactions through a Safe multisig wallet
67
+ */
68
+ export class SafeContractRunner {
69
+ address;
70
+ publicClient;
71
+ privateKey;
72
+ rpcUrl;
73
+ safeAddress;
74
+ safe;
75
+ /**
76
+ * Creates a new SafeContractRunner
77
+ * @param publicClient - The viem public client for reading blockchain state
78
+ * @param privateKey - The private key of one of the Safe signers
79
+ * @param rpcUrl - The RPC URL to use for Safe operations
80
+ * @param safeAddress - The address of the Safe wallet (optional, can be set in init)
81
+ */
82
+ // @todo rpc might be taken from public client
83
+ constructor(publicClient, privateKey, rpcUrl, safeAddress) {
84
+ this.publicClient = publicClient;
85
+ this.privateKey = privateKey;
86
+ this.rpcUrl = rpcUrl;
87
+ this.safeAddress = safeAddress;
88
+ }
89
+ /**
90
+ * Create and initialize a SafeContractRunner in one step
91
+ * @param rpcUrl - The RPC URL to connect to
92
+ * @param privateKey - The private key of one of the Safe signers
93
+ * @param safeAddress - The address of the Safe wallet
94
+ * @param chain - The viem chain configuration (e.g., gnosis from 'viem/chains')
95
+ * @returns An initialized SafeContractRunner instance
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * import { gnosis } from 'viem/chains';
100
+ * import { SafeContractRunner } from '@aboutcircles/sdk-runner';
101
+ *
102
+ * const runner = await SafeContractRunner.create(
103
+ * 'https://rpc.gnosischain.com',
104
+ * '0xYourPrivateKey...',
105
+ * '0xYourSafeAddress...',
106
+ * gnosis
107
+ * );
108
+ * ```
109
+ */
110
+ static async create(rpcUrl, privateKey, safeAddress, chain) {
111
+ const publicClient = createPublicClient({
112
+ chain,
113
+ transport: http(rpcUrl),
114
+ });
115
+ const runner = new SafeContractRunner(publicClient, privateKey, rpcUrl, safeAddress);
116
+ await runner.init();
117
+ return runner;
118
+ }
119
+ /**
120
+ * Initialize the runner with a Safe address
121
+ * @param safeAddress - The address of the Safe wallet (optional if provided in constructor)
122
+ */
123
+ async init(safeAddress) {
124
+ // Use provided address or the one from constructor
125
+ const targetSafeAddress = safeAddress || this.safeAddress;
126
+ if (!targetSafeAddress) {
127
+ throw new Error('Safe address must be provided either in constructor or init()');
128
+ }
129
+ this.safeAddress = targetSafeAddress;
130
+ this.address = targetSafeAddress;
131
+ // Initialize Safe Protocol Kit
132
+ this.safe = await Safe.init({
133
+ provider: this.rpcUrl,
134
+ signer: this.privateKey,
135
+ safeAddress: targetSafeAddress,
136
+ });
137
+ }
138
+ /**
139
+ * Ensures the Safe is initialized
140
+ */
141
+ ensureSafe() {
142
+ if (!this.safe) {
143
+ throw new Error('SafeContractRunner not initialized. Call init() first.');
144
+ }
145
+ return this.safe;
146
+ }
147
+ /**
148
+ * Estimate gas for a transaction
149
+ */
150
+ estimateGas = async (tx) => {
151
+ const estimate = await this.publicClient.estimateGas({
152
+ // @ts-expect-error - Address type is compatible with viem's 0x${string}
153
+ account: this.address,
154
+ // @ts-expect-error - Address type is compatible with viem's 0x${string}
155
+ to: tx.to,
156
+ data: tx.data,
157
+ value: tx.value,
158
+ });
159
+ return estimate;
160
+ };
161
+ /**
162
+ * Call a contract (read-only operation)
163
+ */
164
+ call = async (tx) => {
165
+ const result = await this.publicClient.call({
166
+ // @ts-expect-error - Address type is compatible with viem's 0x${string}
167
+ account: tx.from || this.address,
168
+ // @ts-expect-error - Address type is compatible with viem's 0x${string}
169
+ to: tx.to,
170
+ data: tx.data,
171
+ value: tx.value,
172
+ gas: tx.gas,
173
+ gasPrice: tx.gasPrice,
174
+ });
175
+ return result.data || '0x';
176
+ };
177
+ /**
178
+ * Resolve an ENS name to an address
179
+ */
180
+ resolveName = async (name) => {
181
+ try {
182
+ const address = await this.publicClient.getEnsAddress({
183
+ name,
184
+ });
185
+ return address;
186
+ }
187
+ catch (error) {
188
+ // ENS resolution failed or not supported
189
+ return null;
190
+ }
191
+ };
192
+ /**
193
+ * Send one or more transactions through the Safe and wait for confirmation
194
+ * All transactions are batched and executed atomically
195
+ *
196
+ * @throws {RunnerError} If transaction reverts or execution fails
197
+ */
198
+ sendTransaction = async (txs) => {
199
+ const safe = this.ensureSafe();
200
+ if (txs.length === 0) {
201
+ throw RunnerError.executionFailed('No transactions provided');
202
+ }
203
+ const metaTransactions = txs.map((tx) => ({
204
+ operation: OperationType.Call,
205
+ to: tx.to,
206
+ value: (tx.value?.toString() ?? '0'),
207
+ data: tx.data ?? '0x',
208
+ }));
209
+ // Create Safe transaction with all transactions
210
+ const safeTransaction = await safe.createTransaction({
211
+ transactions: metaTransactions,
212
+ });
213
+ // Execute the batched transaction
214
+ const txResult = await safe.executeTransaction(safeTransaction);
215
+ if (!txResult.hash) {
216
+ throw RunnerError.executionFailed('No transaction hash returned from Safe execution');
217
+ }
218
+ // Wait for transaction receipt
219
+ const receipt = await this.publicClient.waitForTransactionReceipt({
220
+ hash: txResult.hash,
221
+ });
222
+ // Check transaction status and throw if reverted
223
+ if (receipt.status === 'reverted') {
224
+ throw RunnerError.transactionReverted(receipt.transactionHash, receipt.blockNumber, receipt.gasUsed);
225
+ }
226
+ // Return viem's TransactionReceipt directly
227
+ return receipt;
228
+ };
229
+ /**
230
+ * Create a batch transaction runner
231
+ * @returns A SafeBatchRun instance for batching multiple transactions
232
+ */
233
+ sendBatchTransaction = () => {
234
+ const safe = this.ensureSafe();
235
+ return new SafeBatchRun(safe, this.publicClient);
236
+ };
237
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@aboutcircles/sdk-runner",
3
+ "version": "0.1.0",
4
+ "description": "Contract runner implementations for Circles SDK",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "bun build ./src/index.ts --outdir ./dist --format esm && tsc --emitDeclarationOnly",
16
+ "dev": "tsc --build --watch",
17
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "circles",
24
+ "runner",
25
+ "ethereum",
26
+ "viem"
27
+ ],
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@aboutcircles/sdk-types": "*",
31
+ "@safe-global/protocol-kit": "^5.1.1",
32
+ "@safe-global/safe-core-sdk-types": "^5.1.0",
33
+ "viem": "^2.38.0"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.0.4"
37
+ }
38
+ }