sdk-triggerx 0.1.17 → 0.1.18

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/api/jobs.js CHANGED
@@ -16,7 +16,6 @@ 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 SafeFactory_1 = require("../contracts/safe/SafeFactory");
20
19
  const JOB_ID = '300949528249665590178224313442040528409305273634097553067152835846309150732';
21
20
  const DYNAMIC_ARGS_URL = 'https://teal-random-koala-993.mypinata.cloud/ipfs/bafkreif426p7t7takzhw3g6we2h6wsvf27p5jxj3gaiynqf22p3jvhx4la';
22
21
  function toCreateJobDataFromTime(input, balances, userAddress, jobCostPrediction) {
@@ -38,9 +37,9 @@ function toCreateJobDataFromTime(input, balances, userAddress, jobCostPrediction
38
37
  cron_expression: input.scheduleType === 'cron' ? input.cronExpression : undefined,
39
38
  specific_schedule: input.scheduleType === 'specific' ? input.specificSchedule : undefined,
40
39
  target_chain_id: input.chainId,
41
- target_contract_address: input.targetContractAddress,
42
- target_function: input.targetFunction,
43
- abi: input.abi,
40
+ target_contract_address: input.targetContractAddress || '',
41
+ target_function: input.targetFunction || '',
42
+ abi: input.abi || '',
44
43
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
45
44
  arguments: input.arguments,
46
45
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
@@ -65,9 +64,9 @@ function toCreateJobDataFromEvent(input, balances, userAddress, jobCostPredictio
65
64
  trigger_contract_address: input.triggerContractAddress,
66
65
  trigger_event: input.triggerEvent,
67
66
  target_chain_id: input.chainId,
68
- target_contract_address: input.targetContractAddress,
69
- target_function: input.targetFunction,
70
- abi: input.abi,
67
+ target_contract_address: input.targetContractAddress || '',
68
+ target_function: input.targetFunction || '',
69
+ abi: input.abi || '',
71
70
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
72
71
  arguments: input.arguments,
73
72
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
@@ -94,9 +93,9 @@ function toCreateJobDataFromCondition(input, balances, userAddress, jobCostPredi
94
93
  value_source_type: input.valueSourceType,
95
94
  value_source_url: input.valueSourceUrl,
96
95
  target_chain_id: input.chainId,
97
- target_contract_address: input.targetContractAddress,
98
- target_function: input.targetFunction,
99
- abi: input.abi,
96
+ target_contract_address: input.targetContractAddress || '',
97
+ target_function: input.targetFunction || '',
98
+ abi: input.abi || '',
100
99
  arg_type: input.dynamicArgumentsScriptUrl ? 2 : 1,
101
100
  arguments: input.arguments,
102
101
  dynamic_arguments_script_url: input.dynamicArgumentsScriptUrl,
@@ -124,46 +123,104 @@ function encodeJobType4or6Data(recurringJob, ipfsHash) {
124
123
  */
125
124
  async function createJob(client, params) {
126
125
  let { jobInput, signer, encodedData } = params;
127
- // 0. Validate user input thoroughly before proceeding
126
+ // Use the API key from the client instance
127
+ const apiKey = client.getApiKey();
128
+ if (!apiKey) {
129
+ return (0, errors_1.createErrorResponse)(new errors_1.AuthenticationError('API key is required but not provided'), 'Authentication failed');
130
+ }
131
+ let userAddress;
128
132
  try {
129
- const argValue = jobInput.argType;
130
- (0, validation_1.validateJobInput)(jobInput, argValue);
131
- console.log('Job input validated successfully');
133
+ userAddress = await signer.getAddress();
132
134
  }
133
135
  catch (err) {
134
- if (err instanceof errors_1.ValidationError) {
135
- return { success: false, error: `${err.field}: ${err.message}` };
136
- }
137
- return { success: false, error: err.message };
136
+ return (0, errors_1.createErrorResponse)(new errors_1.AuthenticationError('Failed to get signer address', { originalError: err }), 'Authentication failed');
138
137
  }
139
- // Use the API key from the client instance
140
- const apiKey = client.getApiKey();
141
- const userAddress = await signer.getAddress();
142
138
  // Resolve chain-specific addresses
143
- const network = await signer.provider?.getNetwork();
139
+ let network;
140
+ try {
141
+ network = await signer.provider?.getNetwork();
142
+ }
143
+ catch (err) {
144
+ return (0, errors_1.createErrorResponse)(new errors_1.NetworkError('Failed to get network information', { originalError: err }), 'Network error');
145
+ }
144
146
  const chainIdStr = network?.chainId ? network.chainId.toString() : undefined;
145
147
  const { jobRegistry, safeModule, safeFactory } = (0, config_1.getChainAddresses)(chainIdStr);
146
148
  const JOB_REGISTRY_ADDRESS = jobRegistry;
147
149
  if (!JOB_REGISTRY_ADDRESS) {
148
- return { success: false, error: 'JobRegistry address not configured for this chain. Update config mapping.' };
150
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`JobRegistry address not configured for chain ID: ${chainIdStr}`), 'Configuration error');
151
+ }
152
+ // If Safe mode, override target fields BEFORE validation so user doesn't need to provide them
153
+ const walletModePre = jobInput.walletMode;
154
+ if (walletModePre === 'safe') {
155
+ if (!safeModule) {
156
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError('Safe Module address not configured for this chain.'), 'Configuration error');
157
+ }
158
+ // If safeAddress is missing, require it (must be created by the user before this call)
159
+ if (!jobInput.safeAddress || typeof jobInput.safeAddress !== 'string' || !jobInput.safeAddress.trim()) {
160
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('safeAddress', 'safeAddress is required when walletMode is "safe". Call createSafeWallet first.'), 'Validation error');
161
+ }
162
+ const dynUrl = jobInput.dynamicArgumentsScriptUrl;
163
+ if (!dynUrl) {
164
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('dynamicArgumentsScriptUrl', 'Safe jobs require dynamicArgumentsScriptUrl (IPFS) for parameters.'), 'Validation error');
165
+ }
166
+ // Validate Safe has single owner and owner matches signer
167
+ try {
168
+ await (0, SafeWallet_1.ensureSingleOwnerAndMatchSigner)(jobInput.safeAddress, signer.provider, await signer.getAddress());
169
+ // Ensure module is enabled on Safe
170
+ await (0, SafeWallet_1.enableSafeModule)(jobInput.safeAddress, signer, safeModule);
171
+ }
172
+ catch (err) {
173
+ return (0, errors_1.createErrorResponse)(new errors_1.ContractError('Failed to configure Safe wallet', { originalError: err, safeAddress: jobInput.safeAddress }), 'Contract error');
174
+ }
175
+ // Auto-set module target; user does not need to pass targetContractAddress in safe mode
176
+ jobInput.targetContractAddress = safeModule;
177
+ jobInput.targetFunction = 'execJobFromHub(address,address,uint256,bytes,uint8)';
178
+ jobInput.abi = JSON.stringify([
179
+ {
180
+ "type": "function", "name": "execJobFromHub", "stateMutability": "nonpayable", "inputs": [
181
+ { "name": "safeAddress", "type": "address" },
182
+ { "name": "actionTarget", "type": "address" },
183
+ { "name": "actionValue", "type": "uint256" },
184
+ { "name": "actionData", "type": "bytes" },
185
+ { "name": "operation", "type": "uint8" }
186
+ ], "outputs": [{ "type": "bool", "name": "success" }]
187
+ }
188
+ ]);
189
+ // Ensure we don't carry static args in safe mode
190
+ jobInput.arguments = undefined;
149
191
  }
150
- let jobTitle, timeFrame, targetContractAddress, jobType;
192
+ // 0. Validate user input thoroughly before proceeding (after safe overrides)
193
+ try {
194
+ const argValue = jobInput.argType;
195
+ (0, validation_1.validateJobInput)(jobInput, argValue);
196
+ console.log('Job input validated successfully');
197
+ }
198
+ catch (err) {
199
+ if (err instanceof errors_1.ValidationError) {
200
+ return (0, errors_1.createErrorResponse)(err);
201
+ }
202
+ return (0, errors_1.createErrorResponse)(err, 'Job input validation failed');
203
+ }
204
+ let jobTitle = '';
205
+ let timeFrame = 0;
206
+ let targetContractAddress = '';
207
+ let jobType = 0;
151
208
  if ('jobTitle' in jobInput)
152
209
  jobTitle = jobInput.jobTitle;
153
210
  if ('timeFrame' in jobInput)
154
211
  timeFrame = jobInput.timeFrame;
155
212
  if ('targetContractAddress' in jobInput)
156
- targetContractAddress = jobInput.targetContractAddress;
213
+ targetContractAddress = jobInput.targetContractAddress || '';
157
214
  // Validate schedule-specific fields for time-based jobs
158
215
  if ('scheduleType' in jobInput) {
159
216
  if (jobInput.scheduleType === 'interval' && (jobInput.timeInterval === undefined || jobInput.timeInterval === null)) {
160
- throw new Error('timeInterval is required when scheduleType is interval');
217
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('timeInterval', 'timeInterval is required when scheduleType is interval'), 'Validation error');
161
218
  }
162
219
  if (jobInput.scheduleType === 'cron' && !jobInput.cronExpression) {
163
- throw new Error('cronExpression is required when scheduleType is cron');
220
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('cronExpression', 'cronExpression is required when scheduleType is cron'), 'Validation error');
164
221
  }
165
222
  if (jobInput.scheduleType === 'specific' && !jobInput.specificSchedule) {
166
- throw new Error('specificSchedule is required when scheduleType is specific');
223
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('specificSchedule', 'specificSchedule is required when scheduleType is specific'), 'Validation error');
167
224
  }
168
225
  }
169
226
  // Infer jobType from jobInput
@@ -232,7 +289,7 @@ async function createJob(client, params) {
232
289
  // Dynamic: call backend API to get fee
233
290
  const ipfs_url = jobInput.dynamicArgumentsScriptUrl;
234
291
  if (!ipfs_url) {
235
- throw new Error('dynamicArgumentsScriptUrl is required for dynamic argType');
292
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('dynamicArgumentsScriptUrl', 'dynamicArgumentsScriptUrl is required for dynamic argType'), 'Validation error');
236
293
  }
237
294
  // Call backend API to get fee
238
295
  let fee = 0;
@@ -246,11 +303,13 @@ async function createJob(client, params) {
246
303
  fee = feeRes.data.total_fee;
247
304
  }
248
305
  else {
249
- throw new Error('Invalid response from /api/fees: missing total_fee');
306
+ return (0, errors_1.createErrorResponse)(new errors_1.ApiError('Invalid response from /api/fees: missing total_fee', { response: feeRes }), 'API error');
250
307
  }
251
308
  }
252
309
  catch (err) {
253
- throw new Error('Failed to fetch job cost prediction: ' + err.message);
310
+ const httpStatusCode = (0, errors_1.extractHttpStatusCode)(err);
311
+ const errorCode = (0, errors_1.determineErrorCode)(err, httpStatusCode);
312
+ return (0, errors_1.createErrorResponse)(new errors_1.ApiError('Failed to fetch job cost prediction', { originalError: err, ipfs_url }, httpStatusCode), 'API error');
254
313
  }
255
314
  job_cost_prediction = fee * noOfExecutions;
256
315
  }
@@ -259,18 +318,41 @@ async function createJob(client, params) {
259
318
  // We'll throw an error with the fee and let the caller handle the prompt/confirmation.
260
319
  // If you want to automate, you can add a `proceed` flag to params in the future.
261
320
  // Check if the user has enough TG to cover the job cost prediction
262
- const { tgBalanceWei, tgBalance } = await (0, checkTgBalance_1.checkTgBalance)(signer);
321
+ let tgBalanceWei, tgBalance;
322
+ try {
323
+ const balanceResult = await (0, checkTgBalance_1.checkTgBalance)(signer);
324
+ if (!balanceResult.success || !balanceResult.data) {
325
+ return (0, errors_1.createErrorResponse)(new errors_1.BalanceError('Failed to check TG balance', balanceResult.details), 'Balance check error');
326
+ }
327
+ tgBalanceWei = balanceResult.data.tgBalanceWei;
328
+ tgBalance = balanceResult.data.tgBalance;
329
+ }
330
+ catch (err) {
331
+ return (0, errors_1.createErrorResponse)(new errors_1.BalanceError('Failed to check TG balance', { originalError: err }), 'Balance check error');
332
+ }
263
333
  if (Number(tgBalance) < job_cost_prediction) {
264
334
  // Check if user has enabled auto topup
265
335
  // For each job type, autotopupTG should be present in jobInput
266
336
  const autoTopupTG = jobInput.autotopupTG === true;
267
337
  if (!autoTopupTG) {
268
- throw new Error(`Insufficient TG balance. Job cost prediction is ${job_cost_prediction}. Current TG balance: ${tgBalance}. Please set autotopupTG: true in jobInput.`);
338
+ return (0, errors_1.createErrorResponse)(new errors_1.BalanceError(`Insufficient TG balance. Job cost prediction is ${job_cost_prediction}. Current TG balance: ${tgBalance}. Please set autotopupTG: true in jobInput.`, {
339
+ required: job_cost_prediction,
340
+ current: tgBalance,
341
+ autoTopupEnabled: false
342
+ }), 'Insufficient balance');
269
343
  }
270
344
  else {
271
345
  // autotopupTG is true, automatically top up
272
346
  const requiredTG = Math.ceil(job_cost_prediction); // 1 TG = 0.001 ETH
273
- await (0, topupTg_1.topupTg)(requiredTG, signer);
347
+ try {
348
+ const topupResult = await (0, topupTg_1.topupTg)(requiredTG, signer);
349
+ if (!topupResult.success) {
350
+ return (0, errors_1.createErrorResponse)(new errors_1.BalanceError('Failed to top up TG balance', topupResult.details), 'Top-up error');
351
+ }
352
+ }
353
+ catch (err) {
354
+ return (0, errors_1.createErrorResponse)(new errors_1.BalanceError('Failed to top up TG balance', { originalError: err, requiredTG }), 'Top-up error');
355
+ }
274
356
  }
275
357
  }
276
358
  // Compute balances to store with the job
@@ -278,56 +360,22 @@ async function createJob(client, params) {
278
360
  const etherBalance = tokenBalanceWei / 1000n;
279
361
  // Patch jobInput with job_cost_prediction for downstream usage
280
362
  jobInput.jobCostPrediction = job_cost_prediction;
281
- // --- Safe wallet integration (override target to module when selected) ---
282
- const walletMode = jobInput.walletMode;
283
- if (walletMode === 'safe') {
284
- if (!safeModule) {
285
- return { success: false, error: 'Safe Module address not configured for this chain.' };
286
- }
287
- // Enforce dynamic parameters coming from IPFS only
288
- const dynUrl = jobInput.dynamicArgumentsScriptUrl;
289
- if (!dynUrl) {
290
- return { success: false, error: 'Safe jobs require dynamicArgumentsScriptUrl (IPFS) for parameters.' };
291
- }
292
- // Resolve or create Safe wallet
293
- const providedSafeAddress = jobInput.safeAddress;
294
- let safeAddressToUse = providedSafeAddress;
295
- if (!safeAddressToUse) {
296
- if (!safeFactory) {
297
- return { success: false, error: 'Safe Factory address not configured for this chain.' };
298
- }
299
- safeAddressToUse = await (0, SafeFactory_1.createSafeWalletForUser)(safeFactory, signer, userAddress);
300
- }
301
- // Validate Safe has single owner and owner matches signer
302
- await (0, SafeWallet_1.ensureSingleOwnerAndMatchSigner)(safeAddressToUse, signer.provider, userAddress);
303
- // Ensure module is enabled on Safe
304
- await (0, SafeWallet_1.enableSafeModule)(safeAddressToUse, signer, safeModule);
305
- // Override target for job to Safe Module and function to execJobFromHub
306
- targetContractAddress = safeModule; // ensure target contract is SAFE_MODULE_ADDRESS only
307
- jobInput.targetContractAddress = safeModule;
308
- jobInput.targetFunction = 'execJobFromHub(address,address,uint256,bytes,uint8)';
309
- jobInput.abi = JSON.stringify([
310
- { "type": "function", "name": "execJobFromHub", "stateMutability": "nonpayable", "inputs": [
311
- { "name": "safeAddress", "type": "address" },
312
- { "name": "actionTarget", "type": "address" },
313
- { "name": "actionValue", "type": "uint256" },
314
- { "name": "actionData", "type": "bytes" },
315
- { "name": "operation", "type": "uint8" }
316
- ], "outputs": [{ "type": "bool", "name": "success" }] }
317
- ]);
318
- // Note: Parameters will be dynamically provided via IPFS script; do not set static arguments here
319
- jobInput.arguments = undefined;
363
+ let jobId;
364
+ try {
365
+ jobId = await (0, JobRegistry_1.createJobOnChain)({
366
+ jobTitle: jobTitle,
367
+ jobType,
368
+ timeFrame: timeFrame,
369
+ targetContractAddress: targetContractAddress,
370
+ encodedData: encodedData || '',
371
+ contractAddress: JOB_REGISTRY_ADDRESS,
372
+ abi: JobRegistry_json_1.default.abi,
373
+ signer,
374
+ });
375
+ }
376
+ catch (err) {
377
+ return (0, errors_1.createErrorResponse)(new errors_1.ContractError('Failed to create job on chain', { originalError: err, jobTitle, jobType, timeFrame }), 'Contract error');
320
378
  }
321
- const jobId = await (0, JobRegistry_1.createJobOnChain)({
322
- jobTitle: jobTitle,
323
- jobType,
324
- timeFrame: timeFrame,
325
- targetContractAddress: targetContractAddress,
326
- encodedData: encodedData || '',
327
- contractAddress: JOB_REGISTRY_ADDRESS,
328
- abi: JobRegistry_json_1.default.abi,
329
- signer,
330
- });
331
379
  // 2. Convert input to CreateJobData
332
380
  let jobData;
333
381
  const balances = { etherBalance, tokenBalanceWei };
@@ -356,6 +404,8 @@ async function createJob(client, params) {
356
404
  return { success: true, data: res };
357
405
  }
358
406
  catch (error) {
359
- return { success: false, error: error.message };
407
+ const httpStatusCode = (0, errors_1.extractHttpStatusCode)(error);
408
+ const errorCode = (0, errors_1.determineErrorCode)(error, httpStatusCode);
409
+ return (0, errors_1.createErrorResponse)(new errors_1.ApiError('Failed to create job via API', { originalError: error, jobId }, httpStatusCode), 'API error');
360
410
  }
361
411
  }
@@ -0,0 +1,8 @@
1
+ import { Signer } from 'ethers';
2
+ /**
3
+ * Creates a new Safe wallet for the user on the signer's network.
4
+ * @param signer ethers.Signer (must be connected to the correct network)
5
+ * @returns Promise<string> - the new Safe wallet address
6
+ * @throws If cannot resolve SafeFactory address from config, or provider/chain/network errors.
7
+ */
8
+ export declare function createSafeWallet(signer: Signer): Promise<string>;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSafeWallet = createSafeWallet;
4
+ const config_1 = require("../config");
5
+ const SafeFactory_1 = require("../contracts/safe/SafeFactory");
6
+ const SafeWallet_1 = require("../contracts/safe/SafeWallet");
7
+ /**
8
+ * Creates a new Safe wallet for the user on the signer's network.
9
+ * @param signer ethers.Signer (must be connected to the correct network)
10
+ * @returns Promise<string> - the new Safe wallet address
11
+ * @throws If cannot resolve SafeFactory address from config, or provider/chain/network errors.
12
+ */
13
+ async function createSafeWallet(signer) {
14
+ if (!signer.provider)
15
+ throw new Error('Signer must have a provider');
16
+ const network = await signer.provider.getNetwork();
17
+ if (!network?.chainId)
18
+ throw new Error('Could not get chainId from signer provider');
19
+ const chainId = network.chainId.toString();
20
+ const { safeFactory } = (0, config_1.getChainAddresses)(chainId);
21
+ if (!safeFactory)
22
+ throw new Error(`SafeFactory not configured for chain ${chainId}`);
23
+ const userAddr = await signer.getAddress();
24
+ const safeAddress = await (0, SafeFactory_1.createSafeWalletForUser)(safeFactory, signer, userAddr);
25
+ const { safeModule } = (0, config_1.getChainAddresses)(chainId);
26
+ if (!safeModule) {
27
+ throw new Error(`SafeModule not configured for chain ${chainId}`);
28
+ }
29
+ await (0, SafeWallet_1.enableSafeModule)(safeAddress, signer, safeModule);
30
+ return safeAddress;
31
+ }
@@ -1,2 +1,9 @@
1
1
  import { ethers } from 'ethers';
2
- export declare const topupTg: (tgAmount: number, signer: ethers.Signer) => Promise<any>;
2
+ export declare const topupTg: (tgAmount: number, signer: ethers.Signer) => Promise<{
3
+ success: boolean;
4
+ data?: any;
5
+ error?: string;
6
+ errorCode?: string;
7
+ errorType?: string;
8
+ details?: any;
9
+ }>;
@@ -7,17 +7,45 @@ exports.topupTg = void 0;
7
7
  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
+ const errors_1 = require("../utils/errors");
10
11
  const topupTg = async (tgAmount, signer) => {
11
- const network = await signer.provider?.getNetwork();
12
- const chainId = network?.chainId ? network.chainId.toString() : undefined;
13
- const { gasRegistry } = (0, config_1.getChainAddresses)(chainId);
14
- const gasRegistryContractAddress = gasRegistry;
15
- const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, signer);
16
- // Each TG costs 0.001 ETH, so calculate the ETH required for the given TG amount
17
- const amountInEthWei = tgAmount;
18
- // const amountInEthWei = ethers.parseEther(amountInEth.toString());
19
- const tx = await contract.purchaseTG(amountInEthWei, { value: amountInEthWei });
20
- await tx.wait();
21
- return tx;
12
+ // Validate inputs
13
+ if (!tgAmount || tgAmount <= 0) {
14
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('tgAmount', 'TG amount must be a positive number'), 'Validation error');
15
+ }
16
+ if (!signer) {
17
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('signer', 'Signer is required'), 'Validation error');
18
+ }
19
+ try {
20
+ const network = await signer.provider?.getNetwork();
21
+ const chainId = network?.chainId ? network.chainId.toString() : undefined;
22
+ const { gasRegistry } = (0, config_1.getChainAddresses)(chainId);
23
+ const gasRegistryContractAddress = gasRegistry;
24
+ if (!gasRegistryContractAddress) {
25
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`GasRegistry address not configured for chain ID: ${chainId}`), 'Configuration error');
26
+ }
27
+ const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, signer);
28
+ // Each TG costs 0.001 ETH, so calculate the ETH required for the given TG amount
29
+ const amountInEthWei = tgAmount;
30
+ // const amountInEthWei = ethers.parseEther(amountInEth.toString());
31
+ const tx = await contract.purchaseTG(amountInEthWei, { value: amountInEthWei });
32
+ await tx.wait();
33
+ return { success: true, data: tx };
34
+ }
35
+ catch (error) {
36
+ console.error('Error topping up TG:', error);
37
+ if (error instanceof Error) {
38
+ if (error.message.includes('network') || error.message.includes('timeout')) {
39
+ return (0, errors_1.createErrorResponse)(new errors_1.NetworkError('Network error during TG top-up', { originalError: error, tgAmount }), 'Network error');
40
+ }
41
+ else if (error.message.includes('contract') || error.message.includes('transaction')) {
42
+ return (0, errors_1.createErrorResponse)(new errors_1.ContractError('Contract error during TG top-up', { originalError: error, tgAmount }), 'Contract error');
43
+ }
44
+ else if (error.message.includes('insufficient funds') || error.message.includes('balance')) {
45
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('balance', 'Insufficient funds for TG top-up', { originalError: error, tgAmount }), 'Validation error');
46
+ }
47
+ }
48
+ return (0, errors_1.createErrorResponse)(error, 'Failed to top up TG');
49
+ }
22
50
  };
23
51
  exports.topupTg = topupTg;
@@ -3,6 +3,13 @@ import { ethers } from 'ethers';
3
3
  * Withdraw ETH in exchange for TG tokens.
4
4
  * @param signer ethers.Signer instance
5
5
  * @param amountTG The amount of TG tokens to withdraw (as a string or BigNumberish)
6
- * @returns The transaction object
6
+ * @returns The transaction object or error response
7
7
  */
8
- export declare const withdrawTg: (signer: ethers.Signer, amountTG: string | ethers.BigNumberish) => Promise<any>;
8
+ export declare const withdrawTg: (signer: ethers.Signer, amountTG: string | ethers.BigNumberish) => Promise<{
9
+ success: boolean;
10
+ data?: any;
11
+ error?: string;
12
+ errorCode?: string;
13
+ errorType?: string;
14
+ details?: any;
15
+ }>;
@@ -7,25 +7,50 @@ exports.withdrawTg = void 0;
7
7
  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
+ const errors_1 = require("../utils/errors");
10
11
  /**
11
12
  * Withdraw ETH in exchange for TG tokens.
12
13
  * @param signer ethers.Signer instance
13
14
  * @param amountTG The amount of TG tokens to withdraw (as a string or BigNumberish)
14
- * @returns The transaction object
15
+ * @returns The transaction object or error response
15
16
  */
16
17
  const withdrawTg = async (signer, amountTG) => {
17
- const network = await signer.provider?.getNetwork();
18
- const chainId = network?.chainId ? network.chainId.toString() : undefined;
19
- const { gasRegistry } = (0, config_1.getChainAddresses)(chainId);
20
- const gasRegistryContractAddress = gasRegistry;
21
- if (!gasRegistryContractAddress) {
22
- throw new Error('GasRegistry address not configured for this chain. Update config mapping.');
18
+ // Validate inputs
19
+ if (!signer) {
20
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('signer', 'Signer is required'), 'Validation error');
21
+ }
22
+ if (!amountTG || (typeof amountTG === 'string' && amountTG.trim() === '') || Number(amountTG) <= 0) {
23
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('amountTG', 'Amount must be a positive number'), 'Validation error');
24
+ }
25
+ try {
26
+ const network = await signer.provider?.getNetwork();
27
+ const chainId = network?.chainId ? network.chainId.toString() : undefined;
28
+ const { gasRegistry } = (0, config_1.getChainAddresses)(chainId);
29
+ const gasRegistryContractAddress = gasRegistry;
30
+ if (!gasRegistryContractAddress) {
31
+ return (0, errors_1.createErrorResponse)(new errors_1.ConfigurationError(`GasRegistry address not configured for chain ID: ${chainId}`), 'Configuration error');
32
+ }
33
+ const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, signer);
34
+ // Assumes the contract has a function: claimEthForTg(uint256 amount)
35
+ const amountTGWei = ethers_1.ethers.parseEther(amountTG.toString());
36
+ const tx = await contract.claimETHForTG(amountTGWei);
37
+ await tx.wait();
38
+ return { success: true, data: tx };
39
+ }
40
+ catch (error) {
41
+ console.error('Error withdrawing TG:', error);
42
+ if (error instanceof Error) {
43
+ if (error.message.includes('network') || error.message.includes('timeout')) {
44
+ return (0, errors_1.createErrorResponse)(new errors_1.NetworkError('Network error during TG withdrawal', { originalError: error, amountTG }), 'Network error');
45
+ }
46
+ else if (error.message.includes('contract') || error.message.includes('transaction')) {
47
+ return (0, errors_1.createErrorResponse)(new errors_1.ContractError('Contract error during TG withdrawal', { originalError: error, amountTG }), 'Contract error');
48
+ }
49
+ else if (error.message.includes('insufficient') || error.message.includes('balance')) {
50
+ return (0, errors_1.createErrorResponse)(new errors_1.ValidationError('balance', 'Insufficient TG balance for withdrawal', { originalError: error, amountTG }), 'Validation error');
51
+ }
52
+ }
53
+ return (0, errors_1.createErrorResponse)(error, 'Failed to withdraw TG');
23
54
  }
24
- const contract = new ethers_1.ethers.Contract(gasRegistryContractAddress, GasRegistry_json_1.default, signer);
25
- // Assumes the contract has a function: claimEthForTg(uint256 amount)
26
- const amountTGWei = ethers_1.ethers.parseEther(amountTG.toString());
27
- const tx = await contract.claimETHForTG(amountTGWei);
28
- await tx.wait();
29
- return tx;
30
55
  };
31
56
  exports.withdrawTg = withdrawTg;
@@ -9,6 +9,8 @@ const ethers_1 = require("ethers");
9
9
  exports.SAFE_ABI = [
10
10
  // module checks
11
11
  "function isModuleEnabled(address module) view returns (bool)",
12
+ // module management
13
+ "function enableModule(address module)",
12
14
  // EIP-712 domain separator for Safe
13
15
  "function domainSeparator() view returns (bytes32)",
14
16
  // Safe nonce
@@ -18,6 +20,8 @@ exports.SAFE_ABI = [
18
20
  // Owners and threshold, to validate single signer safes
19
21
  "function getOwners() view returns (address[])",
20
22
  "function getThreshold() view returns (uint256)",
23
+ // Add getTransactionHash for onchain hash calculation
24
+ "function getTransactionHash(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce) view returns (bytes32)"
21
25
  ];
22
26
  // Safe EIP-712 typehash for transactions
23
27
  exports.SAFE_TX_TYPEHASH = ethers_1.ethers.id("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 _nonce)");
@@ -52,38 +56,54 @@ async function enableSafeModule(safeAddress, signer, moduleAddress) {
52
56
  const safeProxy = new ethers_1.ethers.Contract(safeAddress, exports.SAFE_ABI, provider);
53
57
  // If already enabled, exit early
54
58
  const already = await safeProxy.isModuleEnabled(moduleAddress);
55
- if (already)
59
+ if (already) {
60
+ console.log('Module is already enabled');
56
61
  return;
62
+ }
63
+ // First, let's try the direct approach for single-owner Safes
64
+ try {
65
+ console.log('Attempting direct enableModule call...');
66
+ const safeWithSigner = new ethers_1.ethers.Contract(safeAddress, exports.SAFE_ABI, signer);
67
+ const tx = await safeWithSigner.enableModule(moduleAddress);
68
+ await tx.wait();
69
+ console.log('Module enabled via direct call');
70
+ return;
71
+ }
72
+ catch (error) {
73
+ console.log('Direct call failed, trying execTransaction approach...');
74
+ }
75
+ // If direct call fails, use execTransaction with proper signature
57
76
  const safeNonce = await safeProxy.nonce();
58
77
  const iface = new ethers_1.ethers.Interface(exports.SAFE_ABI);
59
78
  const data = iface.encodeFunctionData('enableModule', [moduleAddress]);
60
79
  const to = safeAddress;
61
- const value = 0n;
80
+ const value = 0;
62
81
  const operation = 0; // CALL
63
- const safeTxGas = 0n;
64
- const baseGas = 0n;
65
- const gasPrice = 0n;
82
+ const safeTxGas = 0;
83
+ const baseGas = 0;
84
+ const gasPrice = 0;
66
85
  const gasToken = ethers_1.ethers.ZeroAddress;
67
86
  const refundReceiver = ethers_1.ethers.ZeroAddress;
68
- // Calculate Safe transaction hash per EIP-712
69
- const safeTxHash = ethers_1.ethers.keccak256(ethers_1.ethers.AbiCoder.defaultAbiCoder().encode([
70
- 'bytes32', 'address', 'uint256', 'bytes32', 'uint8',
71
- 'uint256', 'uint256', 'uint256', 'address', 'address', 'uint256'
72
- ], [
73
- exports.SAFE_TX_TYPEHASH, to, value, ethers_1.ethers.keccak256(data), operation,
74
- safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, safeNonce
75
- ]));
76
- const domainSeparator = await safeProxy.domainSeparator();
77
- const txHash = ethers_1.ethers.keccak256(ethers_1.ethers.solidityPacked(['bytes1', 'bytes1', 'bytes32', 'bytes32'], ['0x19', '0x01', domainSeparator, safeTxHash]));
78
- const rawSignature = await signer.signMessage(ethers_1.ethers.getBytes(txHash));
79
- const sig = ethers_1.ethers.Signature.from(rawSignature);
80
- const adjustedV = sig.v + 4; // EthSign type
81
- const signature = ethers_1.ethers.concat([sig.r, sig.s, ethers_1.ethers.toBeHex(adjustedV, 1)]);
82
- const safeWithSigner = new ethers_1.ethers.Contract(safeAddress, exports.SAFE_ABI, signer);
83
- const tx = await safeWithSigner.execTransaction(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signature);
87
+ // Use contract to compute tx hash to avoid mismatch
88
+ const safeTxHash = await safeProxy.getTransactionHash(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, safeNonce);
89
+ // Sign the transaction hash using the connected wallet (personal_sign)
90
+ // For Gnosis Safe, personal_sign signatures must have v adjusted by +4 to mark EthSign
91
+ const rawSignature = await signer.signMessage(ethers_1.ethers.getBytes(safeTxHash));
92
+ const sigObj = ethers_1.ethers.Signature.from(rawSignature);
93
+ const adjustedV = sigObj.v + 4;
94
+ const signature = ethers_1.ethers.concat([
95
+ sigObj.r,
96
+ sigObj.s,
97
+ ethers_1.ethers.toBeHex(adjustedV, 1),
98
+ ]);
99
+ // Execute the transaction through Safe's execTransaction
100
+ const safeProxyWithSigner = new ethers_1.ethers.Contract(safeAddress, exports.SAFE_ABI, signer);
101
+ const tx = await safeProxyWithSigner.execTransaction(to, value, data, operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, signature);
84
102
  await tx.wait();
85
- const check = await safeProxy.isModuleEnabled(moduleAddress);
86
- if (!check) {
87
- throw new Error('Module verification failed');
103
+ // Verify module is enabled
104
+ const isNowEnabled = await safeProxy.isModuleEnabled(moduleAddress);
105
+ if (!isNowEnabled) {
106
+ throw new Error("Module verification failed");
88
107
  }
108
+ console.log('Module enabled successfully via execTransaction');
89
109
  }