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

safe-global / safe-client-gateway / 10939516417

19 Sep 2024 10:51AM UTC coverage: 90.785% (+0.02%) from 90.77%
10939516417

Pull #1921

github

iamacook
Merge branch 'main' into aggregate-event-logs
Pull Request #1921: Aggregate event logs for unsupported chains

2551 of 3182 branches covered (80.17%)

Branch coverage included in aggregate %.

30 of 30 new or added lines in 3 files covered. (100.0%)

18 existing lines in 4 files now uncovered.

8325 of 8798 relevant lines covered (94.62%)

444.23 hits per line

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

85.8
/src/routes/transactions/transactions-view.service.ts
1
import { IConfigurationService } from '@/config/configuration.service.interface';
96✔
2
import { IDataDecodedRepository } from '@/domain/data-decoder/data-decoded.repository.interface';
96✔
3
import { DataDecoded } from '@/domain/data-decoder/entities/data-decoded.entity';
4
import { KilnDecoder } from '@/domain/staking/contracts/decoders/kiln-decoder.helper';
96✔
5
import { ComposableCowDecoder } from '@/domain/swaps/contracts/decoders/composable-cow-decoder.helper';
96✔
6
import { GPv2Decoder } from '@/domain/swaps/contracts/decoders/gp-v2-decoder.helper';
96✔
7
import { OrderStatus } from '@/domain/swaps/entities/order.entity';
96✔
8
import { ISwapsRepository } from '@/domain/swaps/swaps.repository';
96✔
9
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
96✔
10
import { TransactionDataDto } from '@/routes/common/entities/transaction-data.dto.entity';
11
import {
96✔
12
  BaselineConfirmationView,
13
  ConfirmationView,
14
  CowSwapConfirmationView,
15
  CowSwapTwapConfirmationView,
16
} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity';
17
import { NativeStakingDepositConfirmationView } from '@/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity';
96✔
18
import { NativeStakingValidatorsExitConfirmationView } from '@/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity';
96✔
19
import { NativeStakingWithdrawConfirmationView } from '@/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity';
96✔
20
import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity';
96✔
21
import { KilnNativeStakingHelper } from '@/routes/transactions/helpers/kiln-native-staking.helper';
96✔
22
import { SwapAppsHelper } from '@/routes/transactions/helpers/swap-apps.helper';
96✔
23
import { SwapOrderHelper } from '@/routes/transactions/helpers/swap-order.helper';
96✔
24
import { TwapOrderHelper } from '@/routes/transactions/helpers/twap-order.helper';
96✔
25
import { NativeStakingMapper } from '@/routes/transactions/mappers/common/native-staking.mapper';
96✔
26
import { Inject, Injectable } from '@nestjs/common';
96✔
27

28
@Injectable({})
29
export class TransactionsViewService {
96✔
30
  private readonly isNativeStakingEnabled: boolean;
31

32
  constructor(
33
    @Inject(IDataDecodedRepository)
34
    private readonly dataDecodedRepository: IDataDecodedRepository,
66✔
35
    private readonly gpv2Decoder: GPv2Decoder,
66✔
36
    private readonly swapOrderHelper: SwapOrderHelper,
66✔
37
    @Inject(LoggingService) private readonly loggingService: ILoggingService,
66✔
38
    private readonly twapOrderHelper: TwapOrderHelper,
66✔
39
    @Inject(ISwapsRepository)
40
    private readonly swapsRepository: ISwapsRepository,
66✔
41
    private readonly composableCowDecoder: ComposableCowDecoder,
66✔
42
    private readonly swapAppsHelper: SwapAppsHelper,
66✔
43
    @Inject(IConfigurationService)
44
    private readonly configurationService: IConfigurationService,
66✔
45
    private readonly kilnNativeStakingHelper: KilnNativeStakingHelper,
66✔
46
    private readonly nativeStakingMapper: NativeStakingMapper,
66✔
47
    private readonly kilnDecoder: KilnDecoder,
66✔
48
  ) {
49
    this.isNativeStakingEnabled = this.configurationService.getOrThrow<boolean>(
66✔
50
      'features.nativeStaking',
51
    );
52
  }
53

54
  async getTransactionConfirmationView(args: {
55
    chainId: string;
56
    safeAddress: `0x${string}`;
57
    transactionDataDto: TransactionDataDto;
58
  }): Promise<ConfirmationView> {
59
    const dataDecoded = await this.dataDecodedRepository
66✔
60
      .getDataDecoded({
61
        chainId: args.chainId,
62
        data: args.transactionDataDto.data,
63
        to: args.transactionDataDto.to,
64
      })
65
      .catch(() => {
66
        // Fallback for unverified contracts
67
        return {
6✔
68
          method: '',
69
          parameters: null,
70
        };
71
      });
72

73
    const swapOrderData = this.swapOrderHelper.findSwapOrder(
66✔
74
      args.transactionDataDto.data,
75
    );
76

77
    const twapSwapOrderData = args.transactionDataDto.to
66✔
78
      ? this.twapOrderHelper.findTwapOrder({
79
          to: args.transactionDataDto.to,
80
          data: args.transactionDataDto.data,
81
        })
82
      : null;
83

84
    const nativeStakingDepositTransaction =
85
      this.isNativeStakingEnabled &&
66✔
86
      (await this.kilnNativeStakingHelper.findDepositTransaction({
87
        chainId: args.chainId,
88
        ...args.transactionDataDto,
89
      }));
90

91
    const nativeStakingValidatorsExitTransaction =
92
      this.isNativeStakingEnabled &&
66✔
93
      (await this.kilnNativeStakingHelper.findValidatorsExitTransaction({
94
        chainId: args.chainId,
95
        ...args.transactionDataDto,
96
      }));
97

98
    const nativeStakingWithdrawTransaction =
99
      this.isNativeStakingEnabled &&
66✔
100
      (await this.kilnNativeStakingHelper.findWithdrawTransaction({
101
        chainId: args.chainId,
102
        ...args.transactionDataDto,
103
      }));
104

105
    if (
66✔
106
      !swapOrderData &&
120✔
107
      !twapSwapOrderData &&
108
      !nativeStakingDepositTransaction &&
109
      !nativeStakingValidatorsExitTransaction &&
110
      !nativeStakingWithdrawTransaction
111
    ) {
112
      return new BaselineConfirmationView({
22✔
113
        method: dataDecoded.method,
114
        parameters: dataDecoded.parameters,
115
      });
116
    }
117

118
    try {
44✔
119
      if (swapOrderData) {
44✔
120
        return await this.getSwapOrderConfirmationView({
12✔
121
          chainId: args.chainId,
122
          data: swapOrderData,
123
          dataDecoded,
124
        });
125
      } else if (twapSwapOrderData) {
32✔
126
        return await this.getTwapOrderConfirmationView({
2✔
127
          chainId: args.chainId,
128
          safeAddress: args.safeAddress,
129
          data: twapSwapOrderData,
130
          dataDecoded,
131
        });
132
      } else if (nativeStakingDepositTransaction) {
30✔
133
        return await this.getNativeStakingDepositConfirmationView({
14✔
134
          ...nativeStakingDepositTransaction,
135
          chainId: args.chainId,
136
          dataDecoded,
137
          value: args.transactionDataDto.value ?? null,
21✔
138
        });
139
      } else if (nativeStakingValidatorsExitTransaction) {
16✔
140
        return await this.getNativeStakingValidatorsExitConfirmationView({
8✔
141
          ...nativeStakingValidatorsExitTransaction,
142
          chainId: args.chainId,
143
          dataDecoded,
144
        });
145
      } else if (nativeStakingWithdrawTransaction) {
8!
146
        return await this.getNativeStakingWithdrawConfirmationView({
8✔
147
          ...nativeStakingWithdrawTransaction,
148
          chainId: args.chainId,
149
          dataDecoded,
150
        });
151
      } else {
152
        // Should not reach here
UNCOV
153
        throw new Error('No swap order data found');
×
154
      }
155
    } catch (error) {
156
      this.loggingService.warn(error);
24✔
157
      return new BaselineConfirmationView({
24✔
158
        method: dataDecoded.method,
159
        parameters: dataDecoded.parameters,
160
      });
161
    }
162
  }
163

164
  private async getSwapOrderConfirmationView(args: {
165
    chainId: string;
166
    data: `0x${string}`;
167
    dataDecoded: DataDecoded;
168
  }): Promise<CowSwapConfirmationView> {
169
    const orderUid: `0x${string}` | null =
170
      this.gpv2Decoder.getOrderUidFromSetPreSignature(args.data);
12✔
171
    if (!orderUid) {
12!
UNCOV
172
      throw new Error('Order UID not found in transaction data');
×
173
    }
174

175
    const order = await this.swapOrderHelper.getOrder({
12✔
176
      chainId: args.chainId,
177
      orderUid,
178
    });
179

180
    if (!this.swapAppsHelper.isAppAllowed(order)) {
10✔
181
      throw new Error(`Unsupported App: ${order.fullAppData?.appCode}`);
2!
182
    }
183

184
    const [sellToken, buyToken] = await Promise.all([
8✔
185
      this.swapOrderHelper.getToken({
186
        chainId: args.chainId,
187
        address: order.sellToken,
188
      }),
189
      this.swapOrderHelper.getToken({
190
        chainId: args.chainId,
191
        address: order.buyToken,
192
      }),
193
    ]);
194

195
    return new CowSwapConfirmationView({
4✔
196
      method: args.dataDecoded.method,
197
      parameters: args.dataDecoded.parameters,
198
      uid: order.uid,
199
      status: order.status,
200
      kind: order.kind,
201
      orderClass: order.class,
202
      validUntil: order.validTo,
203
      sellAmount: order.sellAmount.toString(),
204
      buyAmount: order.buyAmount.toString(),
205
      executedSellAmount: order.executedSellAmount.toString(),
206
      executedBuyAmount: order.executedBuyAmount.toString(),
207
      explorerUrl: this.swapOrderHelper.getOrderExplorerUrl(order).toString(),
208
      sellToken: new TokenInfo({
209
        address: sellToken.address,
210
        decimals: sellToken.decimals,
211
        logoUri: sellToken.logoUri,
212
        name: sellToken.name,
213
        symbol: sellToken.symbol,
214
        trusted: sellToken.trusted,
215
      }),
216
      buyToken: new TokenInfo({
217
        address: buyToken.address,
218
        decimals: buyToken.decimals,
219
        logoUri: buyToken.logoUri,
220
        name: buyToken.name,
221
        symbol: buyToken.symbol,
222
        trusted: buyToken.trusted,
223
      }),
224
      executedSurplusFee: order.executedSurplusFee?.toString() ?? null,
11✔
225
      receiver: order.receiver,
226
      owner: order.owner,
227
      fullAppData: order.fullAppData,
228
    });
229
  }
230

231
  private async getTwapOrderConfirmationView(args: {
232
    chainId: string;
233
    safeAddress: `0x${string}`;
234
    data: `0x${string}`;
235
    dataDecoded: DataDecoded;
236
  }): Promise<CowSwapTwapConfirmationView> {
237
    // Decode `staticInput` of `createWithContextCall`
238
    const twapStruct = this.composableCowDecoder.decodeTwapStruct(args.data);
2✔
239
    const twapOrderData =
240
      this.twapOrderHelper.twapStructToPartialOrderInfo(twapStruct);
2✔
241

242
    // Generate parts of the TWAP order
243
    const twapParts = this.twapOrderHelper.generateTwapOrderParts({
2✔
244
      twapStruct,
245
      executionDate: new Date(),
246
      chainId: args.chainId,
247
    });
248

249
    // Decode hash of `appData`
250
    const fullAppData = await this.swapsRepository.getFullAppData(
2✔
251
      args.chainId,
252
      twapStruct.appData,
253
    );
254

255
    if (!this.swapAppsHelper.isAppAllowed(fullAppData)) {
2!
UNCOV
256
      throw new Error(`Unsupported App: ${fullAppData.fullAppData?.appCode}`);
×
257
    }
258

259
    const [buyToken, sellToken] = await Promise.all([
2✔
260
      this.swapOrderHelper.getToken({
261
        chainId: args.chainId,
262
        address: twapStruct.buyToken,
263
      }),
264
      this.swapOrderHelper.getToken({
265
        chainId: args.chainId,
266
        address: twapStruct.sellToken,
267
      }),
268
    ]);
269

270
    return new CowSwapTwapConfirmationView({
2✔
271
      method: args.dataDecoded.method,
272
      parameters: args.dataDecoded.parameters,
273
      status: OrderStatus.PreSignaturePending,
274
      kind: twapOrderData.kind,
275
      class: twapOrderData.class,
276
      activeOrderUid: null,
277
      validUntil: Math.max(...twapParts.map((order) => order.validTo)),
4✔
278
      sellAmount: twapOrderData.sellAmount,
279
      buyAmount: twapOrderData.buyAmount,
280
      executedSellAmount: '0',
281
      executedBuyAmount: '0',
282
      executedSurplusFee: '0',
283
      sellToken: new TokenInfo({
284
        address: sellToken.address,
285
        decimals: sellToken.decimals,
286
        logoUri: sellToken.logoUri,
287
        name: sellToken.name,
288
        symbol: sellToken.symbol,
289
        trusted: sellToken.trusted,
290
      }),
291
      buyToken: new TokenInfo({
292
        address: buyToken.address,
293
        decimals: buyToken.decimals,
294
        logoUri: buyToken.logoUri,
295
        name: buyToken.name,
296
        symbol: buyToken.symbol,
297
        trusted: buyToken.trusted,
298
      }),
299
      receiver: twapStruct.receiver,
300
      owner: args.safeAddress,
301
      fullAppData: fullAppData.fullAppData,
302
      numberOfParts: twapOrderData.numberOfParts,
303
      partSellAmount: twapStruct.partSellAmount.toString(),
304
      minPartLimit: twapStruct.minPartLimit.toString(),
305
      timeBetweenParts: twapOrderData.timeBetweenParts,
306
      durationOfPart: twapOrderData.durationOfPart,
307
      startTime: twapOrderData.startTime,
308
    });
309
  }
310

311
  private async getNativeStakingDepositConfirmationView(args: {
312
    chainId: string;
313
    to: `0x${string}`;
314
    data: `0x${string}`;
315
    dataDecoded: DataDecoded;
316
    value: string | null;
317
  }): Promise<NativeStakingDepositConfirmationView> {
318
    const dataDecoded =
319
      args.dataDecoded.method !== ''
14✔
320
        ? args.dataDecoded
321
        : this.kilnDecoder.decodeDeposit(args.data);
322
    if (!dataDecoded) {
14!
UNCOV
323
      throw new Error('Transaction data could not be decoded');
×
324
    }
325
    const depositInfo = await this.nativeStakingMapper.mapDepositInfo({
14✔
326
      chainId: args.chainId,
327
      to: args.to,
328
      value: args.value,
329
      isConfirmed: false,
330
      depositExecutionDate: null,
331
    });
332
    return new NativeStakingDepositConfirmationView({
6✔
333
      method: dataDecoded.method,
334
      parameters: dataDecoded.parameters,
335
      ...depositInfo,
336
    });
337
  }
338

339
  private async getNativeStakingValidatorsExitConfirmationView(args: {
340
    chainId: string;
341
    to: `0x${string}`;
342
    data: `0x${string}`;
343
    dataDecoded: DataDecoded;
344
  }): Promise<NativeStakingValidatorsExitConfirmationView> {
345
    const dataDecoded =
346
      args.dataDecoded.method !== ''
8✔
347
        ? args.dataDecoded
348
        : this.kilnDecoder.decodeValidatorsExit(args.data);
349
    if (!dataDecoded) {
8!
UNCOV
350
      throw new Error('Transaction data could not be decoded');
×
351
    }
352
    const validatorsExitInfo =
353
      await this.nativeStakingMapper.mapValidatorsExitInfo({
8✔
354
        chainId: args.chainId,
355
        to: args.to,
356
        transaction: null,
357
        dataDecoded,
358
      });
359
    return new NativeStakingValidatorsExitConfirmationView({
4✔
360
      method: dataDecoded.method,
361
      parameters: dataDecoded.parameters,
362
      ...validatorsExitInfo,
363
    });
364
  }
365

366
  private async getNativeStakingWithdrawConfirmationView(args: {
367
    chainId: string;
368
    to: `0x${string}`;
369
    data: `0x${string}`;
370
    dataDecoded: DataDecoded;
371
  }): Promise<NativeStakingWithdrawConfirmationView> {
372
    const dataDecoded =
373
      args.dataDecoded.method !== ''
8✔
374
        ? args.dataDecoded
375
        : this.kilnDecoder.decodeBatchWithdrawCLFee(args.data);
376
    if (!dataDecoded) {
8!
UNCOV
377
      throw new Error('Transaction data could not be decoded');
×
378
    }
379
    const withdrawInfo = await this.nativeStakingMapper.mapWithdrawInfo({
8✔
380
      chainId: args.chainId,
381
      to: args.to,
382
      transaction: null,
383
      dataDecoded,
384
    });
385
    return new NativeStakingWithdrawConfirmationView({
4✔
386
      method: dataDecoded.method,
387
      parameters: dataDecoded.parameters,
388
      ...withdrawInfo,
389
    });
390
  }
391
}
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