sdk-triggerx 0.1.26 → 0.1.28

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 CHANGED
@@ -12,6 +12,10 @@ Supports job automation on EVM-compatible chains using time, event, or condition
12
12
  - 📅 Supports time-based, event-based, and condition-based jobs
13
13
  - 🔐 TypeScript support with clean types
14
14
  - 🧠 Dynamic argument fetching via external scripts (e.g., IPFS)
15
+ - 🛡️ Safe (Gnosis Safe) wallet integration with support for:
16
+ - Static single transactions (e.g., ETH transfers)
17
+ - Static batch transactions (e.g., DeFi operations like token approvals + swaps)
18
+ - Dynamic runtime arguments via IPFS scripts
15
19
 
16
20
  ---
17
21
 
@@ -57,9 +61,11 @@ const client = new TriggerXClient('YOUR_API_KEY');
57
61
  - Requirements:
58
62
  - Safe threshold must be 1 (single-signer) in this implementation.
59
63
  - The signer must be an owner of the Safe.
60
- - Parameters must be dynamic (provided by `dynamicArgumentsScriptUrl`).
61
- - The SDK will auto-create a Safe if `safeAddress` is not provided and the chain is configured with a Safe Factory.
64
+ - Supports both static transactions (`safeTransactions` array) and dynamic arguments (`dynamicArgumentsScriptUrl`).
62
65
  - The SDK will auto-enable the TriggerX Safe Module on the Safe if not already enabled.
66
+ - Static transactions:
67
+ - Single transaction: Provide one transaction in `safeTransactions` array (operation: CALL).
68
+ - Batch transactions: Provide multiple transactions in `safeTransactions` array (uses Safe MultiSend with DELEGATECALL).
63
69
 
64
70
  #### Supported Condition Types (for `conditionType`)
65
71
 
@@ -128,11 +134,11 @@ const jobInput = {
128
134
 
129
135
  // Safe mode (no target required — SDK auto-sets module target/function/ABI)
130
136
  walletMode: 'safe',
131
- // Optional: provide an existing Safe; otherwise the SDK will create one for you
132
- // safeAddress: '0xYourSafeAddress',
137
+ safeAddress: '0xYourSafeAddress', // Required: provide your Safe address
133
138
 
134
- // Dynamic params must come from an IPFS/URL script
139
+ // Dynamic params fetched from IPFS/URL script at execution time
135
140
  dynamicArgumentsScriptUrl: 'https://your-ipfs-gateway/ipfs/your-hash',
141
+ language:'', //Your code language exmampel-> language:'go',
136
142
 
137
143
  // Optional helper to auto-top up TG if low
138
144
  autotopupTG: true,
@@ -144,8 +150,8 @@ console.log(result);
144
150
 
145
151
  Notes for Safe wallet mode:
146
152
  - In Safe mode, you do NOT need to set `targetContractAddress`/`targetFunction`/`abi` — the SDK sets these for the Safe Module and uses `execJobFromHub(address,address,uint256,bytes,uint8)` under the hood.
147
- - Your action details (action target/value/data/op) must be produced by your IPFS script at execution time.
148
- - No static arguments are allowed in Safe mode.
153
+ - For dynamic jobs: your action details (action target/value/data/op) must be produced by your IPFS script at execution time.
154
+ - For static jobs: use the `safeTransactions` array to provide hardcoded transaction details (see Safe Wallet Flow section below).
149
155
 
150
156
  ---
151
157
 
@@ -172,6 +178,7 @@ const jobInput = {
172
178
 
173
179
  arguments: [], // Target function args as strings
174
180
  dynamicArgumentsScriptUrl: 'https://your-ipfs-url', // Script URL for dynamic args
181
+ language:'', //Your code language exmampel-> language:'go',
175
182
  isImua: false, // Optional feature flag
176
183
  autotopupTG: true, // Auto top-up TG if balance is low
177
184
  };
@@ -224,7 +231,7 @@ To create jobs that use Safe wallets (`walletMode: 'safe'`), you must first crea
224
231
  #### 1️⃣ Create a Safe wallet for your user
225
232
 
226
233
  ```ts
227
- import { createSafeWallet } from 'sdk-triggerx/api/safeWallet'; // <--- new dedicated helper!
234
+ import { createSafeWallet } from 'sdk-triggerx/api/safeWallet';
228
235
  const safeAddress = await createSafeWallet(signer /* ethers.Signer */);
229
236
  console.log('Your Safe address:', safeAddress);
230
237
  ```
@@ -234,29 +241,133 @@ console.log('Your Safe address:', safeAddress);
234
241
  - This MUST be done before you attempt to create any job with `walletMode: 'safe'`.
235
242
  - All safe wallet creation helpers are now in the dedicated `api/safeWallet` module so your file structure stays clean.
236
243
 
237
- #### 2️⃣ Create a job using the Safe (walletMode: 'safe')
244
+ #### 2️⃣ Create a job using the Safe
245
+
246
+ Safe wallet jobs support two modes: **static transactions** and **dynamic arguments**.
247
+
248
+ ##### Option A: Static Single Transaction (e.g., ETH transfer)
238
249
 
239
250
  ```ts
240
251
  const jobInput = {
241
252
  jobType: JobType.Time,
242
- argType: ArgType.Dynamic, // required in safe mode
243
- jobTitle: 'Safe Job',
253
+ argType: ArgType.Static, // Static for hardcoded transactions
254
+ jobTitle: 'Safe ETH Transfer',
244
255
  timeFrame: 3600,
245
256
  scheduleType: 'interval',
246
257
  timeInterval: 300,
247
258
  timezone: 'UTC',
248
259
  chainId: '421614',
249
260
  walletMode: 'safe',
250
- safeAddress, // <---- required and must come from step 1
261
+ safeAddress: '0xYourSafeAddress', // from step 1
262
+ safeTransactions: [
263
+ {
264
+ to: '0xRecipientAddress',
265
+ value: '10000000000000', // 0.00001 ETH in wei
266
+ data: '0x' // empty for simple ETH transfer
267
+ }
268
+ ],
269
+ autotopupTG: true,
270
+ };
271
+
272
+ await createJob(client, { jobInput, signer });
273
+ ```
274
+
275
+ ##### Option B: Static Batch Transactions (e.g., Uniswap Swap)
276
+
277
+ For complex multi-step operations like token approvals + swaps:
278
+
279
+ ```ts
280
+ import { ethers } from 'ethers';
281
+
282
+ // Token addresses (Arbitrum Sepolia)
283
+ const USDC = '0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d';
284
+ const WETH = '0x980B62Da83eFf3D4576C647993b0c1D7faf17c73';
285
+ const UNISWAP_ROUTER = '0x101F443B4d1b059569D643917553c771E1b9663E';
286
+
287
+ // Encode approve transaction
288
+ const erc20Interface = new ethers.Interface([
289
+ 'function approve(address spender, uint256 amount) returns (bool)'
290
+ ]);
291
+ const approveData = erc20Interface.encodeFunctionData('approve', [
292
+ UNISWAP_ROUTER,
293
+ '10000' // 0.01 USDC (6 decimals)
294
+ ]);
295
+
296
+ // Encode Uniswap V3 swap transaction
297
+ const swapInterface = new ethers.Interface([
298
+ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) payable returns (uint256 amountOut)'
299
+ ]);
300
+ const swapData = swapInterface.encodeFunctionData('exactInputSingle', [{
301
+ tokenIn: USDC,
302
+ tokenOut: WETH,
303
+ fee: 3000, // 0.3%
304
+ recipient: safeAddress, // Safe receives the output
305
+ amountIn: '10000',
306
+ amountOutMinimum: '0',
307
+ sqrtPriceLimitX96: 0
308
+ }]);
309
+
310
+ const jobInput = {
311
+ jobType: JobType.Time,
312
+ argType: ArgType.Static,
313
+ jobTitle: 'Safe Uniswap Swap',
314
+ timeFrame: 3600,
315
+ scheduleType: 'interval',
316
+ timeInterval: 600,
317
+ timezone: 'UTC',
318
+ chainId: '421614',
319
+ walletMode: 'safe',
320
+ safeAddress: '0xYourSafeAddress',
321
+ safeTransactions: [
322
+ {
323
+ to: USDC, // Approve USDC to router
324
+ value: '0',
325
+ data: approveData
326
+ },
327
+ {
328
+ to: UNISWAP_ROUTER, // Execute swap
329
+ value: '0',
330
+ data: swapData
331
+ }
332
+ ],
333
+ autotopupTG: true,
334
+ };
335
+
336
+ await createJob(client, { jobInput, signer });
337
+ ```
338
+
339
+ **Note:** When multiple transactions are provided in `safeTransactions`, the SDK automatically:
340
+ - Encodes them using Safe's MultiSend format
341
+ - Wraps them with the `multiSend(bytes)` function call
342
+ - Sets the operation to DELEGATECALL for batch execution
343
+
344
+ ##### Option C: Dynamic Arguments
345
+
346
+ For runtime-determined parameters:
347
+
348
+ ```ts
349
+ const jobInput = {
350
+ jobType: JobType.Time,
351
+ argType: ArgType.Dynamic,
352
+ jobTitle: 'Dynamic Safe Job',
353
+ timeFrame: 3600,
354
+ scheduleType: 'interval',
355
+ timeInterval: 300,
356
+ timezone: 'UTC',
357
+ chainId: '421614',
358
+ walletMode: 'safe',
359
+ safeAddress: '0xYourSafeAddress',
251
360
  dynamicArgumentsScriptUrl: 'https://your-ipfs-gateway/ipfs/your-hash',
361
+ language:'go', //Your code language exmampel-> language:'go',
252
362
  autotopupTG: true,
253
363
  };
254
- // ...
255
- await createJob(client, { jobInput, signer }); // as normal
364
+
365
+ await createJob(client, { jobInput, signer });
256
366
  ```
257
367
 
258
- - The `safeAddress` property is now required for jobs using `walletMode: 'safe'`.
259
- - The SDK will no longer auto-create a Safe wallet for you; you must explicitly create and pass it in your job input.
368
+ - The `safeAddress` property is required for all Safe wallet jobs.
369
+ - For static jobs, provide `safeTransactions` array with transaction details.
370
+ - For dynamic jobs, provide `dynamicArgumentsScriptUrl` that returns the action parameters at execution time.
260
371
 
261
372
  ---
262
373
 
@@ -337,6 +448,14 @@ Includes:
337
448
  - `JobInput`, `JobType`, `ArgType`
338
449
  - `ConditionType`, `ScheduleType`
339
450
  - `UserData`, `JobData`, etc.
451
+ - `SafeTransaction` - Interface for Safe wallet transaction objects:
452
+ ```ts
453
+ interface SafeTransaction {
454
+ to: string; // Target contract address
455
+ value: string; // Value in wei (as string)
456
+ data: string; // Encoded function call data (hex with 0x prefix)
457
+ }
458
+ ```
340
459
 
341
460
  ---
342
461
 
@@ -1,5 +1,13 @@
1
1
  import { ethers } from 'ethers';
2
- export declare const checkTgBalance: (signer: ethers.Signer) => Promise<{
2
+ /**
3
+ * Check TG balance for a given signer using SDK-provided RPC
4
+ * This function uses our own RPC provider to ensure reliable connection
5
+ * even if the user's RPC fails
6
+ * @param signer - ethers.Signer instance (used to get wallet address)
7
+ * @param chainId - Optional chain ID. If not provided, will try to get from signer's provider
8
+ * @returns Balance information or error response
9
+ */
10
+ export declare const checkTgBalance: (signer: ethers.Signer, chainId?: string | number) => Promise<{
3
11
  success: boolean;
4
12
  data?: {
5
13
  tgBalanceWei: bigint;
@@ -8,21 +8,53 @@ const ethers_1 = require("ethers");
8
8
  const GasRegistry_json_1 = __importDefault(require("../contracts/abi/GasRegistry.json"));
9
9
  const config_1 = require("../config");
10
10
  const errors_1 = require("../utils/errors");
11
- const checkTgBalance = async (signer) => {
11
+ /**
12
+ * Check TG balance for a given signer using SDK-provided RPC
13
+ * This function uses our own RPC provider to ensure reliable connection
14
+ * even if the user's RPC fails
15
+ * @param signer - ethers.Signer instance (used to get wallet address)
16
+ * @param chainId - Optional chain ID. If not provided, will try to get from signer's provider
17
+ * @returns Balance information or error response
18
+ */
19
+ const checkTgBalance = async (signer, chainId) => {
12
20
  // Validate inputs
13
21
  if (!signer) {
14
22
  return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('signer', 'Signer is required'), 'Validation error');
15
23
  }
16
24
  try {
17
- const network = await signer.provider?.getNetwork();
18
- const chainId = network?.chainId ? network.chainId.toString() : undefined;
19
- const { gasRegistry } = (0, config_1.getChainAddresses)(chainId);
25
+ // Try to get chainId from signer's provider if not provided
26
+ // If signer's provider fails, we'll use the provided chainId or return error
27
+ let resolvedChainId = chainId?.toString();
28
+ if (!resolvedChainId) {
29
+ try {
30
+ const network = await signer.provider?.getNetwork();
31
+ resolvedChainId = network?.chainId ? network.chainId.toString() : undefined;
32
+ }
33
+ catch (providerError) {
34
+ // If user's RPC fails, we can't get chainId from it
35
+ // This is expected in cases where user's RPC is down
36
+ console.warn('Could not get network from signer provider, using provided chainId or will fail:', providerError);
37
+ }
38
+ }
39
+ if (!resolvedChainId) {
40
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError('Chain ID is required. Please provide chainId parameter or ensure signer has a working provider.'), 'Configuration error');
41
+ }
42
+ const { gasRegistry } = (0, config_1.getChainAddresses)(resolvedChainId);
20
43
  const gasRegistryContractAddress = gasRegistry;
21
44
  if (!gasRegistryContractAddress) {
22
- return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`GasRegistry address not configured for chain ID: ${chainId}`), 'Configuration error');
45
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`GasRegistry address not configured for chain ID: ${resolvedChainId}`), 'Configuration error');
46
+ }
47
+ // Use SDK-provided RPC provider instead of user's provider
48
+ // This ensures we can read balance even if user's RPC fails
49
+ const rpcProvider = (0, config_1.getRpcProvider)(resolvedChainId);
50
+ if (!rpcProvider) {
51
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`RPC URL not configured for chain ID: ${resolvedChainId}. Cannot check balance without RPC provider.`), 'Configuration error');
23
52
  }
24
- const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, signer);
53
+ // Create contract instance with our RPC provider (read-only)
54
+ const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, rpcProvider);
55
+ // Get address from signer (this doesn't require provider)
25
56
  const address = await signer.getAddress();
57
+ // Read balance using our RPC provider
26
58
  const balance = await contract.balances(address);
27
59
  // balance is likely an array or object with ethSpent and TGbalance, both in wei
28
60
  // We'll convert TGbalance from wei to ETH
package/dist/api/jobs.js CHANGED
@@ -16,11 +16,44 @@ const config_1 = require("../config");
16
16
  const validation_1 = require("../utils/validation");
17
17
  const errors_1 = require("../utils/errors");
18
18
  const SafeWallet_1 = require("../contracts/safe/SafeWallet");
19
- const JOB_ID = '300949528249665590178224313442040528409305273634097553067152835846309150732';
20
- const DYNAMIC_ARGS_URL = 'https://teal-random-koala-993.mypinata.cloud/ipfs/bafkreif426p7t7takzhw3g6we2h6wsvf27p5jxj3gaiynqf22p3jvhx4la';
19
+ // Helper function to encode multisend batch transactions
20
+ function encodeMultisendData(transactions) {
21
+ // Multisend format: for each transaction, encode as:
22
+ // operation (1 byte) + to (20 bytes) + value (32 bytes) + dataLength (32 bytes) + data (variable)
23
+ let encodedTransactions = '';
24
+ for (const tx of transactions) {
25
+ const txWithOperation = tx;
26
+ const to = txWithOperation.to;
27
+ const value = ethers_1.ethers.toBigInt(txWithOperation.value);
28
+ const data = txWithOperation.data;
29
+ // Remove 0x prefix from data if present
30
+ const dataWithoutPrefix = data.startsWith('0x') ? data.slice(2) : data;
31
+ const dataLength = ethers_1.ethers.toBigInt(dataWithoutPrefix.length / 2);
32
+ // Encode each field and concatenate
33
+ // operation: uint8 (1 byte)
34
+ const operation = typeof txWithOperation.operation === 'number' ? txWithOperation.operation : 0;
35
+ if (operation < 0 || operation > 1) {
36
+ throw new Error(`Invalid Safe transaction operation: ${operation}. Expected 0 (CALL) or 1 (DELEGATECALL).`);
37
+ }
38
+ const operationHex = operation.toString(16).padStart(2, '0');
39
+ // to: address (20 bytes)
40
+ const toHex = to.toLowerCase().replace(/^0x/, '').padStart(40, '0');
41
+ // value: uint256 (32 bytes)
42
+ const valueHex = value.toString(16).padStart(64, '0');
43
+ // dataLength: uint256 (32 bytes)
44
+ const dataLengthHex = dataLength.toString(16).padStart(64, '0');
45
+ // data: bytes (variable length)
46
+ encodedTransactions += operationHex + toHex + valueHex + dataLengthHex + dataWithoutPrefix;
47
+ }
48
+ const packedTransactions = `0x${encodedTransactions}`;
49
+ const multiSendInterface = new ethers_1.ethers.Interface([
50
+ 'function multiSend(bytes transactions)'
51
+ ]);
52
+ return multiSendInterface.encodeFunctionData('multiSend', [packedTransactions]);
53
+ }
21
54
  function toCreateJobDataFromTime(input, balances, userAddress, jobCostPrediction) {
22
55
  return {
23
- job_id: JOB_ID,
56
+ job_id: "",
24
57
  user_address: userAddress,
25
58
  ether_balance: balances.etherBalance,
26
59
  token_balance: balances.tokenBalanceWei,
@@ -43,16 +76,16 @@ function toCreateJobDataFromTime(input, balances, userAddress, jobCostPrediction
43
76
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
44
77
  arguments: input.arguments,
45
78
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
46
- is_imua: input.isImua ?? true,
79
+ is_imua: input.isImua ?? false,
47
80
  is_safe: input.walletMode === 'safe',
48
81
  safe_name: input.safeName || '',
49
82
  safe_address: input.safeAddress || '',
50
- language: input.language || '',
83
+ language: input.language || 'go',
51
84
  };
52
85
  }
53
86
  function toCreateJobDataFromEvent(input, balances, userAddress, jobCostPrediction) {
54
87
  return {
55
- job_id: JOB_ID,
88
+ job_id: "",
56
89
  user_address: userAddress,
57
90
  ether_balance: balances.etherBalance,
58
91
  token_balance: balances.tokenBalanceWei,
@@ -74,16 +107,16 @@ function toCreateJobDataFromEvent(input, balances, userAddress, jobCostPredictio
74
107
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
75
108
  arguments: input.arguments,
76
109
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
77
- is_imua: input.isImua ?? true,
110
+ is_imua: input.isImua ?? false,
78
111
  is_safe: input.walletMode === 'safe',
79
112
  safe_name: input.safeName || '',
80
113
  safe_address: input.safeAddress || '',
81
- language: input.language || '',
114
+ language: input.language || 'go',
82
115
  };
83
116
  }
84
117
  function toCreateJobDataFromCondition(input, balances, userAddress, jobCostPrediction) {
85
118
  return {
86
- job_id: JOB_ID,
119
+ job_id: "",
87
120
  user_address: userAddress,
88
121
  ether_balance: balances.etherBalance,
89
122
  token_balance: balances.tokenBalanceWei,
@@ -107,11 +140,11 @@ function toCreateJobDataFromCondition(input, balances, userAddress, jobCostPredi
107
140
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
108
141
  arguments: input.arguments,
109
142
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
110
- is_imua: input.isImua ?? true,
143
+ is_imua: input.isImua ?? false,
111
144
  is_safe: input.walletMode === 'safe',
112
145
  safe_name: input.safeName || '',
113
146
  safe_address: input.safeAddress || '',
114
- language: input.language || '',
147
+ language: input.language || 'go',
115
148
  };
116
149
  }
117
150
  // --- Encoding helpers for different job types ---
@@ -156,7 +189,7 @@ async function createJob(client, params) {
156
189
  return (0, errors_1.createErrorResponse)(new errors_1.NetworkError('Failed to get network information', { originalError: err }), 'Network error');
157
190
  }
158
191
  const chainIdStr = network?.chainId ? network.chainId.toString() : undefined;
159
- const { jobRegistry, safeModule, safeFactory } = (0, config_1.getChainAddresses)(chainIdStr);
192
+ const { jobRegistry, safeModule, safeFactory, multisendCallOnly } = (0, config_1.getChainAddresses)(chainIdStr);
160
193
  const JOB_REGISTRY_ADDRESS = jobRegistry;
161
194
  if (!JOB_REGISTRY_ADDRESS) {
162
195
  return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`JobRegistry address not configured for chain ID: ${chainIdStr}`), 'Configuration error');
@@ -171,10 +204,6 @@ async function createJob(client, params) {
171
204
  if (!jobInput.safeAddress || typeof jobInput.safeAddress !== 'string' || !jobInput.safeAddress.trim()) {
172
205
  return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('safeAddress', 'safeAddress is required when walletMode is "safe". Call createSafeWallet first.'), 'Validation error');
173
206
  }
174
- const dynUrl = jobInput.dynamicArgumentsScriptUrl;
175
- if (!dynUrl) {
176
- return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('dynamicArgumentsScriptUrl', 'Safe jobs require dynamicArgumentsScriptUrl (IPFS) for parameters.'), 'Validation error');
177
- }
178
207
  // Validate Safe has single owner and owner matches signer
179
208
  try {
180
209
  await (0, SafeWallet_1.ensureSingleOwnerAndMatchSigner)(jobInput.safeAddress, signer.provider, await signer.getAddress());
@@ -186,20 +215,67 @@ async function createJob(client, params) {
186
215
  }
187
216
  // Auto-set module target; user does not need to pass targetContractAddress in safe mode
188
217
  jobInput.targetContractAddress = safeModule;
189
- jobInput.targetFunction = 'execJobFromHub(address,address,uint256,bytes,uint8)';
218
+ // Function signature must match exactly as in ABI
219
+ jobInput.targetFunction = 'execJobFromHub';
220
+ // ABI verified per provided interface and matches execJobFromHub
190
221
  jobInput.abi = JSON.stringify([
191
222
  {
192
- "type": "function", "name": "execJobFromHub", "stateMutability": "nonpayable", "inputs": [
193
- { "name": "safeAddress", "type": "address" },
194
- { "name": "actionTarget", "type": "address" },
195
- { "name": "actionValue", "type": "uint256" },
196
- { "name": "actionData", "type": "bytes" },
197
- { "name": "operation", "type": "uint8" }
198
- ], "outputs": [{ "type": "bool", "name": "success" }]
223
+ "inputs": [
224
+ { "internalType": "address", "name": "safeAddress", "type": "address" },
225
+ { "internalType": "address", "name": "actionTarget", "type": "address" },
226
+ { "internalType": "uint256", "name": "actionValue", "type": "uint256" },
227
+ { "internalType": "bytes", "name": "actionData", "type": "bytes" },
228
+ { "internalType": "uint8", "name": "operation", "type": "uint8" }
229
+ ],
230
+ "name": "execJobFromHub",
231
+ "outputs": [
232
+ { "internalType": "bool", "name": "success", "type": "bool" }
233
+ ],
234
+ "stateMutability": "nonpayable",
235
+ "type": "function"
199
236
  }
200
237
  ]);
201
- // Ensure we don't carry static args in safe mode
202
- jobInput.arguments = undefined;
238
+ // Handle static vs dynamic safe wallet jobs
239
+ const hasDynamicUrl = !!jobInput.dynamicArgumentsScriptUrl;
240
+ const hasSafeTransactions = !!(jobInput.safeTransactions && jobInput.safeTransactions.length > 0);
241
+ if (hasDynamicUrl) {
242
+ // Dynamic safe wallet job - keep existing behavior
243
+ jobInput.arguments = undefined;
244
+ }
245
+ else if (hasSafeTransactions) {
246
+ // Static safe wallet job - encode transactions into arguments
247
+ const safeTransactions = jobInput.safeTransactions;
248
+ const safeAddress = jobInput.safeAddress;
249
+ if (safeTransactions.length === 1) {
250
+ // Single transaction: use transaction directly
251
+ const tx = safeTransactions[0];
252
+ jobInput.arguments = [
253
+ safeAddress,
254
+ tx.to,
255
+ tx.value,
256
+ tx.data,
257
+ '0' // CALL
258
+ ];
259
+ }
260
+ else {
261
+ // Multiple transactions: use multisend
262
+ if (!multisendCallOnly) {
263
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError('MultisendCallOnly address not configured for this chain.'), 'Configuration error');
264
+ }
265
+ const encodedMultisendData = encodeMultisendData(safeTransactions);
266
+ jobInput.arguments = [
267
+ safeAddress,
268
+ multisendCallOnly,
269
+ '0',
270
+ encodedMultisendData,
271
+ '1' // DELEGATECALL
272
+ ];
273
+ }
274
+ }
275
+ else {
276
+ // Will be caught by validation
277
+ jobInput.arguments = undefined;
278
+ }
203
279
  }
204
280
  // 0. Validate user input thoroughly before proceeding (after safe overrides)
205
281
  try {
@@ -330,9 +406,10 @@ async function createJob(client, params) {
330
406
  // We'll throw an error with the fee and let the caller handle the prompt/confirmation.
331
407
  // If you want to automate, you can add a `proceed` flag to params in the future.
332
408
  // Check if the user has enough TG to cover the job cost prediction
409
+ // Use chainId if available, so we can use SDK RPC provider even if user's RPC fails
333
410
  let tgBalanceWei, tgBalance;
334
411
  try {
335
- const balanceResult = await (0, checkTgBalance_1.checkTgBalance)(signer);
412
+ const balanceResult = await (0, checkTgBalance_1.checkTgBalance)(signer, chainIdStr);
336
413
  if (!balanceResult.success || !balanceResult.data) {
337
414
  return (0, errors_1.createErrorResponse)(new errors_1.BalanceError('Failed to check TG balance', balanceResult.details), 'Balance check error');
338
415
  }
@@ -410,6 +487,7 @@ async function createJob(client, params) {
410
487
  ether_balance: typeof jobData.ether_balance === 'bigint' ? Number(jobData.ether_balance) : Number(jobData.ether_balance),
411
488
  token_balance: typeof jobData.token_balance === 'bigint' ? Number(jobData.token_balance) : Number(jobData.token_balance),
412
489
  };
490
+ console.log('jobDataForApi', jobDataForApi);
413
491
  const res = await client.post('/api/jobs', [jobDataForApi], {
414
492
  headers: { 'Content-Type': 'application/json', 'X-API-KEY': apiKey },
415
493
  });
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ethers } from 'ethers';
1
2
  export interface SDKConfig {
2
3
  apiKey: string;
3
4
  apiUrl: string;
@@ -8,12 +9,23 @@ export declare const CONTRACT_ADDRESSES_BY_CHAIN: Record<string, {
8
9
  safeFactory?: string;
9
10
  safeModule?: string;
10
11
  safeSingleton?: string;
12
+ multisendCallOnly?: string;
13
+ rpcUrl?: string;
11
14
  }>;
12
15
  export declare function getConfig(): SDKConfig;
16
+ /**
17
+ * Get RPC provider for a given chain ID using SDK-configured RPC URLs
18
+ * This ensures reliable connection even if user's RPC fails
19
+ * @param chainId - Chain ID as string or number
20
+ * @returns ethers.JsonRpcProvider instance or null if chain not supported
21
+ */
22
+ export declare function getRpcProvider(chainId: string | number | undefined): ethers.JsonRpcProvider | null;
13
23
  export declare function getChainAddresses(chainId: string | number | undefined): {
14
24
  gasRegistry: string;
15
25
  jobRegistry: string;
16
26
  safeFactory: string | undefined;
17
27
  safeModule: string | undefined;
18
28
  safeSingleton: string | undefined;
29
+ multisendCallOnly: string | undefined;
30
+ rpcUrl: string | undefined;
19
31
  };
package/dist/config.js CHANGED
@@ -4,7 +4,9 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.CONTRACT_ADDRESSES_BY_CHAIN = void 0;
6
6
  exports.getConfig = getConfig;
7
+ exports.getRpcProvider = getRpcProvider;
7
8
  exports.getChainAddresses = getChainAddresses;
9
+ const ethers_1 = require("ethers");
8
10
  // Contract addresses per chain
9
11
  // Keyed by chainId as string to avoid bigint conversions throughout the SDK
10
12
  exports.CONTRACT_ADDRESSES_BY_CHAIN = {
@@ -15,6 +17,8 @@ exports.CONTRACT_ADDRESSES_BY_CHAIN = {
15
17
  jobRegistry: '0x476ACc7949a95e31144cC84b8F6BC7abF0967E4b',
16
18
  safeFactory: '0x04359eDC46Cd6C6BD7F6359512984222BE10F8Be',
17
19
  safeModule: '0xa0bC1477cfc452C05786262c377DE51FB8bc4669',
20
+ multisendCallOnly: '0x9641d764fc13c8B624c04430C7356C1C7C8102e2',
21
+ rpcUrl: 'https://sepolia.optimism.io',
18
22
  },
19
23
  // Ethereum Sepolia (11155111) - Ethereum Sepolia Testnet
20
24
  '11155111': {
@@ -22,6 +26,8 @@ exports.CONTRACT_ADDRESSES_BY_CHAIN = {
22
26
  jobRegistry: '0x476ACc7949a95e31144cC84b8F6BC7abF0967E4b',
23
27
  safeFactory: '0xdf76E2A796a206D877086c717979054544B1D9Bc',
24
28
  safeModule: '0xa0bC1477cfc452C05786262c377DE51FB8bc4669',
29
+ multisendCallOnly: '0x9641d764fc13c8B624c04430C7356C1C7C8102e2',
30
+ rpcUrl: 'https://rpc.sepolia.org',
25
31
  },
26
32
  // Arbitrum Sepolia (421614) - Arbitrum Sepolia Testnet
27
33
  '421614': {
@@ -29,6 +35,8 @@ exports.CONTRACT_ADDRESSES_BY_CHAIN = {
29
35
  jobRegistry: '0x476ACc7949a95e31144cC84b8F6BC7abF0967E4b',
30
36
  safeFactory: '0x04359eDC46Cd6C6BD7F6359512984222BE10F8Be',
31
37
  safeModule: '0xa0bC1477cfc452C05786262c377DE51FB8bc4669',
38
+ multisendCallOnly: '0x9641d764fc13c8B624c04430C7356C1C7C8102e2',
39
+ rpcUrl: 'https://sepolia-rollup.arbitrum.io/rpc',
32
40
  // safeSingleton can be provided per deployment (Safe or SafeL2)
33
41
  },
34
42
  // Base Sepolia (84532) - Base Sepolia Testnet
@@ -37,12 +45,16 @@ exports.CONTRACT_ADDRESSES_BY_CHAIN = {
37
45
  jobRegistry: '0x476ACc7949a95e31144cC84b8F6BC7abF0967E4b',
38
46
  safeFactory: '0x04359eDC46Cd6C6BD7F6359512984222BE10F8Be',
39
47
  safeModule: '0xa0bC1477cfc452C05786262c377DE51FB8bc4669',
48
+ multisendCallOnly: '0x9641d764fc13c8B624c04430C7356C1C7C8102e2',
49
+ rpcUrl: 'https://sepolia.base.org',
40
50
  },
41
51
  // MAINNET CONFIGURATIONS
42
52
  // Arbitrum One (42161) - Mainnet
43
53
  '42161': {
44
54
  gasRegistry: '0x93dDB2307F3Af5df85F361E5Cddd898Acd3d132d',
45
55
  jobRegistry: '0xAf1189aFd1F1880F09AeC3Cbc32cf415c735C710',
56
+ multisendCallOnly: '0x9641d764fc13c8B624c04430C7356C1C7C8102e2',
57
+ rpcUrl: 'https://arb1.arbitrum.io/rpc',
46
58
  },
47
59
  // Default/fallbacks can be extended as needed for other networks
48
60
  };
@@ -52,6 +64,19 @@ function getConfig() {
52
64
  apiUrl: '',
53
65
  };
54
66
  }
67
+ /**
68
+ * Get RPC provider for a given chain ID using SDK-configured RPC URLs
69
+ * This ensures reliable connection even if user's RPC fails
70
+ * @param chainId - Chain ID as string or number
71
+ * @returns ethers.JsonRpcProvider instance or null if chain not supported
72
+ */
73
+ function getRpcProvider(chainId) {
74
+ const { rpcUrl } = getChainAddresses(chainId);
75
+ if (!rpcUrl) {
76
+ return null;
77
+ }
78
+ return new ethers_1.ethers.JsonRpcProvider(rpcUrl);
79
+ }
55
80
  function getChainAddresses(chainId) {
56
81
  const chainKey = String(chainId ?? '');
57
82
  const mapped = exports.CONTRACT_ADDRESSES_BY_CHAIN[chainKey];
@@ -61,5 +86,7 @@ function getChainAddresses(chainId) {
61
86
  safeFactory: mapped ? mapped.safeFactory : '',
62
87
  safeModule: mapped ? mapped.safeModule : '',
63
88
  safeSingleton: mapped ? mapped.safeSingleton : '',
89
+ multisendCallOnly: mapped ? mapped.multisendCallOnly : '',
90
+ rpcUrl: mapped ? mapped.rpcUrl : '',
64
91
  };
65
92
  }
package/dist/types.d.ts CHANGED
@@ -33,6 +33,11 @@ export declare enum ArgType {
33
33
  Dynamic = "dynamic"
34
34
  }
35
35
  export type WalletMode = 'regular' | 'safe';
36
+ export interface SafeTransaction {
37
+ to: string;
38
+ value: string;
39
+ data: string;
40
+ }
36
41
  export type CreateJobInput = (TimeBasedJobInput & {
37
42
  jobType: JobType.Time;
38
43
  argType: ArgType.Static | ArgType.Dynamic;
@@ -67,6 +72,13 @@ export interface TimeBasedJobInput {
67
72
  * Required if walletMode is 'safe'.
68
73
  */
69
74
  safeAddress?: string;
75
+ /**
76
+ * Array of transactions for safe wallet jobs with static parameters.
77
+ * If length === 1: single call (operation 0)
78
+ * If length > 1: batch call via multisend (operation 1)
79
+ * Only used when walletMode is 'safe' and argType is 'static'.
80
+ */
81
+ safeTransactions?: SafeTransaction[];
70
82
  }
71
83
  export interface EventBasedJobInput {
72
84
  jobTitle: string;
@@ -92,6 +104,13 @@ export interface EventBasedJobInput {
92
104
  * Required if walletMode is 'safe'.
93
105
  */
94
106
  safeAddress?: string;
107
+ /**
108
+ * Array of transactions for safe wallet jobs with static parameters.
109
+ * If length === 1: single call (operation 0)
110
+ * If length > 1: batch call via multisend (operation 1)
111
+ * Only used when walletMode is 'safe' and argType is 'static'.
112
+ */
113
+ safeTransactions?: SafeTransaction[];
95
114
  }
96
115
  export interface ConditionBasedJobInput {
97
116
  jobTitle: string;
@@ -119,6 +138,13 @@ export interface ConditionBasedJobInput {
119
138
  * Required if walletMode is 'safe'.
120
139
  */
121
140
  safeAddress?: string;
141
+ /**
142
+ * Array of transactions for safe wallet jobs with static parameters.
143
+ * If length === 1: single call (operation 0)
144
+ * If length > 1: batch call via multisend (operation 1)
145
+ * Only used when walletMode is 'safe' and argType is 'static'.
146
+ */
147
+ safeTransactions?: SafeTransaction[];
122
148
  }
123
149
  export interface CreateJobData {
124
150
  job_id: string;
@@ -68,6 +68,26 @@ function validateStaticArguments(abiString, targetFunction, args, fieldPrefix =
68
68
  throw new errors_1.ValidationError(`${fieldPrefix}Args`, 'All function arguments are required for static argument type.');
69
69
  }
70
70
  }
71
+ function validateSafeTransactions(transactions) {
72
+ if (!transactions || !Array.isArray(transactions) || transactions.length === 0) {
73
+ throw new errors_1.ValidationError('safeTransactions', 'safeTransactions array is required and must contain at least one transaction for static safe wallet jobs.');
74
+ }
75
+ for (let i = 0; i < transactions.length; i++) {
76
+ const tx = transactions[i];
77
+ if (!tx || typeof tx !== 'object') {
78
+ throw new errors_1.ValidationError('safeTransactions', `Transaction at index ${i} is invalid.`);
79
+ }
80
+ if (!isNonEmptyString(tx.to) || !ethers_1.ethers.isAddress(tx.to)) {
81
+ throw new errors_1.ValidationError('safeTransactions', `Transaction at index ${i}: 'to' must be a valid Ethereum address.`);
82
+ }
83
+ if (typeof tx.value !== 'string') {
84
+ throw new errors_1.ValidationError('safeTransactions', `Transaction at index ${i}: 'value' must be a string representing wei amount.`);
85
+ }
86
+ if (!isNonEmptyString(tx.data) || !tx.data.startsWith('0x')) {
87
+ throw new errors_1.ValidationError('safeTransactions', `Transaction at index ${i}: 'data' must be a hex string starting with 0x.`);
88
+ }
89
+ }
90
+ }
71
91
  function validateTimeBasedJobInput(input, argType) {
72
92
  if (!isNonEmptyString(input.jobTitle)) {
73
93
  throw new errors_1.ValidationError('jobTitle', 'Job title is required.');
@@ -103,18 +123,37 @@ function validateTimeBasedJobInput(input, argType) {
103
123
  else {
104
124
  throw new errors_1.ValidationError('scheduleType', 'scheduleType must be one of interval | cron | specific.');
105
125
  }
106
- if (input.walletMode !== 'safe') {
107
- validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
108
- }
109
126
  // Arg type checks
110
127
  const isDynamic = argType === 'dynamic' || argType === 2;
111
- if (isDynamic) {
112
- if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
113
- throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
128
+ // Safe wallet mode validation
129
+ if (input.walletMode === 'safe') {
130
+ // Ensure static and dynamic are mutually exclusive
131
+ if (isDynamic && input.safeTransactions && input.safeTransactions.length > 0) {
132
+ throw new errors_1.ValidationError('safeTransactions', 'Cannot provide both dynamicArgumentsScriptUrl and safeTransactions. Use one or the other.');
133
+ }
134
+ if (!isDynamic && !input.safeTransactions) {
135
+ throw new errors_1.ValidationError('safeTransactions', 'For static safe wallet jobs, either provide safeTransactions or use dynamicArgumentsScriptUrl for dynamic jobs.');
136
+ }
137
+ if (isDynamic) {
138
+ // Dynamic safe wallet job
139
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
140
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
141
+ }
142
+ }
143
+ else {
144
+ // Static safe wallet job
145
+ validateSafeTransactions(input.safeTransactions);
114
146
  }
115
147
  }
116
148
  else {
117
- if (input.walletMode !== 'safe') {
149
+ // Regular wallet mode
150
+ validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
151
+ if (isDynamic) {
152
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
153
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
154
+ }
155
+ }
156
+ else {
118
157
  validateStaticArguments(input.abi, input.targetFunction, input.arguments, 'contract');
119
158
  }
120
159
  }
@@ -136,17 +175,37 @@ function validateEventBasedJobInput(input, argType) {
136
175
  throw new errors_1.ValidationError('triggerChainId', 'Trigger chain ID is required.');
137
176
  }
138
177
  validateContractBasics(input.triggerContractAddress, input.abi, input.triggerEvent, 'eventContract');
139
- if (input.walletMode !== 'safe') {
140
- validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
141
- }
178
+ // Arg type checks
142
179
  const isDynamic = argType === 'dynamic' || argType === 2;
143
- if (isDynamic) {
144
- if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
145
- throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
180
+ // Safe wallet mode validation
181
+ if (input.walletMode === 'safe') {
182
+ // Ensure static and dynamic are mutually exclusive
183
+ if (isDynamic && input.safeTransactions && input.safeTransactions.length > 0) {
184
+ throw new errors_1.ValidationError('safeTransactions', 'Cannot provide both dynamicArgumentsScriptUrl and safeTransactions. Use one or the other.');
185
+ }
186
+ if (!isDynamic && !input.safeTransactions) {
187
+ throw new errors_1.ValidationError('safeTransactions', 'For static safe wallet jobs, either provide safeTransactions or use dynamicArgumentsScriptUrl for dynamic jobs.');
188
+ }
189
+ if (isDynamic) {
190
+ // Dynamic safe wallet job
191
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
192
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
193
+ }
194
+ }
195
+ else {
196
+ // Static safe wallet job
197
+ validateSafeTransactions(input.safeTransactions);
146
198
  }
147
199
  }
148
200
  else {
149
- if (input.walletMode !== 'safe') {
201
+ // Regular wallet mode
202
+ validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
203
+ if (isDynamic) {
204
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
205
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
206
+ }
207
+ }
208
+ else {
150
209
  validateStaticArguments(input.abi, input.targetFunction, input.arguments, 'contract');
151
210
  }
152
211
  }
@@ -184,17 +243,37 @@ function validateConditionBasedJobInput(input, argType) {
184
243
  throw new errors_1.ValidationError('contractLimits', 'Value is required.');
185
244
  }
186
245
  }
187
- if (input.walletMode !== 'safe') {
188
- validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
189
- }
246
+ // Arg type checks
190
247
  const isDynamic = argType === 'dynamic' || argType === 2;
191
- if (isDynamic) {
192
- if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
193
- throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
248
+ // Safe wallet mode validation
249
+ if (input.walletMode === 'safe') {
250
+ // Ensure static and dynamic are mutually exclusive
251
+ if (isDynamic && input.safeTransactions && input.safeTransactions.length > 0) {
252
+ throw new errors_1.ValidationError('safeTransactions', 'Cannot provide both dynamicArgumentsScriptUrl and safeTransactions. Use one or the other.');
253
+ }
254
+ if (!isDynamic && !input.safeTransactions) {
255
+ throw new errors_1.ValidationError('safeTransactions', 'For static safe wallet jobs, either provide safeTransactions or use dynamicArgumentsScriptUrl for dynamic jobs.');
256
+ }
257
+ if (isDynamic) {
258
+ // Dynamic safe wallet job
259
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
260
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
261
+ }
262
+ }
263
+ else {
264
+ // Static safe wallet job
265
+ validateSafeTransactions(input.safeTransactions);
194
266
  }
195
267
  }
196
268
  else {
197
- if (input.walletMode !== 'safe') {
269
+ // Regular wallet mode
270
+ validateContractBasics(input.targetContractAddress, input.abi, input.targetFunction, 'contract');
271
+ if (isDynamic) {
272
+ if (!isNonEmptyString(input.dynamicArgumentsScriptUrl) || !isValidUrl(input.dynamicArgumentsScriptUrl)) {
273
+ throw new errors_1.ValidationError('contractIpfs', 'IPFS Code URL is required and must be valid for dynamic argument type.');
274
+ }
275
+ }
276
+ else {
198
277
  validateStaticArguments(input.abi, input.targetFunction, input.arguments, 'contract');
199
278
  }
200
279
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdk-triggerx",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
4
4
  "description": "SDK for interacting with the TriggerX backend and smart contracts.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",