• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-client-gateway / 14467761241

15 Apr 2025 11:00AM UTC coverage: 90.28% (-0.007%) from 90.287%
14467761241

push

github

hectorgomezv
Add AddressBooksController tests

3219 of 3876 branches covered (83.05%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

4 existing lines in 2 files now uncovered.

11150 of 12040 relevant lines covered (92.61%)

528.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.38
/src/routes/transactions/transactions.service.ts
1
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
106✔
2
import { MultisigTransaction as DomainMultisigTransaction } from '@/domain/safe/entities/multisig-transaction.entity';
3
import { SafeRepository } from '@/domain/safe/safe.repository';
106✔
4
import { ISafeRepository } from '@/domain/safe/safe.repository.interface';
106✔
5
import { AddConfirmationDto } from '@/domain/transactions/entities/add-confirmation.dto.entity';
6
import { ProposeTransactionDto } from '@/domain/transactions/entities/propose-transaction.dto.entity';
7
import { Page } from '@/routes/common/entities/page.entity';
8
import {
106✔
9
  buildNextPageURL,
10
  buildPreviousPageURL,
11
  cursorUrlFromLimitAndOffset,
12
  PaginationData,
13
} from '@/routes/common/pagination/pagination.data';
14
import {
106✔
15
  MODULE_TRANSACTION_PREFIX,
16
  MULTISIG_TRANSACTION_PREFIX,
17
  TRANSACTION_ID_SEPARATOR,
18
  TRANSFER_PREFIX,
19
} from '@/routes/transactions/constants';
20
import { ConflictType } from '@/routes/transactions/entities/conflict-type.entity';
106✔
21
import { CreationTransaction } from '@/routes/transactions/entities/creation-transaction.entity';
22
import { IncomingTransfer } from '@/routes/transactions/entities/incoming-transfer.entity';
106✔
23
import { ModuleTransaction } from '@/routes/transactions/entities/module-transaction.entity';
106✔
24
import { MultisigTransaction } from '@/routes/transactions/entities/multisig-transaction.entity';
106✔
25
import { PreviewTransactionDto } from '@/routes/transactions/entities/preview-transaction.dto.entity';
26
import { QueuedItem } from '@/routes/transactions/entities/queued-item.entity';
27
import { TransactionDetails } from '@/routes/transactions/entities/transaction-details/transaction-details.entity';
28
import { TransactionItemPage } from '@/routes/transactions/entities/transaction-item-page.entity';
29
import { TransactionPreview } from '@/routes/transactions/entities/transaction-preview.entity';
30
import { ModuleTransactionDetailsMapper } from '@/routes/transactions/mappers/module-transactions/module-transaction-details.mapper';
106✔
31
import { ModuleTransactionMapper } from '@/routes/transactions/mappers/module-transactions/module-transaction.mapper';
106✔
32
import { MultisigTransactionDetailsMapper } from '@/routes/transactions/mappers/multisig-transactions/multisig-transaction-details.mapper';
106✔
33
import { MultisigTransactionMapper } from '@/routes/transactions/mappers/multisig-transactions/multisig-transaction.mapper';
106✔
34
import { QueuedItemsMapper } from '@/routes/transactions/mappers/queued-items/queued-items.mapper';
106✔
35
import { TransactionPreviewMapper } from '@/routes/transactions/mappers/transaction-preview.mapper';
106✔
36
import { TransactionsHistoryMapper } from '@/routes/transactions/mappers/transactions-history.mapper';
106✔
37
import { TransferDetailsMapper } from '@/routes/transactions/mappers/transfers/transfer-details.mapper';
106✔
38
import { TransferMapper } from '@/routes/transactions/mappers/transfers/transfer.mapper';
106✔
39
import {
106✔
40
  getAddress,
41
  isAddress,
42
  isAddressEqual,
43
  parseEther,
44
  parseUnits,
45
} from 'viem';
46
import { LoggingService, ILoggingService } from '@/logging/logging.interface';
106✔
47
import { MultisigTransactionNoteMapper } from '@/routes/transactions/mappers/multisig-transactions/multisig-transaction-note.mapper';
106✔
48
import { LogType } from '@/domain/common/entities/log-type.entity';
106✔
49
import { TXSMultisigTransaction } from '@/routes/transactions/entities/txs-multisig-transaction.entity';
106✔
50
import { TXSMultisigTransactionPage } from '@/routes/transactions/entities/txs-multisig-transaction-page.entity';
51
import { TXSCreationTransaction } from '@/routes/transactions/entities/txs-creation-transaction.entity';
106✔
52
import { ITokenRepository } from '@/domain/tokens/token.repository.interface';
106✔
53
import { IConfigurationService } from '@/config/configuration.service.interface';
106✔
54

55
@Injectable()
56
export class TransactionsService {
106✔
57
  private readonly isFilterValueParsingEnabled: boolean;
58

59
  constructor(
60
    @Inject(ISafeRepository) private readonly safeRepository: SafeRepository,
1,596✔
61
    private readonly multisigTransactionMapper: MultisigTransactionMapper,
1,596✔
62
    private readonly transferMapper: TransferMapper,
1,596✔
63
    private readonly moduleTransactionMapper: ModuleTransactionMapper,
1,596✔
64
    private readonly queuedItemsMapper: QueuedItemsMapper,
1,596✔
65
    private readonly transactionsHistoryMapper: TransactionsHistoryMapper,
1,596✔
66
    private readonly transactionPreviewMapper: TransactionPreviewMapper,
1,596✔
67
    private readonly moduleTransactionDetailsMapper: ModuleTransactionDetailsMapper,
1,596✔
68
    private readonly multisigTransactionDetailsMapper: MultisigTransactionDetailsMapper,
1,596✔
69
    private readonly multisigTransactionNoteMapper: MultisigTransactionNoteMapper,
1,596✔
70
    private readonly transferDetailsMapper: TransferDetailsMapper,
1,596✔
71
    @Inject(LoggingService) private readonly loggingService: ILoggingService,
1,596✔
72
    @Inject(ITokenRepository)
73
    private readonly tokenRepository: ITokenRepository,
1,596✔
74
    @Inject(IConfigurationService)
75
    private readonly configurationService: IConfigurationService,
1,596✔
76
  ) {
77
    this.isFilterValueParsingEnabled = this.configurationService.getOrThrow(
1,596✔
78
      'features.filterValueParsing',
79
    );
80
  }
81

82
  async getById(args: {
83
    chainId: string;
84
    txId: string;
85
  }): Promise<TransactionDetails> {
86
    const [txType, safeAddress, id] = args.txId.split(TRANSACTION_ID_SEPARATOR);
40✔
87

88
    switch (txType) {
40✔
89
      case MODULE_TRANSACTION_PREFIX: {
90
        const [tx] = await Promise.all([
8✔
91
          this.safeRepository.getModuleTransaction({
92
            chainId: args.chainId,
93
            moduleTransactionId: id,
94
          }),
95
        ]);
96
        return this.moduleTransactionDetailsMapper.mapDetails(args.chainId, tx);
2✔
97
      }
98

99
      case TRANSFER_PREFIX: {
100
        if (!isAddress(safeAddress)) {
4!
101
          throw new BadRequestException('Invalid transaction ID');
×
102
        }
103

104
        const [transfer, safe] = await Promise.all([
4✔
105
          this.safeRepository.getTransfer({
106
            chainId: args.chainId,
107
            transferId: id,
108
          }),
109
          this.safeRepository.getSafe({
110
            chainId: args.chainId,
111
            // We can't checksum outside of case as some IDs don't contain addresses
112
            address: getAddress(safeAddress),
113
          }),
114
        ]);
115
        return this.transferDetailsMapper.mapDetails(
2✔
116
          args.chainId,
117
          transfer,
118
          safe,
119
        );
120
      }
121

122
      case MULTISIG_TRANSACTION_PREFIX: {
123
        if (!isAddress(safeAddress)) {
26!
124
          throw new BadRequestException('Invalid transaction ID');
×
125
        }
126

127
        const [tx, safe] = await Promise.all([
26✔
128
          this.safeRepository.getMultiSigTransaction({
129
            chainId: args.chainId,
130
            safeTransactionHash: id,
131
          }),
132
          this.safeRepository.getSafe({
133
            chainId: args.chainId,
134
            // We can't checksum outside of case as some IDs don't contain addresses
135
            address: getAddress(safeAddress),
136
          }),
137
        ]);
138

139
        if (!isAddressEqual(tx.safe, safe.address)) {
14✔
140
          throw new BadRequestException('Invalid transaction ID');
2✔
141
        }
142

143
        return this.multisigTransactionDetailsMapper.mapDetails(
12✔
144
          args.chainId,
145
          tx,
146
          safe,
147
        );
148
      }
149

150
      // txId is safeTxHash
151
      default: {
152
        const tx = await this.safeRepository.getMultiSigTransaction({
2✔
153
          chainId: args.chainId,
154
          safeTransactionHash: args.txId,
155
        });
156
        const safe = await this.safeRepository.getSafe({
2✔
157
          chainId: args.chainId,
158
          address: tx.safe,
159
        });
160
        return this.multisigTransactionDetailsMapper.mapDetails(
2✔
161
          args.chainId,
162
          tx,
163
          safe,
164
        );
165
      }
166
    }
167
  }
168

169
  async getDomainMultisigTransactionBySafeTxHash(args: {
170
    chainId: string;
171
    safeTxHash: string;
172
  }): Promise<TXSMultisigTransaction> {
173
    const tx = await this.safeRepository.getMultiSigTransactionWithNoCache({
×
174
      chainId: args.chainId,
175
      safeTransactionHash: args.safeTxHash,
176
    });
177
    return new TXSMultisigTransaction(tx);
×
178
  }
179

180
  async getMultisigTransactions(args: {
181
    chainId: string;
182
    routeUrl: Readonly<URL>;
183
    paginationData: PaginationData;
184
    safeAddress: `0x${string}`;
185
    executionDateGte?: string;
186
    executionDateLte?: string;
187
    to?: `0x${string}`;
188
    value?: string;
189
    nonce?: string;
190
    executed?: boolean;
191
  }): Promise<Partial<Page<MultisigTransaction>>> {
192
    const domainTransactions =
193
      await this.safeRepository.getMultisigTransactions({
16✔
194
        ...args,
195
        ...(this.isFilterValueParsingEnabled &&
17✔
196
          args.value && {
197
            value: await this.parseTokenValue({
198
              ...args,
199
              value: args.value,
200
            }),
201
          }),
202
        limit: args.paginationData.limit,
203
        offset: args.paginationData.offset,
204
      });
205

206
    const safeInfo = await this.safeRepository.getSafe({
8✔
207
      chainId: args.chainId,
208
      address: args.safeAddress,
209
    });
210
    await this.multisigTransactionMapper.prefetchAddressInfos({
8✔
211
      chainId: args.chainId,
212
      transactions: domainTransactions.results,
213
    });
214
    const results = await Promise.all(
8✔
215
      domainTransactions.results.map(
216
        async (domainTransaction) =>
217
          new MultisigTransaction(
8✔
218
            await this.multisigTransactionMapper.mapTransaction(
219
              args.chainId,
220
              domainTransaction,
221
              safeInfo,
222
            ),
223
            ConflictType.None,
224
          ),
225
      ),
226
    );
227
    const nextURL = cursorUrlFromLimitAndOffset(
8✔
228
      args.routeUrl,
229
      domainTransactions.next,
230
    );
231
    const previousURL = cursorUrlFromLimitAndOffset(
8✔
232
      args.routeUrl,
233
      domainTransactions.previous,
234
    );
235

236
    return {
8✔
237
      next: nextURL?.toString() ?? null,
24!
238
      previous: previousURL?.toString() ?? null,
24!
239
      results,
240
    };
241
  }
242

243
  async getDomainMultisigTransactions(args: {
244
    safeAddress: `0x${string}`;
245
    chainId: string;
246
    // Transaction Service parameters
247
    failed?: boolean;
248
    modified__lt?: string;
249
    modified__gt?: string;
250
    modified__lte?: string;
251
    modified__gte?: string;
252
    nonce__lt?: number;
253
    nonce__gt?: number;
254
    nonce__lte?: number;
255
    nonce__gte?: number;
256
    nonce?: number;
257
    safe_tx_hash?: string;
258
    to?: string;
259
    value__lt?: number;
260
    value__gt?: number;
261
    value?: number;
262
    executed?: boolean;
263
    has_confirmations?: boolean;
264
    trusted?: boolean;
265
    execution_date__gte?: string;
266
    execution_date__lte?: string;
267
    submission_date__gte?: string;
268
    submission_date__lte?: string;
269
    transaction_hash?: string;
270
    ordering?: string;
271
    limit?: number;
272
    offset?: number;
273
  }): Promise<TXSMultisigTransactionPage> {
274
    return await this.safeRepository.getMultisigTransactionsWithNoCache(args);
×
275
  }
276

277
  async deleteTransaction(args: {
278
    chainId: string;
279
    safeTxHash: string;
280
    signature: string;
281
  }): Promise<void> {
282
    return await this.safeRepository.deleteTransaction(args);
6✔
283
  }
284

285
  async addConfirmation(args: {
286
    chainId: string;
287
    safeTxHash: string;
288
    addConfirmationDto: AddConfirmationDto;
289
  }): Promise<TransactionDetails> {
290
    await this.safeRepository.addConfirmation(args);
22✔
291
    const transaction = await this.safeRepository.getMultiSigTransaction({
8✔
292
      chainId: args.chainId,
293
      safeTransactionHash: args.safeTxHash,
294
    });
295
    const safe = await this.safeRepository.getSafe({
8✔
296
      chainId: args.chainId,
297
      address: transaction.safe,
298
    });
299

300
    return this.multisigTransactionDetailsMapper.mapDetails(
8✔
301
      args.chainId,
302
      transaction,
303
      safe,
304
    );
305
  }
306

307
  async getModuleTransactions(args: {
308
    chainId: string;
309
    routeUrl: Readonly<URL>;
310
    safeAddress: `0x${string}`;
311
    to?: string;
312
    module?: string;
313
    txHash?: string;
314
    paginationData?: PaginationData;
315
  }): Promise<Page<ModuleTransaction>> {
316
    const domainTransactions = await this.safeRepository.getModuleTransactions({
10✔
317
      ...args,
318
      limit: args.paginationData?.limit,
15!
319
      offset: args.paginationData?.offset,
15!
320
    });
321

322
    const results = await Promise.all(
2✔
323
      domainTransactions.results.map(
324
        async (domainTransaction) =>
325
          new ModuleTransaction(
4✔
326
            await this.moduleTransactionMapper.mapTransaction(
327
              args.chainId,
328
              domainTransaction,
329
            ),
330
          ),
331
      ),
332
    );
333
    const nextURL = cursorUrlFromLimitAndOffset(
2✔
334
      args.routeUrl,
335
      domainTransactions.next,
336
    );
337
    const previousURL = cursorUrlFromLimitAndOffset(
2✔
338
      args.routeUrl,
339
      domainTransactions.previous,
340
    );
341

342
    return {
2✔
343
      count: domainTransactions.count,
344
      next: nextURL?.toString() ?? null,
5!
345
      previous: previousURL?.toString() ?? null,
5!
346
      results,
347
    };
348
  }
349

350
  async getIncomingTransfers(args: {
351
    chainId: string;
352
    routeUrl: Readonly<URL>;
353
    safeAddress: `0x${string}`;
354
    executionDateGte?: string;
355
    executionDateLte?: string;
356
    to?: `0x${string}`;
357
    value?: string;
358
    tokenAddress?: `0x${string}`;
359
    paginationData?: PaginationData;
360
    onlyTrusted: boolean;
361
  }): Promise<Partial<Page<IncomingTransfer>>> {
362
    const transfers = await this.safeRepository.getIncomingTransfers({
22✔
363
      ...args,
364
      ...(this.isFilterValueParsingEnabled &&
11!
365
        args.value && {
366
          value: await this.parseTokenValue({
367
            ...args,
368
            value: args.value,
369
          }),
370
        }),
371
      limit: args.paginationData?.limit,
33!
372
      offset: args.paginationData?.offset,
33!
373
    });
374

375
    const safeInfo = await this.safeRepository.getSafe({
14✔
376
      chainId: args.chainId,
377
      address: args.safeAddress,
378
    });
379
    const results = (
14✔
380
      await this.transferMapper.mapTransfers({
381
        chainId: args.chainId,
382
        transfers: transfers.results,
383
        safe: safeInfo,
384
        onlyTrusted: args.onlyTrusted,
385
      })
386
    ).map((incomingTransfer) => new IncomingTransfer(incomingTransfer));
12✔
387

388
    const nextURL = cursorUrlFromLimitAndOffset(args.routeUrl, transfers.next);
14✔
389
    const previousURL = cursorUrlFromLimitAndOffset(
14✔
390
      args.routeUrl,
391
      transfers.previous,
392
    );
393

394
    return {
14✔
395
      next: nextURL?.toString() ?? null,
42!
396
      previous: previousURL?.toString() ?? null,
42!
397
      results,
398
    };
399
  }
400

401
  async previewTransaction(args: {
402
    chainId: string;
403
    safeAddress: `0x${string}`;
404
    previewTransactionDto: PreviewTransactionDto;
405
  }): Promise<TransactionPreview> {
406
    const safe = await this.safeRepository.getSafe({
78✔
407
      chainId: args.chainId,
408
      address: args.safeAddress,
409
    });
410
    return this.transactionPreviewMapper.mapTransactionPreview(
78✔
411
      args.chainId,
412
      safe,
413
      args.previewTransactionDto,
414
    );
415
  }
416

417
  async getTransactionQueue(args: {
418
    chainId: string;
419
    routeUrl: Readonly<URL>;
420
    safeAddress: `0x${string}`;
421
    paginationData: PaginationData;
422
    trusted?: boolean;
423
  }): Promise<Page<QueuedItem>> {
424
    const pagination = this.getAdjustedPaginationForQueue(args.paginationData);
28✔
425
    const safeInfo = await this.safeRepository.getSafe({
28✔
426
      chainId: args.chainId,
427
      address: args.safeAddress,
428
    });
429
    const transactions = await this.safeRepository.getTransactionQueue({
28✔
430
      chainId: args.chainId,
431
      safe: safeInfo,
432
      limit: pagination.limit,
433
      offset: pagination.offset,
434
      trusted: args.trusted,
435
    });
436

437
    const nextURL = buildNextPageURL(args.routeUrl, transactions.count);
16✔
438
    const previousURL = buildPreviousPageURL(args.routeUrl);
16✔
439
    const results = await this.queuedItemsMapper.getQueuedItems(
16✔
440
      this.adjustTransactionsPage(transactions),
441
      safeInfo,
442
      args.chainId,
443
      this.getPreviousPageLastNonce(transactions, args.paginationData),
444
      this.getNextPageFirstNonce(transactions),
445
    );
446

447
    return {
6✔
448
      count: results.length,
449
      next: nextURL?.toString() ?? null,
16✔
450
      previous: previousURL?.toString() ?? null,
16✔
451
      results,
452
    };
453
  }
454

455
  private getAdjustedPaginationForHistory(
456
    paginationData: PaginationData,
457
  ): PaginationData {
458
    if (paginationData.offset > 0) {
72✔
459
      return new PaginationData(
2✔
460
        paginationData.limit + 1,
461
        paginationData.offset - 1,
462
      );
463
    }
464
    return paginationData;
70✔
465
  }
466

467
  async getTransactionHistory(args: {
468
    chainId: string;
469
    routeUrl: Readonly<URL>;
470
    safeAddress: `0x${string}`;
471
    paginationData: PaginationData;
472
    timezoneOffsetMs: number;
473
    onlyTrusted: boolean;
474
    showImitations: boolean;
475
    timezone?: string;
476
  }): Promise<TransactionItemPage> {
477
    const paginationDataAdjusted = this.getAdjustedPaginationForHistory(
72✔
478
      args.paginationData,
479
    );
480
    const domainTransactions = await this.safeRepository.getTransactionHistory({
72✔
481
      chainId: args.chainId,
482
      safeAddress: args.safeAddress,
483
      limit: paginationDataAdjusted.limit,
484
      offset: paginationDataAdjusted.offset,
485
    });
486
    const nextURL = buildNextPageURL(args.routeUrl, domainTransactions.count);
66✔
487
    const previousURL = buildPreviousPageURL(args.routeUrl);
66✔
488
    if (nextURL == null) {
66✔
489
      // If creation is not indexed, we shouldn't block the entire history
490
      try {
6✔
491
        const creationTransaction =
492
          await this.safeRepository.getCreationTransaction({
6✔
493
            chainId: args.chainId,
494
            safeAddress: args.safeAddress,
495
          });
496
        domainTransactions.results.push(creationTransaction);
4✔
497
      } catch (error) {
498
        this.loggingService.warn(error);
2✔
499
      }
500
    }
501
    const safeInfo = await this.safeRepository.getSafe({
66✔
502
      chainId: args.chainId,
503
      address: args.safeAddress,
504
    });
505
    const results = await this.transactionsHistoryMapper.mapTransactionsHistory(
66✔
506
      args.chainId,
507
      domainTransactions.results,
508
      safeInfo,
509
      args.paginationData.offset,
510
      args.timezoneOffsetMs,
511
      args.onlyTrusted,
512
      args.showImitations,
513
      args.timezone,
514
    );
515

516
    return {
66✔
517
      count: domainTransactions.count,
518
      next: nextURL?.toString() ?? null,
195✔
519
      previous: previousURL?.toString() ?? null,
166✔
520
      results,
521
    };
522
  }
523

524
  async proposeTransaction(args: {
525
    chainId: string;
526
    safeAddress: `0x${string}`;
527
    proposeTransactionDto: ProposeTransactionDto;
528
  }): Promise<TransactionDetails> {
529
    this.logProposeTx(args);
36✔
530
    args.proposeTransactionDto.origin = this.verifyOrigin(
36✔
531
      args.proposeTransactionDto,
532
    );
533
    await this.safeRepository.proposeTransaction(args);
36✔
534

535
    const safe = await this.safeRepository.getSafe({
16✔
536
      chainId: args.chainId,
537
      address: args.safeAddress,
538
    });
539
    const domainTransaction = await this.safeRepository.getMultiSigTransaction({
16✔
540
      chainId: args.chainId,
541
      safeTransactionHash: args.proposeTransactionDto.safeTxHash,
542
    });
543

544
    return this.multisigTransactionDetailsMapper.mapDetails(
16✔
545
      args.chainId,
546
      domainTransaction,
547
      safe,
548
    );
549
  }
550

551
  async getCreationTransaction(args: {
552
    chainId: string;
553
    safeAddress: `0x${string}`;
554
  }): Promise<CreationTransaction> {
555
    return this.safeRepository.getCreationTransaction(args);
8✔
556
  }
557

558
  async getDomainCreationTransaction(args: {
559
    chainId: string;
560
    safeAddress: `0x${string}`;
561
  }): Promise<TXSCreationTransaction> {
562
    const tx = await this.safeRepository.getCreationTransaction(args);
×
563
    return new TXSCreationTransaction(tx);
×
564
  }
565

566
  /**
567
   * Adjusts the pagination data to return extra items in both "edges" of the current page:
568
   * - If no pagination data info, then return the original pagination data.
569
   * - If it is the first page (offset 0), then return offset: 0, limit: limit + 1.
570
   * - If it is not the first page, then return offset: offset - 1, limit: limit + 2.
571
   * @param paginationData pagination data to adjust.
572
   * @returns pagination data adjusted.
573
   */
574
  private getAdjustedPaginationForQueue(
575
    paginationData: PaginationData,
576
  ): PaginationData {
577
    if (!paginationData.limit || !paginationData.offset) {
28✔
578
      return paginationData;
26✔
579
    }
580
    if (paginationData.offset === 0) {
2!
581
      return new PaginationData(
×
582
        paginationData.limit + 1,
583
        paginationData.offset,
584
      );
585
    } else {
586
      return new PaginationData(
2✔
587
        paginationData.limit + 2,
588
        paginationData.offset - 1,
589
      );
590
    }
591
  }
592

593
  private async parseTokenValue(args: {
594
    chainId: string;
595
    value: string;
596
    tokenAddress?: `0x${string}`;
597
  }): Promise<string> {
598
    if (!args.tokenAddress) {
2✔
599
      return parseEther(args.value).toString();
2✔
600
    }
601
    const token = await this.tokenRepository.getToken({
×
602
      chainId: args.chainId,
603
      address: args.tokenAddress,
604
    });
605
    return parseUnits(args.value, token.decimals).toString();
×
606
  }
607

608
  private getNextPageFirstNonce(
609
    page: Page<DomainMultisigTransaction>,
610
  ): number | null {
611
    return this.hasNextPage(page) ? this.getLastTransactionNonce(page) : null;
16✔
612
  }
613

614
  private getPreviousPageLastNonce(
615
    page: Page<DomainMultisigTransaction>,
616
    paginationData?: PaginationData,
617
  ): number | null {
618
    return paginationData && paginationData.offset
16✔
619
      ? this.getFirstTransactionNonce(page)
620
      : null;
621
  }
622

623
  /**
624
   * If the page has next page, returns a copy of the original transactions page without its last element.
625
   * Otherwise the original page of transactions is returned.
626
   *
627
   * @param page page of Transactions.
628
   * @returns transactions array without its first element if there is next page.
629
   */
630
  private adjustTransactionsPage(
631
    page: Page<DomainMultisigTransaction>,
632
  ): Page<DomainMultisigTransaction> {
633
    return this.hasNextPage(page)
16✔
634
      ? { ...page, results: page.results.slice(0, -1) }
635
      : page;
636
  }
637

638
  /**
639
   * Checks the if page contains a next cursor.
640
   */
641
  private hasNextPage(page: Page<DomainMultisigTransaction>): boolean {
642
    return page.next !== null;
32✔
643
  }
644

645
  private getFirstTransactionNonce(
646
    page: Page<DomainMultisigTransaction>,
647
  ): number | null {
648
    return page.results[0]?.nonce ?? null;
2!
649
  }
650

651
  private getLastTransactionNonce(
652
    page: Page<DomainMultisigTransaction>,
653
  ): number | null {
654
    return page.results.at(-1)?.nonce ?? null;
2!
655
  }
656

657
  private verifyOrigin(transaction: ProposeTransactionDto): string | null {
658
    if (transaction.origin) {
36✔
659
      try {
36✔
660
        const note = this.multisigTransactionNoteMapper.mapTxNote(transaction);
36✔
661

662
        const origin = JSON.parse(transaction.origin);
36✔
UNCOV
663
        origin.note = note;
×
664

665
        return JSON.stringify(origin);
×
666
      } catch {
667
        // If the origin is not a valid JSON, we return null
668
      }
669
    }
670

671
    return null;
36✔
672
  }
673

674
  private logProposeTx(
675
    args: Parameters<TransactionsService['proposeTransaction']>[0],
676
  ): void {
677
    this.loggingService.info({
36✔
678
      transaction: args.proposeTransactionDto,
679
      safeAddress: args.safeAddress,
680
      chainId: args.chainId,
681
      type: LogType.TransactionPropose,
682
    });
683
  }
684
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc