@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.
- package/README.md +106 -0
- package/dist/errors.d.ts +48 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +89 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +147349 -0
- package/dist/runner.d.ts +3 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +1 -0
- package/dist/safe-browser-runner.d.ts +130 -0
- package/dist/safe-browser-runner.d.ts.map +1 -0
- package/dist/safe-browser-runner.js +265 -0
- package/dist/safe-runner.d.ts +102 -0
- package/dist/safe-runner.d.ts.map +1 -0
- package/dist/safe-runner.js +237 -0
- package/package.json +38 -0
|
@@ -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
|
+
}
|