diff --git a/src/app.module.ts b/src/app.module.ts index 3e0f648565..89a7fb3825 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,7 +46,6 @@ import { RelayControllerModule } from '@/routes/relay/relay.controller.module'; import { ZodErrorFilter } from '@/routes/common/filters/zod-error.filter'; import { CacheControlInterceptor } from '@/routes/common/interceptors/cache-control.interceptor'; import { AuthModule } from '@/routes/auth/auth.module'; -import { TransactionsViewControllerModule } from '@/routes/transactions/transactions-view.controller'; import { DelegatesV2Module } from '@/routes/delegates/v2/delegates.v2.module'; import { AccountsModule } from '@/routes/accounts/accounts.module'; import { NotificationsModuleV2 } from '@/routes/notifications/v2/notifications.module'; @@ -111,7 +110,6 @@ export class AppModule implements NestModule { SafesModule, TargetedMessagingModule, TransactionsModule, - TransactionsViewControllerModule, ...(isUsersFeatureEnabled ? [UsersModule] : []), // common CacheModule, diff --git a/src/domain/staking/contracts/decoders/__tests__/kiln-decoder.helper.spec.ts b/src/domain/staking/contracts/decoders/__tests__/kiln-decoder.helper.spec.ts index 50c6ae9fda..9b33043683 100644 --- a/src/domain/staking/contracts/decoders/__tests__/kiln-decoder.helper.spec.ts +++ b/src/domain/staking/contracts/decoders/__tests__/kiln-decoder.helper.spec.ts @@ -23,42 +23,12 @@ describe('KilnDecoder', () => { kilnDecoder = new KilnDecoder(mockLoggingService); }); - describe('decodeDeposit', () => { - it('decodes a deposit function call correctly', () => { - const data = depositEncoder().encode(); - expect(kilnDecoder.decodeDeposit(data)).toEqual({ - method: 'deposit', - parameters: [], - }); - }); - - it('returns null if the data is not a deposit function call', () => { - const data = faker.string.hexadecimal({ length: 1 }) as `0x${string}`; - expect(kilnDecoder.decodeDeposit(data)).toBeNull(); - }); - - it('returns null if the data is another Kiln function call', () => { - const data = requestValidatorsExitEncoder().encode(); - expect(kilnDecoder.decodeDeposit(data)).toBeNull(); - }); - }); - describe('decodeValidatorsExit', () => { it('decodes a requestValidatorsExit function call correctly', () => { const requestValidatorsExist = requestValidatorsExitEncoder(); const { _publicKeys } = requestValidatorsExist.build(); const data = requestValidatorsExist.encode(); - expect(kilnDecoder.decodeValidatorsExit(data)).toEqual({ - method: 'requestValidatorsExit', - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: _publicKeys, - valueDecoded: null, - }, - ], - }); + expect(kilnDecoder.decodeValidatorsExit(data)).toEqual(_publicKeys); }); it('returns null if the data is not a requestValidatorsExit function call', () => { @@ -77,17 +47,7 @@ describe('KilnDecoder', () => { const decodeBatchWithdrawCLFee = batchWithdrawCLFeeEncoder(); const { _publicKeys } = decodeBatchWithdrawCLFee.build(); const data = decodeBatchWithdrawCLFee.encode(); - expect(kilnDecoder.decodeBatchWithdrawCLFee(data)).toEqual({ - method: 'batchWithdrawCLFee', - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: _publicKeys, - valueDecoded: null, - }, - ], - }); + expect(kilnDecoder.decodeBatchWithdrawCLFee(data)).toEqual(_publicKeys); }); it('returns null if the data is not a batchWithdrawCLFee function call', () => { diff --git a/src/domain/staking/contracts/decoders/kiln-decoder.helper.ts b/src/domain/staking/contracts/decoders/kiln-decoder.helper.ts index 415677c229..ea2e913f52 100644 --- a/src/domain/staking/contracts/decoders/kiln-decoder.helper.ts +++ b/src/domain/staking/contracts/decoders/kiln-decoder.helper.ts @@ -29,35 +29,7 @@ export class KilnDecoder extends AbiDecoder { super(KilnAbi); } - // TODO: When confirmation view endpoint is removed, remove this - // and use this.helpers.isDeposit instead - decodeDeposit( - data: `0x${string}`, - ): { method: string; parameters: [] } | null { - if (!this.helpers.isDeposit(data)) { - return null; - } - try { - const decoded = this.decodeFunctionData({ data }); - if (decoded.functionName !== 'deposit') { - throw new Error('Data is not of deposit type'); - } - return { - method: decoded.functionName, - parameters: [], - }; - } catch (e) { - this.loggingService.debug(e); - return null; - } - } - - // TODO: When confirmation view endpoint is removed, return only - // publicKeys and don't format it like DataDecoded - decodeValidatorsExit(data: `0x${string}`): { - method: string; - parameters: Array; - } | null { + decodeValidatorsExit(data: `0x${string}`): `0x${string}` | null { if (!this.helpers.isRequestValidatorsExit(data)) { return null; } @@ -66,29 +38,14 @@ export class KilnDecoder extends AbiDecoder { if (decoded.functionName !== 'requestValidatorsExit') { throw new Error('Data is not of requestValidatorsExit type'); } - return { - method: decoded.functionName, - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: decoded.args[0], - valueDecoded: null, - }, - ], - }; + return decoded.args[0]; } catch (e) { this.loggingService.debug(e); return null; } } - // TODO: When confirmation view endpoint is removed, return only - // publicKeys and don't format it like DataDecoded - decodeBatchWithdrawCLFee(data: `0x${string}`): { - method: string; - parameters: Array; - } | null { + decodeBatchWithdrawCLFee(data: `0x${string}`): `0x${string}` | null { if (!this.helpers.isBatchWithdrawCLFee(data)) { return null; } @@ -97,17 +54,7 @@ export class KilnDecoder extends AbiDecoder { if (decoded.functionName !== 'batchWithdrawCLFee') { throw new Error('Data is not of batchWithdrawCLFee type'); } - return { - method: decoded.functionName, - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: decoded.args[0], - valueDecoded: null, - }, - ], - }; + return decoded.args[0]; } catch (e) { this.loggingService.debug(e); return null; diff --git a/src/routes/transactions/entities/confirmation-view/confirmation-view.entity.ts b/src/routes/transactions/entities/confirmation-view/confirmation-view.entity.ts deleted file mode 100644 index 862c0cfd9f..0000000000 --- a/src/routes/transactions/entities/confirmation-view/confirmation-view.entity.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { CowSwapConfirmationView } from '@/routes/transactions/entities/swaps/swap-confirmation-view.entity'; -import { CowSwapTwapConfirmationView } from '@/routes/transactions/entities/swaps/twap-confirmation-view.entity'; -import { NativeStakingDepositConfirmationView } from '@/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity'; -import { NativeStakingValidatorsExitConfirmationView } from '@/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity'; -import { NativeStakingWithdrawConfirmationView } from '@/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity'; - -export interface Baseline { - method: string; - parameters: Array | null; -} - -export enum DecodedType { - Generic = 'GENERIC', - CowSwapOrder = 'COW_SWAP_ORDER', - CowSwapTwapOrder = 'COW_SWAP_TWAP_ORDER', - KilnNativeStakingDeposit = 'KILN_NATIVE_STAKING_DEPOSIT', - KilnNativeStakingValidatorsExit = 'KILN_NATIVE_STAKING_VALIDATORS_EXIT', - KilnNativeStakingWithdraw = 'KILN_NATIVE_STAKING_WITHDRAW', -} - -export type ConfirmationView = - | BaselineConfirmationView - | CowSwapConfirmationView - | CowSwapTwapConfirmationView - | NativeStakingDepositConfirmationView - | NativeStakingValidatorsExitConfirmationView - | NativeStakingWithdrawConfirmationView; - -export class BaselineConfirmationView implements Baseline { - @ApiProperty({ enum: [DecodedType.Generic] }) - type = DecodedType.Generic; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - constructor(args: { - method: string; - parameters: Array | null; - }) { - this.method = args.method; - this.parameters = args.parameters; - } -} diff --git a/src/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity.ts b/src/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity.ts deleted file mode 100644 index 8d80774df4..0000000000 --- a/src/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { - Baseline, - DecodedType, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; -import { NativeStakingDepositInfo } from '@/routes/transactions/entities/staking/native-staking-deposit-info.entity'; -import { StakingStatus } from '@/routes/transactions/entities/staking/staking.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class NativeStakingDepositConfirmationView - implements Baseline, NativeStakingDepositInfo -{ - @ApiProperty({ - enum: [DecodedType.KilnNativeStakingDeposit], - }) - type = DecodedType.KilnNativeStakingDeposit; - - @ApiProperty({ - enum: StakingStatus, - }) - status: StakingStatus; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - @ApiProperty() - estimatedEntryTime: number; - - @ApiProperty() - estimatedExitTime: number; - - @ApiProperty() - estimatedWithdrawalTime: number; - - @ApiProperty() - fee: number; - - @ApiProperty() - monthlyNrr: number; - - @ApiProperty() - annualNrr: number; - - @ApiProperty() - value: string; - - @ApiProperty() - numValidators: number; - - @ApiProperty() - expectedAnnualReward: string; - - @ApiProperty() - expectedMonthlyReward: string; - - @ApiProperty() - expectedFiatAnnualReward: number; - - @ApiProperty() - expectedFiatMonthlyReward: number; - - @ApiProperty() - tokenInfo: TokenInfo; - - constructor(args: { - method: string; - parameters: Array | null; - status: StakingStatus; - estimatedEntryTime: number; - estimatedExitTime: number; - estimatedWithdrawalTime: number; - fee: number; - monthlyNrr: number; - annualNrr: number; - value: string; - numValidators: number; - expectedAnnualReward: string; - expectedMonthlyReward: string; - expectedFiatAnnualReward: number; - expectedFiatMonthlyReward: number; - tokenInfo: TokenInfo; - }) { - this.method = args.method; - this.parameters = args.parameters; - this.status = args.status; - this.estimatedEntryTime = args.estimatedEntryTime; - this.estimatedExitTime = args.estimatedExitTime; - this.estimatedWithdrawalTime = args.estimatedWithdrawalTime; - this.fee = args.fee; - this.monthlyNrr = args.monthlyNrr; - this.annualNrr = args.annualNrr; - this.value = args.value; - this.numValidators = args.numValidators; - this.expectedAnnualReward = args.expectedAnnualReward; - this.expectedMonthlyReward = args.expectedMonthlyReward; - this.expectedFiatAnnualReward = args.expectedFiatAnnualReward; - this.expectedFiatMonthlyReward = args.expectedFiatMonthlyReward; - this.tokenInfo = args.tokenInfo; - } -} diff --git a/src/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity.ts b/src/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity.ts deleted file mode 100644 index 01989e4fff..0000000000 --- a/src/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { - Baseline, - DecodedType, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; -import { StakingStatus } from '@/routes/transactions/entities/staking/staking.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class NativeStakingValidatorsExitConfirmationView implements Baseline { - @ApiProperty({ - enum: [DecodedType.KilnNativeStakingValidatorsExit], - }) - type = DecodedType.KilnNativeStakingValidatorsExit; - - @ApiProperty({ - enum: StakingStatus, - }) - status: StakingStatus; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - @ApiProperty() - estimatedExitTime: number; - - @ApiProperty() - estimatedWithdrawalTime: number; - - @ApiProperty() - value: string; - - @ApiProperty() - numValidators: number; - - @ApiProperty() - tokenInfo: TokenInfo; - - @ApiProperty() - validators: Array<`0x${string}`>; - - constructor(args: { - method: string; - parameters: Array | null; - status: StakingStatus; - estimatedExitTime: number; - estimatedWithdrawalTime: number; - value: string; - numValidators: number; - tokenInfo: TokenInfo; - validators: Array<`0x${string}`>; - }) { - this.method = args.method; - this.parameters = args.parameters; - this.status = args.status; - this.estimatedExitTime = args.estimatedExitTime; - this.estimatedWithdrawalTime = args.estimatedWithdrawalTime; - this.value = args.value; - this.numValidators = args.numValidators; - this.tokenInfo = args.tokenInfo; - this.validators = args.validators; - } -} diff --git a/src/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity.ts b/src/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity.ts deleted file mode 100644 index 004856aa5c..0000000000 --- a/src/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { - Baseline, - DecodedType, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; - -export class NativeStakingWithdrawConfirmationView implements Baseline { - @ApiProperty({ - enum: [DecodedType.KilnNativeStakingWithdraw], - }) - type = DecodedType.KilnNativeStakingWithdraw; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - @ApiProperty() - value: string; - - @ApiProperty() - tokenInfo: TokenInfo; - - @ApiProperty() - validators: Array<`0x${string}`>; - - constructor(args: { - method: string; - parameters: Array | null; - value: string; - tokenInfo: TokenInfo; - validators: Array<`0x${string}`>; - }) { - this.method = args.method; - this.parameters = args.parameters; - this.value = args.value; - this.tokenInfo = args.tokenInfo; - this.validators = args.validators; - } -} diff --git a/src/routes/transactions/entities/swaps/swap-confirmation-view.entity.ts b/src/routes/transactions/entities/swaps/swap-confirmation-view.entity.ts deleted file mode 100644 index ebba85c2e2..0000000000 --- a/src/routes/transactions/entities/swaps/swap-confirmation-view.entity.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { OrderInfo } from '@/routes/transactions/entities/swaps/swap-order-info.entity'; -import { - OrderClass, - OrderKind, - OrderStatus, -} from '@/domain/swaps/entities/order.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { - Baseline, - DecodedType, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; - -export class CowSwapConfirmationView implements Baseline, OrderInfo { - // Baseline implementation - @ApiProperty({ enum: [DecodedType.CowSwapOrder] }) - type = DecodedType.CowSwapOrder; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - // OrderInfo implementation - @ApiProperty({ description: 'The order UID' }) - uid: string; - - @ApiProperty({ - enum: OrderStatus, - }) - status: OrderStatus; - - @ApiProperty({ enum: Object.values(OrderKind) }) - kind: OrderKind; - - @ApiProperty({ - enum: OrderClass, - }) - orderClass: OrderClass; - - @ApiProperty({ description: 'The timestamp when the order expires' }) - validUntil: number; - - @ApiProperty({ - description: 'The sell token raw amount (no decimals)', - }) - sellAmount: string; - - @ApiProperty({ - description: 'The buy token raw amount (no decimals)', - }) - buyAmount: string; - - @ApiProperty({ - description: 'The executed sell token raw amount (no decimals)', - }) - executedSellAmount: string; - - @ApiProperty({ - description: 'The executed buy token raw amount (no decimals)', - }) - executedBuyAmount: string; - - @ApiProperty({ - type: String, - description: 'The URL to the explorer page of the order', - }) - explorerUrl: string; - - @ApiProperty({ - type: String, - description: 'The amount of fees paid for this order.', - deprecated: true, - }) - executedSurplusFee: string; - - @ApiProperty({ - type: String, - description: 'The amount of fees paid for this order.', - }) - executedFee: string; - - @ApiProperty({ - description: - 'The token in which the fee was paid, expressed by SURPLUS tokens (BUY tokens for SELL orders and SELL tokens for BUY orders).', - }) - executedFeeToken: TokenInfo; - - @ApiPropertyOptional({ - type: String, - nullable: true, - description: 'The (optional) address to receive the proceeds of the trade', - }) - receiver: string | null; - - @ApiProperty({ - type: String, - }) - owner: `0x${string}`; - - @ApiPropertyOptional({ - type: Object, - nullable: true, - description: 'The App Data for this order', - }) - fullAppData: Record | null; - - @ApiProperty({ description: 'The sell token of the order' }) - sellToken: TokenInfo; - - @ApiProperty({ description: 'The buy token of the order' }) - buyToken: TokenInfo; - - constructor( - args: Baseline & OrderInfo & { sellToken: TokenInfo; buyToken: TokenInfo }, - ) { - this.method = args.method; - this.parameters = args.parameters; - this.uid = args.uid; - this.status = args.status; - this.kind = args.kind; - this.orderClass = args.orderClass; - this.validUntil = args.validUntil; - this.sellAmount = args.sellAmount; - this.buyAmount = args.buyAmount; - this.executedSellAmount = args.executedSellAmount; - this.executedBuyAmount = args.executedBuyAmount; - this.explorerUrl = args.explorerUrl; - this.executedSurplusFee = args.executedSurplusFee; - this.executedFee = args.executedFee; - this.executedFeeToken = args.executedFeeToken; - this.sellToken = args.sellToken; - this.buyToken = args.buyToken; - this.receiver = args.receiver; - this.owner = args.owner; - this.fullAppData = args.fullAppData; - } -} diff --git a/src/routes/transactions/entities/swaps/twap-confirmation-view.entity.ts b/src/routes/transactions/entities/swaps/twap-confirmation-view.entity.ts deleted file mode 100644 index 5d5497793b..0000000000 --- a/src/routes/transactions/entities/swaps/twap-confirmation-view.entity.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; -import { - OrderClass, - OrderKind, - OrderStatus, -} from '@/domain/swaps/entities/order.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { - DurationOfPart, - StartTime, - TwapOrderInfo, -} from '@/routes/transactions/entities/swaps/twap-order-info.entity'; -import { - Baseline, - DecodedType, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; - -export class CowSwapTwapConfirmationView implements Baseline, TwapOrderInfo { - // Baseline implementation - @ApiProperty({ enum: [DecodedType.CowSwapTwapOrder] }) - type = DecodedType.CowSwapTwapOrder; - - @ApiProperty() - method: string; - - @ApiPropertyOptional({ - type: DataDecodedParameter, - isArray: true, - nullable: true, - }) - parameters: Array | null; - - // TwapOrderInfo implementation - @ApiProperty({ - enum: OrderStatus, - description: 'The TWAP status', - }) - status: OrderStatus; - - @ApiProperty({ enum: OrderKind }) - kind: OrderKind.Sell; - - @ApiProperty({ enum: OrderClass }) - class: OrderClass.Limit; - - @ApiProperty({ - description: - 'The order UID of the active order, null as it is not an active order', - // Prevent bidirectional dependency - type: 'null', - }) - activeOrderUid: null; - - @ApiProperty({ description: 'The timestamp when the TWAP expires' }) - validUntil: number; - - @ApiProperty({ - description: 'The sell token raw amount (no decimals)', - }) - sellAmount: string; - - @ApiProperty({ - description: 'The buy token raw amount (no decimals)', - }) - buyAmount: string; - - @ApiPropertyOptional({ - type: String, - nullable: true, - description: - 'The executed sell token raw amount (no decimals), or null if there are too many parts', - }) - executedSellAmount: string | null; - - @ApiProperty({ - type: String, - description: - 'The executed surplus fee raw amount (no decimals), or null if there are too many parts', - deprecated: true, - }) - executedSurplusFee: string; - - @ApiProperty({ - type: String, - description: - 'The executed surplus fee raw amount (no decimals), or null if there are too many parts', - }) - executedFee: string; - - @ApiProperty({ - type: String, - description: - 'The token in which the fee was paid, expressed by SURPLUS tokens (BUY tokens for SELL orders and SELL tokens for BUY orders).', - }) - executedFeeToken: TokenInfo; - - @ApiPropertyOptional({ - type: String, - nullable: true, - description: - 'The executed buy token raw amount (no decimals), or null if there are too many parts', - }) - executedBuyAmount: string | null; - - @ApiProperty({ description: 'The sell token of the TWAP' }) - sellToken: TokenInfo; - - @ApiProperty({ description: 'The buy token of the TWAP' }) - buyToken: TokenInfo; - - @ApiProperty({ - description: 'The address to receive the proceeds of the trade', - }) - receiver: `0x${string}`; - - @ApiProperty({ - type: String, - }) - owner: `0x${string}`; - - @ApiPropertyOptional({ - type: Object, - nullable: true, - description: 'The App Data for this TWAP', - }) - fullAppData: Record | null; - - @ApiProperty({ - description: 'The number of parts in the TWAP', - }) - numberOfParts: string; - - @ApiProperty({ - description: 'The amount of sellToken to sell in each part', - }) - partSellAmount: string; - - @ApiProperty({ - description: 'The amount of buyToken that must be bought in each part', - }) - minPartLimit: string; - - @ApiProperty({ - description: 'The duration of the TWAP interval', - }) - timeBetweenParts: number; - - @ApiProperty({ - description: 'Whether the TWAP is valid for the entire interval or not', - }) - durationOfPart: DurationOfPart; - - @ApiProperty({ - description: 'The start time of the TWAP', - }) - startTime: StartTime; - - constructor(args: { - method: string; - parameters: Array | null; - status: OrderStatus; - kind: OrderKind.Sell; - class: OrderClass.Limit; - activeOrderUid: null; - validUntil: number; - sellAmount: string; - buyAmount: string; - executedSellAmount: string | null; - executedBuyAmount: string | null; - executedSurplusFee: string; - executedFee: string; - executedFeeToken: TokenInfo; - sellToken: TokenInfo; - buyToken: TokenInfo; - receiver: `0x${string}`; - owner: `0x${string}`; - fullAppData: Record | null; - numberOfParts: string; - partSellAmount: string; - minPartLimit: string; - timeBetweenParts: number; - durationOfPart: DurationOfPart; - startTime: StartTime; - }) { - this.method = args.method; - this.parameters = args.parameters; - this.status = args.status; - this.kind = args.kind; - this.class = args.class; - this.activeOrderUid = args.activeOrderUid; - this.validUntil = args.validUntil; - this.sellAmount = args.sellAmount; - this.buyAmount = args.buyAmount; - this.executedSellAmount = args.executedSellAmount; - this.executedBuyAmount = args.executedBuyAmount; - this.executedSurplusFee = args.executedSurplusFee; - this.executedFee = args.executedFee; - this.executedFeeToken = args.executedFeeToken; - this.sellToken = args.sellToken; - this.buyToken = args.buyToken; - this.receiver = args.receiver; - this.owner = args.owner; - this.fullAppData = args.fullAppData; - this.numberOfParts = args.numberOfParts; - this.partSellAmount = args.partSellAmount; - this.minPartLimit = args.minPartLimit; - this.timeBetweenParts = args.timeBetweenParts; - this.durationOfPart = args.durationOfPart; - this.startTime = args.startTime; - } -} diff --git a/src/routes/transactions/mappers/common/native-staking.mapper.ts b/src/routes/transactions/mappers/common/native-staking.mapper.ts index a51f1eba51..b97178c513 100644 --- a/src/routes/transactions/mappers/common/native-staking.mapper.ts +++ b/src/routes/transactions/mappers/common/native-staking.mapper.ts @@ -181,7 +181,7 @@ export class NativeStakingMapper { ]); this.validateDeployment(deployment); const publicKeys = this.kilnNativeStakingHelper.splitPublicKeys( - this.kilnDecoder.decodeValidatorsExit(args.data)!.parameters[0].value, + this.kilnDecoder.decodeValidatorsExit(args.data)!, ); const value = @@ -245,7 +245,7 @@ export class NativeStakingMapper { this.validateDeployment(deployment); const publicKeys = this.kilnNativeStakingHelper.splitPublicKeys( - this.kilnDecoder.decodeBatchWithdrawCLFee(args.data)!.parameters[0].value, + this.kilnDecoder.decodeBatchWithdrawCLFee(args.data)!, ); const value = await this.getWithdrawValue({ txHash: args.txHash, diff --git a/src/routes/transactions/transactions-view.controller.spec.ts b/src/routes/transactions/transactions-view.controller.spec.ts deleted file mode 100644 index f30b1b0116..0000000000 --- a/src/routes/transactions/transactions-view.controller.spec.ts +++ /dev/null @@ -1,2439 +0,0 @@ -import { TestAppProvider } from '@/__tests__/test-app.provider'; -import { AppModule } from '@/app.module'; -import { IConfigurationService } from '@/config/configuration.service.interface'; -import configuration from '@/config/entities/__tests__/configuration'; -import { TestCacheModule } from '@/datasources/cache/__tests__/test.cache.module'; -import { CacheModule } from '@/datasources/cache/cache.module'; -import { TestPostgresDatabaseModule } from '@/datasources/db/__tests__/test.postgres-database.module'; -import { PostgresDatabaseModule } from '@/datasources/db/v1/postgres-database.module'; -import { PostgresDatabaseModuleV2 } from '@/datasources/db/v2/postgres-database.module'; -import { TestPostgresDatabaseModuleV2 } from '@/datasources/db/v2/test.postgres-database.module'; -import { TestNetworkModule } from '@/datasources/network/__tests__/test.network.module'; -import { NetworkModule } from '@/datasources/network/network.module'; -import type { INetworkService } from '@/datasources/network/network.service.interface'; -import { NetworkService } from '@/datasources/network/network.service.interface'; -import { TestQueuesApiModule } from '@/datasources/queues/__tests__/test.queues-api.module'; -import { QueuesApiModule } from '@/datasources/queues/queues-api.module'; -import { dedicatedStakingStatsBuilder } from '@/datasources/staking-api/entities/__tests__/dedicated-staking-stats.entity.builder'; -import { deploymentBuilder } from '@/datasources/staking-api/entities/__tests__/deployment.entity.builder'; -import { networkStatsBuilder } from '@/datasources/staking-api/entities/__tests__/network-stats.entity.builder'; -import { stakeBuilder } from '@/datasources/staking-api/entities/__tests__/stake.entity.builder'; -import type { Stake } from '@/datasources/staking-api/entities/stake.entity'; -import { StakeState } from '@/datasources/staking-api/entities/stake.entity'; -import { TestTargetedMessagingDatasourceModule } from '@/datasources/targeted-messaging/__tests__/test.targeted-messaging.datasource.module'; -import { TargetedMessagingDatasourceModule } from '@/datasources/targeted-messaging/targeted-messaging.datasource.module'; -import { chainBuilder } from '@/domain/chains/entities/__tests__/chain.builder'; -import { getNumberString } from '@/domain/common/utils/utils'; -import { - multiSendEncoder, - multiSendTransactionsEncoder, -} from '@/domain/contracts/__tests__/encoders/multi-send-encoder.builder'; -import { dataDecodedBuilder } from '@/domain/data-decoder/entities/__tests__/data-decoded.builder'; -import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { KilnDecoder } from '@/domain/staking/contracts/decoders/kiln-decoder.helper'; -import { setPreSignatureEncoder } from '@/domain/swaps/contracts/__tests__/encoders/gp-v2-encoder.builder'; -import { orderBuilder } from '@/domain/swaps/entities/__tests__/order.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; -import { RequestScopedLoggingModule } from '@/logging/logging.module'; -import { NULL_ADDRESS } from '@/routes/common/constants'; -import { rawify } from '@/validation/entities/raw.entity'; -import { faker } from '@faker-js/faker'; -import type { INestApplication } from '@nestjs/common'; -import { NotFoundException, ServiceUnavailableException } from '@nestjs/common'; -import type { TestingModule } from '@nestjs/testing'; -import { Test } from '@nestjs/testing'; -import type { Server } from 'net'; -import request from 'supertest'; -import { concat, encodeFunctionData, getAddress, parseAbi } from 'viem'; - -describe('TransactionsViewController tests', () => { - let app: INestApplication; - let safeConfigUrl: string; - let swapsApiUrl: string; - let stakingApiUrl: string; - let networkService: jest.MockedObjectDeep; - - const swapsVerifiedApp = faker.company.buzzNoun(); - const swapsChainId = '1'; - - beforeEach(async () => { - jest.resetAllMocks(); - - const baseConfig = configuration(); - const testConfiguration: typeof configuration = () => ({ - ...baseConfig, - swaps: { - ...baseConfig.swaps, - restrictApps: true, - allowedApps: [swapsVerifiedApp], - }, - }); - - const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [AppModule.register(testConfiguration)], - }) - .overrideModule(PostgresDatabaseModule) - .useModule(TestPostgresDatabaseModule) - .overrideModule(TargetedMessagingDatasourceModule) - .useModule(TestTargetedMessagingDatasourceModule) - .overrideModule(CacheModule) - .useModule(TestCacheModule) - .overrideModule(RequestScopedLoggingModule) - .useModule(TestLoggingModule) - .overrideModule(NetworkModule) - .useModule(TestNetworkModule) - .overrideModule(QueuesApiModule) - .useModule(TestQueuesApiModule) - .overrideModule(PostgresDatabaseModuleV2) - .useModule(TestPostgresDatabaseModuleV2) - .compile(); - - const configurationService = moduleFixture.get( - IConfigurationService, - ); - safeConfigUrl = configurationService.getOrThrow('safeConfig.baseUri'); - swapsApiUrl = configurationService.getOrThrow(`swaps.api.${swapsChainId}`); - stakingApiUrl = configurationService.getOrThrow('staking.mainnet.baseUri'); - networkService = moduleFixture.get(NetworkService); - app = await new TestAppProvider().provide(moduleFixture); - await app.init(); - }); - - afterAll(async () => { - await app.close(); - }); - - it('Gets Generic confirmation view', async () => { - const chain = chainBuilder().build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ data: rawify(dataDecoded), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: '0x', - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - describe('Swaps', () => { - it('Gets swap confirmation view with swap data', async () => { - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - const preSignatureEncoder = setPreSignatureEncoder(); - const preSignature = preSignatureEncoder.build(); - const order = orderBuilder() - .with('uid', preSignature.orderUid) - .with('fullAppData', `{ "appCode": "${swapsVerifiedApp}" }`) - .build(); - const buyToken = tokenBuilder().with('address', order.buyToken).build(); - const sellToken = tokenBuilder().with('address', order.sellToken).build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/orders/${order.uid}`) { - return Promise.resolve({ data: rawify(order), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.buyToken}` - ) { - return Promise.resolve({ data: rawify(buyToken), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.sellToken}` - ) { - return Promise.resolve({ data: rawify(sellToken), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: preSignatureEncoder.encode(), - }) - .expect(200) - .expect(({ body }) => - expect(body).toMatchObject({ - type: 'COW_SWAP_ORDER', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - uid: order.uid, - status: order.status, - kind: order.kind, - orderClass: order.class, - validUntil: order.validTo, - sellAmount: order.sellAmount.toString(), - buyAmount: order.buyAmount.toString(), - executedSellAmount: order.executedSellAmount.toString(), - executedBuyAmount: order.executedBuyAmount.toString(), - explorerUrl: expect.any(String), - executedSurplusFee: order.executedSurplusFee.toString(), - executedFee: order.executedFee.toString(), - executedFeeToken: { - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }, - sellToken: { - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }, - buyToken: { - address: buyToken.address, - decimals: buyToken.decimals, - logoUri: buyToken.logoUri, - name: buyToken.name, - symbol: buyToken.symbol, - trusted: buyToken.trusted, - }, - receiver: order.receiver, - owner: order.owner, - fullAppData: JSON.parse(order.fullAppData as string), - }), - ); - }); - - it('gets TWAP confirmation view with TWAP data', async () => { - const ComposableCowAddress = '0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74'; - /** - * @see https://sepolia.etherscan.io/address/0xfdaFc9d1902f4e0b84f65F49f244b32b31013b74 - */ - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder() - .with('address', '0x31eaC7F0141837B266De30f4dc9aF15629Bd5381') - .build(); - const data = - '0x0d0d9800000000000000000000000000000000000000000000000000000000000000008000000000000000000000000052ed56da04309aca4c3fecc595298d80c2f16bac000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006cf1e9ca41f7611def408122793c358a3d11e5a500000000000000000000000000000000000000000000000000000019011f294a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000140000000000000000000000000be72e441bf55620febc26715db68d3494213d8cb000000000000000000000000fff9976782d46cc05630d1f6ebab18b2324d6b1400000000000000000000000031eac7f0141837b266de30f4dc9af15629bd538100000000000000000000000000000000000000000000000b941d039eed310b36000000000000000000000000000000000000000000000000087bbc924df9167e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000007080000000000000000000000000000000000000000000000000000000000000000f7be7261f56698c258bf75f888d68a00c85b22fb21958b9009c719eb88aebda00000000000000000000000000000000000000000000000000000000000000000'; - const appDataHash = - '0xf7be7261f56698c258bf75f888d68a00c85b22fb21958b9009c719eb88aebda0'; - const fullAppData = { - fullAppData: JSON.stringify({ appCode: swapsVerifiedApp }), - }; - const dataDecoded = dataDecodedBuilder().build(); - const buyToken = tokenBuilder() - .with( - 'address', - getAddress('0xfff9976782d46cc05630d1f6ebab18b2324d6b14'), - ) - .build(); - const sellToken = tokenBuilder() - .with( - 'address', - getAddress('0xbe72e441bf55620febc26715db68d3494213d8cb'), - ) - .build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if ( - url === - `${chain.transactionService}/api/v1/tokens/${buyToken.address}` - ) { - return Promise.resolve({ data: rawify(buyToken), status: 200 }); - } - if ( - url === - `${chain.transactionService}/api/v1/tokens/${sellToken.address}` - ) { - return Promise.resolve({ data: rawify(sellToken), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/app_data/${appDataHash}`) { - return Promise.resolve({ data: rawify(fullAppData), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data, - to: ComposableCowAddress, - }) - .expect(200) - .expect(({ body }) => - expect(body).toMatchObject({ - type: 'COW_SWAP_TWAP_ORDER', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }), - ); - }); - - it('Gets Generic confirmation view if order data is not available', async () => { - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - const preSignatureEncoder = setPreSignatureEncoder(); - const preSignature = preSignatureEncoder.build(); - const order = orderBuilder() - .with('uid', preSignature.orderUid) - .with('fullAppData', `{ "appCode": "${swapsVerifiedApp}" }`) - .build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/orders/${order.uid}`) { - return Promise.reject({ status: 500 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: preSignatureEncoder.encode(), - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('Gets Generic confirmation view if buy token data is not available', async () => { - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - const preSignatureEncoder = setPreSignatureEncoder(); - const preSignature = preSignatureEncoder.build(); - const order = orderBuilder() - .with('uid', preSignature.orderUid) - .with('fullAppData', `{ "appCode": "${swapsVerifiedApp}" }`) - .build(); - const sellToken = tokenBuilder().with('address', order.sellToken).build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/orders/${order.uid}`) { - return Promise.resolve({ data: rawify(order), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.buyToken}` - ) { - return Promise.reject({ status: 500 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.sellToken}` - ) { - return Promise.resolve({ data: rawify(sellToken), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: preSignatureEncoder.encode(), - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('Gets Generic confirmation view if sell token data is not available', async () => { - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - const preSignatureEncoder = setPreSignatureEncoder(); - const preSignature = preSignatureEncoder.build(); - const order = orderBuilder() - .with('uid', preSignature.orderUid) - .with('fullAppData', `{ "appCode": "${swapsVerifiedApp}" }`) - .build(); - const buyToken = tokenBuilder().with('address', order.sellToken).build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/orders/${order.uid}`) { - return Promise.resolve({ data: rawify(order), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.buyToken}` - ) { - return Promise.resolve({ data: rawify(buyToken), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.sellToken}` - ) { - return Promise.reject({ status: 500 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: preSignatureEncoder.encode(), - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('Gets Generic confirmation view if swap app is restricted', async () => { - const chain = chainBuilder().with('chainId', swapsChainId).build(); - const safe = safeBuilder().build(); - const dataDecoded = dataDecodedBuilder().build(); - const preSignatureEncoder = setPreSignatureEncoder(); - const preSignature = preSignatureEncoder.build(); - const order = orderBuilder() - .with('uid', preSignature.orderUid) - // We don't use buzzNoun here as it can generate the same value as verifiedApp - .with('fullAppData', `{ "appCode": "restricted app code" }`) - .build(); - const buyToken = tokenBuilder().with('address', order.buyToken).build(); - const sellToken = tokenBuilder().with('address', order.sellToken).build(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${swapsApiUrl}/api/v1/orders/${order.uid}`) { - return Promise.resolve({ data: rawify(order), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.buyToken}` - ) { - return Promise.resolve({ data: rawify(buyToken), status: 200 }); - } - if ( - url === `${chain.transactionService}/api/v1/tokens/${order.sellToken}` - ) { - return Promise.resolve({ data: rawify(sellToken), status: 200 }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safe.address}/views/transaction-confirmation`, - ) - .send({ - data: preSignatureEncoder.encode(), - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - }); - - describe('Staking', () => { - describe('Native', () => { - describe('deposit', () => { - it('returns the native staking `deposit` confirmation view for proposal', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const networkStats = networkStatsBuilder().build(); - // Transaction being proposed (no stakes exists) - const stakes: Array = []; - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - const value = getNumberString(64 * 10 ** 18 + 1); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/kiln-stats`: - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - const annualNrr = - dedicatedStakingStats.gross_apy.last_30d * - (1 - Number(deployment.product_fee)); - const monthlyNrr = annualNrr / 12; - const expectedAnnualReward = (annualNrr / 100) * Number(value); - const expectedMonthlyReward = expectedAnnualReward / 12; - const expectedFiatAnnualReward = - (expectedAnnualReward * networkStats.eth_price_usd) / - Math.pow(10, chain.nativeCurrency.decimals); - const expectedFiatMonthlyReward = expectedFiatAnnualReward / 12; - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - value, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_DEPOSIT', - method: dataDecoded.method, - status: 'NOT_STAKED', - parameters: dataDecoded.parameters, - estimatedEntryTime: - networkStats.estimated_entry_time_seconds * 1_000, - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - fee: +deployment.product_fee!, - monthlyNrr, - annualNrr, - value, - numValidators: 2, - expectedAnnualReward: getNumberString(expectedAnnualReward), - expectedMonthlyReward: getNumberString(expectedMonthlyReward), - expectedFiatAnnualReward, - expectedFiatMonthlyReward, - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - }); - }); - it('returns the native staking `deposit` confirmation view', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const networkStats = networkStatsBuilder().build(); - const stakes = [ - stakeBuilder().with('state', StakeState.Unstaked).build(), - ]; - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - const value = getNumberString(64 * 10 ** 18 + 1); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/kiln-stats`: - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - const annualNrr = - dedicatedStakingStats.gross_apy.last_30d * - (1 - Number(deployment.product_fee)); - const monthlyNrr = annualNrr / 12; - const expectedAnnualReward = (annualNrr / 100) * Number(value); - const expectedMonthlyReward = expectedAnnualReward / 12; - const expectedFiatAnnualReward = - (expectedAnnualReward * networkStats.eth_price_usd) / - Math.pow(10, chain.nativeCurrency.decimals); - const expectedFiatMonthlyReward = expectedFiatAnnualReward / 12; - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - value, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_DEPOSIT', - method: dataDecoded.method, - status: 'NOT_STAKED', - parameters: dataDecoded.parameters, - estimatedEntryTime: - networkStats.estimated_entry_time_seconds * 1_000, - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - fee: +deployment.product_fee!, - monthlyNrr, - annualNrr, - value, - numValidators: 2, - expectedAnnualReward: getNumberString(expectedAnnualReward), - expectedMonthlyReward: getNumberString(expectedMonthlyReward), - expectedFiatAnnualReward, - expectedFiatMonthlyReward, - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - }); - }); - - it('returns the native staking `deposit` confirmation view using local decoding', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const networkStats = networkStatsBuilder().build(); - const safeAddress = faker.finance.ethereumAddress(); - // Transaction being proposed (no stakes exists) - const stakes: Array = []; - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - const value = getNumberString(64 * 10 ** 18 + 1); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/kiln-stats`: - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.reject(new ServiceUnavailableException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - const annualNrr = - dedicatedStakingStats.gross_apy.last_30d * - (1 - Number(deployment.product_fee)); - const monthlyNrr = annualNrr / 12; - const expectedAnnualReward = (annualNrr / 100) * Number(value); - const expectedMonthlyReward = expectedAnnualReward / 12; - const expectedFiatAnnualReward = - (expectedAnnualReward * networkStats.eth_price_usd) / - Math.pow(10, chain.nativeCurrency.decimals); - const expectedFiatMonthlyReward = expectedFiatAnnualReward / 12; - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - value, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_DEPOSIT', - method: 'deposit', - status: 'NOT_STAKED', - parameters: [], - estimatedEntryTime: - networkStats.estimated_entry_time_seconds * 1_000, - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - fee: +deployment.product_fee!, - monthlyNrr, - annualNrr, - value, - numValidators: 2, - expectedAnnualReward: getNumberString(expectedAnnualReward), - expectedMonthlyReward: getNumberString(expectedMonthlyReward), - expectedFiatAnnualReward, - expectedFiatMonthlyReward, - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - }); - }); - - it('returns the dedicated staking `deposit` confirmation view from batch', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const networkStats = networkStatsBuilder().build(); - const safeAddress = faker.finance.ethereumAddress(); - // Transaction being proposed (no stakes exists) - const stakes: Array = []; - const depositData = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - const multiSendAddress = getAddress(faker.finance.ethereumAddress()); - const multiSendData = multiSendEncoder() - .with( - 'transactions', - multiSendTransactionsEncoder([ - { - to: deployment.address, - data: depositData, - value: BigInt(0), - operation: 0, - }, - ]), - ) - .encode(); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/network-stats`) { - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/stakes`) { - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: multiSendAddress, - data: multiSendData, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_DEPOSIT', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - status: 'NOT_STAKED', - estimatedEntryTime: - networkStats.estimated_entry_time_seconds * 1_000, - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - fee: +deployment.product_fee!, - monthlyNrr: - (dedicatedStakingStats.gross_apy.last_30d * - (1 - +deployment.product_fee!)) / - 12, - annualNrr: - dedicatedStakingStats.gross_apy.last_30d * - (1 - +deployment.product_fee!), - value: '0', // defaults to 0 if not provided in the request - numValidators: 0, // 0 as value is 0 - expectedMonthlyReward: '0', // 0 as value is 0 - expectedAnnualReward: '0', // 0 as value is 0 - expectedFiatMonthlyReward: 0, // 0 as value is 0 - expectedFiatAnnualReward: 0, // 0 as value is 0 - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - }); - }); - - it('returns the generic confirmation view if the deployment is not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.reject(new NotFoundException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment is not dedicated-specific', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'pooling') // Pooling - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment chain is unknown', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('chain', 'unknown') // Unknown - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if not transacting with a deployment address', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const to = faker.finance.ethereumAddress(); // Not deployment.address, ergo "unknown" - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment has no product fee', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', null) // No product fee - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the dedicated staking stats are not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const networkStats = networkStatsBuilder().build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.reject(new NotFoundException()); - } - if (url === `${stakingApiUrl}/v1/eth/network-stats`) { - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the network stats are not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function deposit() external payable']), - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/network-stats`) { - return Promise.reject(new NotFoundException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - }); - - describe('validators exit', () => { - it('returns the native staking `validators exit` confirmation view', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const networkStats = networkStatsBuilder().build(); - const validators = [ - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - ] as Array<`0x${string}`>; - const validatorPublicKey = concat(validators); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [validatorPublicKey], - }); - const dataDecoded = dataDecodedBuilder() - .with('method', 'requestValidatorsExit') - .with('parameters', [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ]) - .build(); - const stakes = [ - stakeBuilder().with('state', StakeState.ActiveOngoing).build(), - stakeBuilder().with('state', StakeState.ActiveOngoing).build(), - ]; - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_VALIDATORS_EXIT', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - status: 'ACTIVE', - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - value: '64000000000000000000', // 2 x 32 ETH, - numValidators: 2, - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - validators, - }); - - // check the public keys are passed to the staking service in the expected format - expect(networkService.get).toHaveBeenNthCalledWith(3, { - url: `${stakingApiUrl}/v1/eth/stakes`, - networkRequest: expect.objectContaining({ - params: { - onchain_v1_include_net_rewards: true, - validators: validators.join(','), - }, - }), - }); - }); - - it('returns the native staking `validators exit` confirmation view using local decoding', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const networkStats = networkStatsBuilder().build(); - const validatorPublicKey = faker.string - .hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }) - .toLowerCase(); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [validatorPublicKey as `0x${string}`], - }); - const stakes = [ - stakeBuilder().with('state', StakeState.ActiveOngoing).build(), - stakeBuilder().with('state', StakeState.ActiveOngoing).build(), - ]; - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.reject(new ServiceUnavailableException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_VALIDATORS_EXIT', - method: 'requestValidatorsExit', - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ], - status: 'ACTIVE', - estimatedExitTime: - networkStats.estimated_exit_time_seconds * 1_000, - estimatedWithdrawalTime: - networkStats.estimated_withdrawal_time_seconds * 1_000, - value: '32000000000000000000', // 32 ETH, - numValidators: 1, - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - validators: [validatorPublicKey], - }); - - // check the public keys are passed to the staking service in the expected format - expect(networkService.get).toHaveBeenNthCalledWith(3, { - url: `${stakingApiUrl}/v1/eth/stakes`, - networkRequest: expect.objectContaining({ - params: { - onchain_v1_include_net_rewards: true, - validators: validatorPublicKey, - }, - }), - }); - }); - - it('returns the generic confirmation view if the deployment is not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const safeAddress = faker.finance.ethereumAddress(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [faker.string.hexadecimal() as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.reject(new NotFoundException()); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: faker.finance.ethereumAddress(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment is not dedicated-specific', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'defi') // Not dedicated - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [faker.string.hexadecimal() as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment chain is unknown', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('chain', 'unknown') // Unknown - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [faker.string.hexadecimal() as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if not transacting with a deployment address', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const to = faker.finance.ethereumAddress(); // Not deployment.address, ergo "unknown" - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [faker.string.hexadecimal() as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the network stats are not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const dataDecoded = dataDecodedBuilder().build(); - const dedicatedStakingStats = dedicatedStakingStatsBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const to = faker.finance.ethereumAddress(); // Not deployment.address, ergo "unknown" - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [faker.string.hexadecimal() as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - if (url === `${safeConfigUrl}/api/v1/chains/${chain.chainId}`) { - return Promise.resolve({ data: rawify(chain), status: 200 }); - } - if (url === `${stakingApiUrl}/v1/deployments`) { - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/kiln-stats`) { - return Promise.resolve({ - data: rawify({ data: dedicatedStakingStats }), - status: 200, - }); - } - if (url === `${stakingApiUrl}/v1/eth/network-stats`) { - return Promise.reject(new NotFoundException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the stakes are not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const networkStats = networkStatsBuilder().build(); - const validators = [ - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - ] as Array<`0x${string}`>; - const validatorPublicKey = concat(validators); - const data = encodeFunctionData({ - abi: parseAbi(['function requestValidatorsExit(bytes)']), - functionName: 'requestValidatorsExit', - args: [validatorPublicKey], - }); - const dataDecoded = dataDecodedBuilder() - .with('method', 'requestValidatorsExit') - .with('parameters', [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ]) - .build(); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/network-stats`: - return Promise.resolve({ - data: rawify({ data: networkStats }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.reject(new ServiceUnavailableException()); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - - // check the public keys are passed to the staking service in the expected format - expect(networkService.get).toHaveBeenNthCalledWith(3, { - url: `${stakingApiUrl}/v1/eth/stakes`, - networkRequest: expect.objectContaining({ - params: { - onchain_v1_include_net_rewards: true, - validators: validators.join(','), - }, - }), - }); - }); - }); - - describe('withdraw', () => { - it('returns the native staking `withdraw` confirmation view', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const validators = [ - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - ] as Array<`0x${string}`>; - const validatorPublicKey = concat(validators); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey], - }); - const dataDecoded = dataDecodedBuilder() - .with('method', 'batchWithdrawCLFee') - .with('parameters', [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ]) - .build(); - const stakes = [ - stakeBuilder() - .with('net_claimable_consensus_rewards', '1000000') - .build(), - stakeBuilder() - .with('net_claimable_consensus_rewards', '2000000') - .build(), - ]; - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_WITHDRAW', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - value: ( - +stakes[0].net_claimable_consensus_rewards! + - +stakes[1].net_claimable_consensus_rewards! - ).toString(), - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - validators, - }); - }); - - it('returns the native staking `withdraw` confirmation view using local decoding', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validators = [ - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - ] as Array<`0x${string}`>; - const validatorPublicKey = concat(validators); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey], - }); - const stakes = [ - stakeBuilder() - .with('net_claimable_consensus_rewards', '6000000') - .build(), - stakeBuilder() - .with('net_claimable_consensus_rewards', '2000000') - .build(), - ]; - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.resolve({ - data: rawify({ data: stakes }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.reject(new ServiceUnavailableException()); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - data, - }) - .expect(200) - .expect({ - type: 'KILN_NATIVE_STAKING_WITHDRAW', - method: 'batchWithdrawCLFee', - parameters: [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ], - value: ( - +stakes[0].net_claimable_consensus_rewards! + - +stakes[1].net_claimable_consensus_rewards! - ).toString(), - tokenInfo: { - address: NULL_ADDRESS, - decimals: chain.nativeCurrency.decimals, - logoUri: chain.nativeCurrency.logoUri, - name: chain.nativeCurrency.name, - symbol: chain.nativeCurrency.symbol, - trusted: true, - }, - validators, - }); - }); - - it('returns the generic confirmation view if the deployment is not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validatorPublicKey = faker.string.hexadecimal(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.reject(new NotFoundException()); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - value: faker.string.numeric(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment is not dedicated-specific', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validatorPublicKey = faker.string.hexadecimal(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'defi') // Not dedicated - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - value: faker.string.numeric(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the deployment chain is unknown', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validatorPublicKey = faker.string.hexadecimal(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('chain', 'unknown') // Unknown - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - value: faker.string.numeric(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if not transacting with a deployment address', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validatorPublicKey = faker.string.hexadecimal(); - const dataDecoded = dataDecodedBuilder().build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey as `0x${string}`], - }); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: faker.finance.ethereumAddress(), // Not the deployment address - value: faker.string.numeric(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - - it('returns the generic confirmation view if the stakes are not available', async () => { - const chain = chainBuilder().with('isTestnet', false).build(); - const validators = [ - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - faker.string.hexadecimal({ - length: KilnDecoder.KilnPublicKeyLength, - casing: 'lower', - }), - ] as Array<`0x${string}`>; - const validatorPublicKey = concat(validators); - const dataDecoded = dataDecodedBuilder() - .with('method', 'batchWithdrawCLFee') - .with('parameters', [ - { - name: '_publicKeys', - type: 'bytes', - value: validatorPublicKey, - valueDecoded: null, - }, - ]) - .build(); - const deployment = deploymentBuilder() - .with('chain_id', +chain.chainId) - .with('product_type', 'dedicated') - .with('product_fee', faker.number.float().toString()) - .build(); - const safeAddress = faker.finance.ethereumAddress(); - const data = encodeFunctionData({ - abi: parseAbi(['function batchWithdrawCLFee(bytes)']), - functionName: 'batchWithdrawCLFee', - args: [validatorPublicKey], - }); - networkService.get.mockImplementation(({ url }) => { - switch (url) { - case `${safeConfigUrl}/api/v1/chains/${chain.chainId}`: - return Promise.resolve({ data: rawify(chain), status: 200 }); - case `${stakingApiUrl}/v1/deployments`: - return Promise.resolve({ - data: rawify({ data: [deployment] }), - status: 200, - }); - case `${stakingApiUrl}/v1/eth/stakes`: - return Promise.reject(new ServiceUnavailableException()); - default: - return Promise.reject(new Error(`Could not match ${url}`)); - } - }); - networkService.post.mockImplementation(({ url }) => { - if (url === `${chain.transactionService}/api/v1/data-decoder/`) { - return Promise.resolve({ - data: rawify(dataDecoded), - status: 200, - }); - } - return Promise.reject(new Error(`Could not match ${url}`)); - }); - - await request(app.getHttpServer()) - .post( - `/v1/chains/${chain.chainId}/safes/${safeAddress}/views/transaction-confirmation`, - ) - .send({ - to: deployment.address, - value: faker.string.numeric(), - data, - }) - .expect(200) - .expect({ - type: 'GENERIC', - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - }); - }); - }); - }); -}); diff --git a/src/routes/transactions/transactions-view.controller.ts b/src/routes/transactions/transactions-view.controller.ts deleted file mode 100644 index dbdfb39a14..0000000000 --- a/src/routes/transactions/transactions-view.controller.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { DataDecodedRepositoryModule } from '@/domain/data-decoder/data-decoded.repository.interface'; -import { KilnDecoder } from '@/domain/staking/contracts/decoders/kiln-decoder.helper'; -import { ComposableCowDecoder } from '@/domain/swaps/contracts/decoders/composable-cow-decoder.helper'; -import { GPv2DecoderModule } from '@/domain/swaps/contracts/decoders/gp-v2-decoder.helper'; -import { SwapsRepositoryModule } from '@/domain/swaps/swaps-repository.module'; -import { - TransactionDataDto, - TransactionDataDtoSchema, -} from '@/routes/common/entities/transaction-data.dto.entity'; -import { - BaselineConfirmationView, - ConfirmationView, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; -import { CowSwapConfirmationView } from '@/routes/transactions/entities/swaps/swap-confirmation-view.entity'; -import { CowSwapTwapConfirmationView } from '@/routes/transactions/entities/swaps/twap-confirmation-view.entity'; -import { NativeStakingDepositConfirmationView } from '@/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity'; -import { NativeStakingValidatorsExitConfirmationView } from '@/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity'; -import { NativeStakingWithdrawConfirmationView } from '@/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity'; -import { KilnNativeStakingHelperModule } from '@/routes/transactions/helpers/kiln-native-staking.helper'; -import { SwapAppsHelperModule } from '@/routes/transactions/helpers/swap-apps.helper'; -import { SwapOrderHelperModule } from '@/routes/transactions/helpers/swap-order.helper'; -import { TwapOrderHelperModule } from '@/routes/transactions/helpers/twap-order.helper'; -import { NativeStakingMapperModule } from '@/routes/transactions/mappers/common/native-staking.mapper'; -import { TransactionsViewService } from '@/routes/transactions/transactions-view.service'; -import { AddressSchema } from '@/validation/entities/schemas/address.schema'; -import { NumericStringSchema } from '@/validation/entities/schemas/numeric-string.schema'; -import { ValidationPipe } from '@/validation/pipes/validation.pipe'; -import { - Body, - Controller, - HttpCode, - Module, - Param, - Post, -} from '@nestjs/common'; -import { - ApiExtraModels, - ApiOkResponse, - ApiOperation, - ApiTags, - getSchemaPath, -} from '@nestjs/swagger'; - -@ApiTags('transactions') -@Controller({ - path: '', - version: '1', -}) -export class TransactionsViewController { - constructor(private readonly service: TransactionsViewService) {} - - @HttpCode(200) - @ApiOkResponse({ - schema: { - oneOf: [ - { $ref: getSchemaPath(BaselineConfirmationView) }, - { $ref: getSchemaPath(CowSwapConfirmationView) }, - { $ref: getSchemaPath(CowSwapTwapConfirmationView) }, - { $ref: getSchemaPath(NativeStakingDepositConfirmationView) }, - { $ref: getSchemaPath(NativeStakingValidatorsExitConfirmationView) }, - { $ref: getSchemaPath(NativeStakingWithdrawConfirmationView) }, - ], - }, - }) - @ApiExtraModels( - BaselineConfirmationView, - CowSwapConfirmationView, - CowSwapTwapConfirmationView, - NativeStakingDepositConfirmationView, - NativeStakingValidatorsExitConfirmationView, - NativeStakingWithdrawConfirmationView, - ) - @ApiOperation({ - description: - 'Deprecated in favour of /v1/chains/:chainId/transactions/:safeAddress/preview.', - deprecated: true, - }) - @Post('chains/:chainId/safes/:safeAddress/views/transaction-confirmation') - async getTransactionConfirmationView( - @Param('chainId', new ValidationPipe(NumericStringSchema)) chainId: string, - @Param('safeAddress', new ValidationPipe(AddressSchema)) - safeAddress: `0x${string}`, - @Body(new ValidationPipe(TransactionDataDtoSchema)) - transactionDataDto: TransactionDataDto, - ): Promise { - return this.service.getTransactionConfirmationView({ - chainId, - safeAddress, - transactionDataDto, - }); - } -} - -@Module({ - imports: [ - DataDecodedRepositoryModule, - GPv2DecoderModule, - KilnNativeStakingHelperModule, - NativeStakingMapperModule, - SwapAppsHelperModule, - SwapOrderHelperModule, - SwapsRepositoryModule, - TwapOrderHelperModule, - ], - providers: [TransactionsViewService, ComposableCowDecoder, KilnDecoder], - controllers: [TransactionsViewController], -}) -export class TransactionsViewControllerModule {} diff --git a/src/routes/transactions/transactions-view.service.ts b/src/routes/transactions/transactions-view.service.ts deleted file mode 100644 index e48c24a9ba..0000000000 --- a/src/routes/transactions/transactions-view.service.ts +++ /dev/null @@ -1,423 +0,0 @@ -import { IDataDecodedRepository } from '@/domain/data-decoder/data-decoded.repository.interface'; -import { DataDecoded } from '@/domain/data-decoder/entities/data-decoded.entity'; -import { KilnDecoder } from '@/domain/staking/contracts/decoders/kiln-decoder.helper'; -import { ComposableCowDecoder } from '@/domain/swaps/contracts/decoders/composable-cow-decoder.helper'; -import { GPv2Decoder } from '@/domain/swaps/contracts/decoders/gp-v2-decoder.helper'; -import { OrderStatus } from '@/domain/swaps/entities/order.entity'; -import { ISwapsRepository } from '@/domain/swaps/swaps.repository'; -import { ILoggingService, LoggingService } from '@/logging/logging.interface'; -import { TransactionDataDto } from '@/routes/common/entities/transaction-data.dto.entity'; -import { - BaselineConfirmationView, - ConfirmationView, -} from '@/routes/transactions/entities/confirmation-view/confirmation-view.entity'; -import { CowSwapConfirmationView } from '@/routes/transactions/entities/swaps/swap-confirmation-view.entity'; -import { CowSwapTwapConfirmationView } from '@/routes/transactions/entities/swaps/twap-confirmation-view.entity'; -import { NativeStakingDepositConfirmationView } from '@/routes/transactions/entities/staking/native-staking-deposit-confirmation-view.entity'; -import { NativeStakingValidatorsExitConfirmationView } from '@/routes/transactions/entities/staking/native-staking-validators-exit-confirmation-view.entity'; -import { NativeStakingWithdrawConfirmationView } from '@/routes/transactions/entities/staking/native-staking-withdraw-confirmation-view.entity'; -import { TokenInfo } from '@/routes/transactions/entities/swaps/token-info.entity'; -import { KilnNativeStakingHelper } from '@/routes/transactions/helpers/kiln-native-staking.helper'; -import { SwapAppsHelper } from '@/routes/transactions/helpers/swap-apps.helper'; -import { SwapOrderHelper } from '@/routes/transactions/helpers/swap-order.helper'; -import { TwapOrderHelper } from '@/routes/transactions/helpers/twap-order.helper'; -import { NativeStakingMapper } from '@/routes/transactions/mappers/common/native-staking.mapper'; -import { Inject, Injectable } from '@nestjs/common'; - -@Injectable() -export class TransactionsViewService { - constructor( - @Inject(IDataDecodedRepository) - private readonly dataDecodedRepository: IDataDecodedRepository, - private readonly gpv2Decoder: GPv2Decoder, - private readonly swapOrderHelper: SwapOrderHelper, - @Inject(LoggingService) private readonly loggingService: ILoggingService, - private readonly twapOrderHelper: TwapOrderHelper, - @Inject(ISwapsRepository) - private readonly swapsRepository: ISwapsRepository, - private readonly composableCowDecoder: ComposableCowDecoder, - private readonly swapAppsHelper: SwapAppsHelper, - private readonly kilnNativeStakingHelper: KilnNativeStakingHelper, - private readonly nativeStakingMapper: NativeStakingMapper, - private readonly kilnDecoder: KilnDecoder, - ) {} - - async getTransactionConfirmationView(args: { - chainId: string; - safeAddress: `0x${string}`; - transactionDataDto: TransactionDataDto; - }): Promise { - const dataDecoded = await this.dataDecodedRepository - .getDataDecoded({ - chainId: args.chainId, - data: args.transactionDataDto.data, - to: args.transactionDataDto.to, - }) - .catch(() => { - // TODO: Remove after Kiln has verified contracts - // Fallback for unverified contracts - return { - method: '', - parameters: null, - }; - }); - - const swapOrderData = this.swapOrderHelper.findSwapOrder( - args.transactionDataDto.data, - ); - - const twapSwapOrderData = args.transactionDataDto.to - ? this.twapOrderHelper.findTwapOrder({ - to: args.transactionDataDto.to, - data: args.transactionDataDto.data, - }) - : null; - - const nativeStakingDepositTransaction = - this.kilnNativeStakingHelper.findDepositTransaction({ - to: args.transactionDataDto.to, - data: args.transactionDataDto.data, - // Value is always defined - value: args.transactionDataDto.value ?? '0', - }); - - const nativeStakingValidatorsExitTransaction = - this.kilnNativeStakingHelper.findValidatorsExitTransaction({ - to: args.transactionDataDto.to, - data: args.transactionDataDto.data, - // Value is always defined - value: args.transactionDataDto.value ?? '0', - }); - - const nativeStakingWithdrawTransaction = - this.kilnNativeStakingHelper.findWithdrawTransaction({ - to: args.transactionDataDto.to, - data: args.transactionDataDto.data, - // Value is always defined - value: args.transactionDataDto.value ?? '0', - }); - - if ( - !swapOrderData && - !twapSwapOrderData && - !nativeStakingDepositTransaction && - !nativeStakingValidatorsExitTransaction && - !nativeStakingWithdrawTransaction - ) { - return new BaselineConfirmationView({ - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - } - - try { - if (swapOrderData) { - return await this.getSwapOrderConfirmationView({ - chainId: args.chainId, - data: swapOrderData, - dataDecoded, - }); - } else if (twapSwapOrderData) { - return await this.getTwapOrderConfirmationView({ - chainId: args.chainId, - safeAddress: args.safeAddress, - data: twapSwapOrderData, - dataDecoded, - }); - } else if ( - nativeStakingDepositTransaction && - nativeStakingDepositTransaction.to - ) { - return await this.getNativeStakingDepositConfirmationView({ - to: nativeStakingDepositTransaction.to, - data: nativeStakingDepositTransaction.data, - chainId: args.chainId, - dataDecoded, - value: nativeStakingDepositTransaction.value, - }); - } else if ( - nativeStakingValidatorsExitTransaction && - nativeStakingValidatorsExitTransaction.to - ) { - return await this.getNativeStakingValidatorsExitConfirmationView({ - to: nativeStakingValidatorsExitTransaction.to, - data: nativeStakingValidatorsExitTransaction.data, - chainId: args.chainId, - safeAddress: args.safeAddress, - dataDecoded, - }); - } else if ( - nativeStakingWithdrawTransaction && - nativeStakingWithdrawTransaction.to - ) { - return await this.getNativeStakingWithdrawConfirmationView({ - to: nativeStakingWithdrawTransaction.to, - data: nativeStakingWithdrawTransaction.data, - safeAddress: args.safeAddress, - chainId: args.chainId, - dataDecoded, - }); - } else { - // Should not reach here - throw new Error('No swap order data found'); - } - } catch (error) { - this.loggingService.warn(error); - return new BaselineConfirmationView({ - method: dataDecoded.method, - parameters: dataDecoded.parameters, - }); - } - } - - private async getSwapOrderConfirmationView(args: { - chainId: string; - data: `0x${string}`; - dataDecoded: DataDecoded; - }): Promise { - const orderUid: `0x${string}` | null = - this.gpv2Decoder.getOrderUidFromSetPreSignature(args.data); - if (!orderUid) { - throw new Error('Order UID not found in transaction data'); - } - - const order = await this.swapOrderHelper.getOrder({ - chainId: args.chainId, - orderUid, - }); - - if (!this.swapAppsHelper.isAppAllowed(order)) { - throw new Error(`Unsupported App: ${order.fullAppData?.appCode}`); - } - - const [sellToken, buyToken] = await Promise.all([ - this.swapOrderHelper.getToken({ - chainId: args.chainId, - address: order.sellToken, - }), - this.swapOrderHelper.getToken({ - chainId: args.chainId, - address: order.buyToken, - }), - ]); - - return new CowSwapConfirmationView({ - method: args.dataDecoded.method, - parameters: args.dataDecoded.parameters, - uid: order.uid, - status: order.status, - kind: order.kind, - orderClass: order.class, - validUntil: order.validTo, - sellAmount: order.sellAmount.toString(), - buyAmount: order.buyAmount.toString(), - executedSellAmount: order.executedSellAmount.toString(), - executedBuyAmount: order.executedBuyAmount.toString(), - explorerUrl: this.swapOrderHelper.getOrderExplorerUrl(order).toString(), - sellToken: new TokenInfo({ - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }), - buyToken: new TokenInfo({ - address: buyToken.address, - decimals: buyToken.decimals, - logoUri: buyToken.logoUri, - name: buyToken.name, - symbol: buyToken.symbol, - trusted: buyToken.trusted, - }), - executedSurplusFee: order.executedSurplusFee.toString(), - executedFee: order.executedFee.toString(), - executedFeeToken: new TokenInfo({ - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }), - receiver: order.receiver, - owner: order.owner, - fullAppData: order.fullAppData, - }); - } - - private async getTwapOrderConfirmationView(args: { - chainId: string; - safeAddress: `0x${string}`; - data: `0x${string}`; - dataDecoded: DataDecoded; - }): Promise { - // Decode `staticInput` of `createWithContextCall` - const twapStruct = this.composableCowDecoder.decodeTwapStruct(args.data); - const twapOrderData = - this.twapOrderHelper.twapStructToPartialOrderInfo(twapStruct); - - // Generate parts of the TWAP order - const twapParts = this.twapOrderHelper.generateTwapOrderParts({ - twapStruct, - executionDate: new Date(), - chainId: args.chainId, - }); - - // Decode hash of `appData` - const fullAppData = await this.swapsRepository.getFullAppData( - args.chainId, - twapStruct.appData, - ); - - if (!this.swapAppsHelper.isAppAllowed(fullAppData)) { - throw new Error(`Unsupported App: ${fullAppData.fullAppData?.appCode}`); - } - - const [buyToken, sellToken] = await Promise.all([ - this.swapOrderHelper.getToken({ - chainId: args.chainId, - address: twapStruct.buyToken, - }), - this.swapOrderHelper.getToken({ - chainId: args.chainId, - address: twapStruct.sellToken, - }), - ]); - - return new CowSwapTwapConfirmationView({ - method: args.dataDecoded.method, - parameters: args.dataDecoded.parameters, - status: OrderStatus.PreSignaturePending, - kind: twapOrderData.kind, - class: twapOrderData.class, - activeOrderUid: null, - validUntil: Math.max(...twapParts.map((order) => order.validTo)), - sellAmount: twapOrderData.sellAmount, - buyAmount: twapOrderData.buyAmount, - executedSellAmount: '0', - executedBuyAmount: '0', - executedSurplusFee: '0', - executedFee: '0', - // TODO: still tbd by CoW but this will be expressed in SURPLUS tokens - // (BUY tokens for SELL orders and SELL tokens for BUY orders) - executedFeeToken: new TokenInfo({ - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }), - sellToken: new TokenInfo({ - address: sellToken.address, - decimals: sellToken.decimals, - logoUri: sellToken.logoUri, - name: sellToken.name, - symbol: sellToken.symbol, - trusted: sellToken.trusted, - }), - buyToken: new TokenInfo({ - address: buyToken.address, - decimals: buyToken.decimals, - logoUri: buyToken.logoUri, - name: buyToken.name, - symbol: buyToken.symbol, - trusted: buyToken.trusted, - }), - receiver: twapStruct.receiver, - owner: args.safeAddress, - fullAppData: fullAppData.fullAppData, - numberOfParts: twapOrderData.numberOfParts, - partSellAmount: twapStruct.partSellAmount.toString(), - minPartLimit: twapStruct.minPartLimit.toString(), - timeBetweenParts: twapOrderData.timeBetweenParts, - durationOfPart: twapOrderData.durationOfPart, - startTime: twapOrderData.startTime, - }); - } - - private async getNativeStakingDepositConfirmationView(args: { - chainId: string; - to: `0x${string}`; - data: `0x${string}`; - dataDecoded: DataDecoded; - value: string | null; - }): Promise { - const dataDecoded = - args.dataDecoded.method !== '' - ? args.dataDecoded - : this.kilnDecoder.decodeDeposit(args.data); - if (!dataDecoded) { - throw new Error('Transaction data could not be decoded'); - } - const depositInfo = await this.nativeStakingMapper.mapDepositInfo({ - chainId: args.chainId, - to: args.to, - value: args.value, - txHash: null, - }); - return new NativeStakingDepositConfirmationView({ - method: dataDecoded.method, - parameters: dataDecoded.parameters, - ...depositInfo, - }); - } - - private async getNativeStakingValidatorsExitConfirmationView(args: { - chainId: string; - safeAddress: `0x${string}`; - to: `0x${string}`; - data: `0x${string}`; - dataDecoded: DataDecoded; - }): Promise { - const dataDecoded = - args.dataDecoded.method !== '' - ? args.dataDecoded - : this.kilnDecoder.decodeValidatorsExit(args.data); - if (!dataDecoded) { - throw new Error('Transaction data could not be decoded'); - } - const validatorsExitInfo = - await this.nativeStakingMapper.mapValidatorsExitInfo({ - chainId: args.chainId, - safeAddress: args.safeAddress, - to: args.to, - data: args.data, - }); - - return new NativeStakingValidatorsExitConfirmationView({ - method: dataDecoded.method, - parameters: dataDecoded.parameters, - ...validatorsExitInfo, - }); - } - - private async getNativeStakingWithdrawConfirmationView(args: { - chainId: string; - safeAddress: `0x${string}`; - to: `0x${string}`; - data: `0x${string}`; - dataDecoded: DataDecoded; - }): Promise { - const dataDecoded = - args.dataDecoded.method !== '' - ? args.dataDecoded - : this.kilnDecoder.decodeBatchWithdrawCLFee(args.data); - if (!dataDecoded) { - throw new Error('Transaction data could not be decoded'); - } - const withdrawInfo = await this.nativeStakingMapper.mapWithdrawInfo({ - chainId: args.chainId, - safeAddress: args.safeAddress, - to: args.to, - data: args.data, - txHash: null, - }); - return new NativeStakingWithdrawConfirmationView({ - method: dataDecoded.method, - parameters: dataDecoded.parameters, - ...withdrawInfo, - }); - } -}