From b5696c10cfd64ce393054b0d2ecb57c2af8c887e Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Tue, 28 Jan 2025 16:53:39 +0100 Subject: [PATCH 1/3] Create token-specific entities --- src/domain/tokens/__tests__/token.builder.ts | 59 +++++++++--- .../entities/__tests__/token.entity.spec.ts | 92 +++++++++++++++++++ .../schemas/__tests__/token.schema.spec.ts | 59 ------------ .../tokens/entities/schemas/token.schema.ts | 16 ---- src/domain/tokens/entities/token.entity.ts | 51 ++++++++-- src/domain/tokens/token.repository.ts | 2 +- src/routes/balances/balances.service.ts | 9 +- .../balances/entities/balance.entity.ts | 19 +++- .../balances/entities/token-type.entity.ts | 6 -- src/routes/balances/entities/token.entity.ts | 42 +++++++-- ...rs-by-safe.transactions.controller.spec.ts | 18 ++-- ...ns-by-safe.transactions.controller.spec.ts | 12 +-- .../multisig-execution-details.entity.ts | 27 +++++- .../transactions/helpers/swap-order.helper.ts | 6 +- .../mappers/common/erc20-transfer.mapper.ts | 4 +- .../mappers/common/erc721-transfer.mapper.ts | 4 +- .../common/human-descriptions.mapper.spec.ts | 4 +- .../mappers/common/transaction-info.mapper.ts | 5 +- .../swap-transfer-info.mapper.spec.ts | 6 +- .../transfers/transfer-info.mapper.spec.ts | 10 +- .../mappers/transfers/transfer-info.mapper.ts | 31 ++++--- .../mappers/transfers/transfer.mapper.spec.ts | 34 +++---- .../transactions-history.controller.spec.ts | 33 +++---- ....imitation-transactions.controller.spec.ts | 32 +++---- 24 files changed, 358 insertions(+), 223 deletions(-) create mode 100644 src/domain/tokens/entities/__tests__/token.entity.spec.ts delete mode 100644 src/domain/tokens/entities/schemas/__tests__/token.schema.spec.ts delete mode 100644 src/domain/tokens/entities/schemas/token.schema.ts delete mode 100644 src/routes/balances/entities/token-type.entity.ts diff --git a/src/domain/tokens/__tests__/token.builder.ts b/src/domain/tokens/__tests__/token.builder.ts index 185a99ad3f..0a31aeb917 100644 --- a/src/domain/tokens/__tests__/token.builder.ts +++ b/src/domain/tokens/__tests__/token.builder.ts @@ -1,21 +1,52 @@ import { faker } from '@faker-js/faker'; import type { IBuilder } from '@/__tests__/builder'; import { Builder } from '@/__tests__/builder'; -import type { Token } from '@/domain/tokens/entities/token.entity'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import type { + Erc20Token, + Erc721Token, + NativeToken, + Token, +} from '@/domain/tokens/entities/token.entity'; import { getAddress } from 'viem'; +export function nativeTokenBuilder(): IBuilder { + return new Builder() + .with('type', 'NATIVE_TOKEN') + .with('decimals', faker.number.int({ min: 0, max: 18 })) + .with('address', getAddress(faker.finance.ethereumAddress())) + .with('logoUri', faker.internet.url({ appendSlash: false })) + .with('name', faker.word.sample()) + .with('symbol', faker.finance.currencySymbol()) + .with('trusted', faker.datatype.boolean()); +} + +export function erc20TokenBuilder(): IBuilder { + return new Builder() + .with('type', 'ERC20') + .with('decimals', faker.number.int({ min: 0, max: 18 })) + .with('address', getAddress(faker.finance.ethereumAddress())) + .with('logoUri', faker.internet.url({ appendSlash: false })) + .with('name', faker.word.sample()) + .with('symbol', faker.finance.currencySymbol()) + .with('trusted', faker.datatype.boolean()); +} + +export function erc721TokenBuilder(): IBuilder { + return new Builder() + .with('type', 'ERC721') + .with('decimals', 0) + .with('trusted', true) + .with('address', getAddress(faker.finance.ethereumAddress())) + .with('logoUri', faker.internet.url({ appendSlash: false })) + .with('name', faker.word.sample()) + .with('symbol', faker.finance.currencySymbol()) + .with('trusted', faker.datatype.boolean()); +} + export function tokenBuilder(): IBuilder { - return ( - new Builder() - .with('address', getAddress(faker.finance.ethereumAddress())) - // min/max boundaries are set here in order to prevent overflows on balances calculation. - // See: https://github.com/safe-global/safe-client-gateway/blob/65364f9ad31fc9832b32248f74356c4f6660787e/src/datasources/balances-api/safe-balances-api.service.ts#L173 - .with('decimals', faker.number.int({ min: 0, max: 32 })) - .with('logoUri', faker.internet.url({ appendSlash: false })) - .with('name', faker.word.sample()) - .with('symbol', faker.finance.currencySymbol()) - .with('type', faker.helpers.arrayElement(Object.values(TokenType))) - .with('trusted', faker.datatype.boolean()) - ); + return faker.helpers.arrayElement([ + nativeTokenBuilder, + erc20TokenBuilder, + erc721TokenBuilder, + ])(); } diff --git a/src/domain/tokens/entities/__tests__/token.entity.spec.ts b/src/domain/tokens/entities/__tests__/token.entity.spec.ts new file mode 100644 index 0000000000..3d9d53d99c --- /dev/null +++ b/src/domain/tokens/entities/__tests__/token.entity.spec.ts @@ -0,0 +1,92 @@ +import { + erc20TokenBuilder, + erc721TokenBuilder, + nativeTokenBuilder, + tokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; +import { TokenSchema } from '@/domain/tokens/entities/token.entity'; +import type { Token } from '@/domain/tokens/entities/token.entity'; +import { faker } from '@faker-js/faker'; +import { getAddress } from 'viem'; + +describe('Token', () => { + it('should validate a token', () => { + const token = tokenBuilder().build(); + + const result = TokenSchema.safeParse(token); + + expect(result.success).toBe(true); + }); + + it('should checksum address', () => { + const nonChecksummedAddress = faker.finance + .ethereumAddress() + .toLowerCase() as `0x${string}`; + const token = tokenBuilder().with('address', nonChecksummedAddress).build(); + + const result = TokenSchema.safeParse(token); + + expect(result.success && result.data['address']).toBe( + getAddress(nonChecksummedAddress), + ); + }); + + it.each([ + 'address', + 'logoUri', + 'name', + 'symbol', + 'type', + 'trusted', + ])('should not allow %s to be undefined', (key) => { + const token = tokenBuilder().build(); + delete token[key]; + + const result = TokenSchema.safeParse(token); + + expect( + !result.success && + result.error.issues.length === 1 && + result.error.issues[0].path.length === 1 && + result.error.issues[0].path[0] === key, + ).toBe(true); + }); + + it('should not allow native tokens to have undefined decimals', () => { + const token = nativeTokenBuilder().build(); + // @ts-expect-error - inferred type does not allow undefined decimals + delete token.decimals; + + const result = TokenSchema.safeParse(token); + + expect(!result.success && result.error.issues).toStrictEqual([ + { + code: 'invalid_type', + expected: 'number', + message: 'Required', + path: ['decimals'], + received: 'undefined', + }, + ]); + }); + + it('should default ERC-20 decimals to 18', () => { + const token = erc20TokenBuilder().build(); + // @ts-expect-error - inferred type does not allow undefined decimals + delete token.decimals; + + const result = TokenSchema.safeParse(token); + + expect(result.success && result.data.decimals).toBe(18); + }); + + it('should default ERC-721 decimals to 0', () => { + const token = erc721TokenBuilder().build(); + // @ts-expect-error - inferred type does not allow undefined decimals + delete token.decimals; + + const result = TokenSchema.safeParse(token); + + expect(result.success && result.data.decimals).toBe(0); + }); +}); diff --git a/src/domain/tokens/entities/schemas/__tests__/token.schema.spec.ts b/src/domain/tokens/entities/schemas/__tests__/token.schema.spec.ts deleted file mode 100644 index 1f6494113f..0000000000 --- a/src/domain/tokens/entities/schemas/__tests__/token.schema.spec.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenSchema } from '@/domain/tokens/entities/schemas/token.schema'; -import type { Token } from '@/domain/tokens/entities/token.entity'; -import { faker } from '@faker-js/faker'; -import { getAddress } from 'viem'; - -describe('TokenSchema', () => { - it('should validate a token', () => { - const token = tokenBuilder().build(); - - const result = TokenSchema.safeParse(token); - - expect(result.success).toBe(true); - }); - - it('should checksum address', () => { - const nonChecksummedAddress = faker.finance - .ethereumAddress() - .toLowerCase() as `0x${string}`; - const token = tokenBuilder().with('address', nonChecksummedAddress).build(); - - const result = TokenSchema.safeParse(token); - - expect(result.success && result.data['address']).toBe( - getAddress(nonChecksummedAddress), - ); - }); - - it('should allow undefined decimals', () => { - const token = tokenBuilder().build(); - // @ts-expect-error - inferred type doesn't allow optional properties - delete token.decimals; - - const result = TokenSchema.safeParse(token); - - expect(result.success && result.data.decimals).toBe(null); - }); - - it.each([ - 'address', - 'logoUri', - 'name', - 'symbol', - 'type', - 'trusted', - ])('should not allow %s to be undefined', (key) => { - const token = tokenBuilder().build(); - delete token[key]; - - const result = TokenSchema.safeParse(token); - - expect( - !result.success && - result.error.issues.length === 1 && - result.error.issues[0].path.length === 1 && - result.error.issues[0].path[0] === key, - ).toBe(true); - }); -}); diff --git a/src/domain/tokens/entities/schemas/token.schema.ts b/src/domain/tokens/entities/schemas/token.schema.ts deleted file mode 100644 index 08df686eb0..0000000000 --- a/src/domain/tokens/entities/schemas/token.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { buildPageSchema } from '@/domain/entities/schemas/page.schema.factory'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; -import { AddressSchema } from '@/validation/entities/schemas/address.schema'; -import { z } from 'zod'; - -export const TokenSchema = z.object({ - address: AddressSchema, - decimals: z.number().nullish().default(null), - logoUri: z.string(), - name: z.string(), - symbol: z.string(), - type: z.nativeEnum(TokenType), - trusted: z.boolean(), -}); - -export const TokenPageSchema = buildPageSchema(TokenSchema); diff --git a/src/domain/tokens/entities/token.entity.ts b/src/domain/tokens/entities/token.entity.ts index 21e46d5656..9ed99ef662 100644 --- a/src/domain/tokens/entities/token.entity.ts +++ b/src/domain/tokens/entities/token.entity.ts @@ -1,10 +1,45 @@ -import type { TokenSchema } from '@/domain/tokens/entities/schemas/token.schema'; -import type { z } from 'zod'; - -export enum TokenType { - Erc721 = 'ERC721', - Erc20 = 'ERC20', - NativeToken = 'NATIVE_TOKEN', -} +import { z } from 'zod'; +import { buildPageSchema } from '@/domain/entities/schemas/page.schema.factory'; +import { AddressSchema } from '@/validation/entities/schemas/address.schema'; + +const DEFAULT_ERC20_DECIMALS = 18; +const DEFAULT_ERC721_DECIMALS = 0; + +const BaseTokenSchema = z.object({ + address: AddressSchema, + logoUri: z.string().url(), + name: z.string(), + symbol: z.string(), + trusted: z.boolean(), +}); + +const NativeTokenSchema = BaseTokenSchema.extend({ + type: z.literal('NATIVE_TOKEN'), + decimals: z.number(), +}); + +const Erc20TokenSchema = BaseTokenSchema.extend({ + type: z.literal('ERC20'), + decimals: z.number().catch(DEFAULT_ERC20_DECIMALS), +}); + +const Erc721TokenSchema = BaseTokenSchema.extend({ + type: z.literal('ERC721'), + decimals: z.number().catch(DEFAULT_ERC721_DECIMALS), +}); + +export const TokenSchema = z.discriminatedUnion('type', [ + NativeTokenSchema, + Erc20TokenSchema, + Erc721TokenSchema, +]); + +export const TokenPageSchema = buildPageSchema(TokenSchema); + +export type NativeToken = z.infer; + +export type Erc20Token = z.infer; + +export type Erc721Token = z.infer; export type Token = z.infer; diff --git a/src/domain/tokens/token.repository.ts b/src/domain/tokens/token.repository.ts index ff010476ed..c9a7c4f077 100644 --- a/src/domain/tokens/token.repository.ts +++ b/src/domain/tokens/token.repository.ts @@ -6,7 +6,7 @@ import { ITokenRepository } from '@/domain/tokens/token.repository.interface'; import { TokenPageSchema, TokenSchema, -} from '@/domain/tokens/entities/schemas/token.schema'; +} from '@/domain/tokens/entities/token.entity'; @Injectable() export class TokenRepository implements ITokenRepository { diff --git a/src/routes/balances/balances.service.ts b/src/routes/balances/balances.service.ts index f770cd876e..58768b64af 100644 --- a/src/routes/balances/balances.service.ts +++ b/src/routes/balances/balances.service.ts @@ -5,7 +5,10 @@ import { IChainsRepository } from '@/domain/chains/chains.repository.interface'; import { NativeCurrency } from '@/domain/chains/entities/native.currency.entity'; import { Balance } from '@/routes/balances/entities/balance.entity'; import { Balances } from '@/routes/balances/entities/balances.entity'; -import { TokenType } from '@/routes/balances/entities/token-type.entity'; +import { + NativeToken, + Erc20Token, +} from '@/routes/balances/entities/token.entity'; import { NULL_ADDRESS } from '@/routes/common/constants'; import orderBy from 'lodash/orderBy'; import { getNumberString } from '@/domain/common/utils/utils'; @@ -50,8 +53,8 @@ export class BalancesService { nativeCurrency: NativeCurrency, ): Balance { const tokenAddress = balance.tokenAddress; - const tokenType = - tokenAddress === null ? TokenType.NativeToken : TokenType.Erc20; + const tokenType: (NativeToken | Erc20Token)['type'] = + tokenAddress === null ? 'NATIVE_TOKEN' : 'ERC20'; const tokenMetaData = tokenAddress === null diff --git a/src/routes/balances/entities/balance.entity.ts b/src/routes/balances/entities/balance.entity.ts index d29335d573..6df85ceee4 100644 --- a/src/routes/balances/entities/balance.entity.ts +++ b/src/routes/balances/entities/balance.entity.ts @@ -1,6 +1,11 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Token } from '@/routes/balances/entities/token.entity'; +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; +import { + NativeToken, + Erc20Token, + Erc721Token, +} from '@/routes/balances/entities/token.entity'; +@ApiExtraModels(NativeToken, Erc20Token, Erc721Token) export class Balance { @ApiProperty() balance!: string; @@ -8,6 +13,12 @@ export class Balance { fiatBalance!: string; @ApiProperty() fiatConversion!: string; - @ApiProperty() - tokenInfo!: Token; + @ApiProperty({ + oneOf: [ + { $ref: getSchemaPath(NativeToken) }, + { $ref: getSchemaPath(Erc20Token) }, + { $ref: getSchemaPath(Erc721Token) }, + ], + }) + tokenInfo!: NativeToken | Erc20Token | Erc721Token; } diff --git a/src/routes/balances/entities/token-type.entity.ts b/src/routes/balances/entities/token-type.entity.ts deleted file mode 100644 index bd670cf83b..0000000000 --- a/src/routes/balances/entities/token-type.entity.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum TokenType { - Erc721 = 'ERC721', - Erc20 = 'ERC20', - NativeToken = 'NATIVE_TOKEN', - Unknown = 'UNKNOWN', -} diff --git a/src/routes/balances/entities/token.entity.ts b/src/routes/balances/entities/token.entity.ts index df590905fe..d424351ec3 100644 --- a/src/routes/balances/entities/token.entity.ts +++ b/src/routes/balances/entities/token.entity.ts @@ -1,17 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { TokenType } from '@/routes/balances/entities/token-type.entity'; +import { ApiProperty } from '@nestjs/swagger'; +import { + NativeToken as DomainNativeToken, + Erc20Token as DomainErc20Token, + Erc721Token as DomainErc721Token, +} from '@/domain/tokens/entities/token.entity'; -export class Token { +class BaseToken { @ApiProperty() address!: `0x${string}`; - @ApiPropertyOptional({ type: Number, nullable: true }) - decimals!: number | null; @ApiProperty() - logoUri?: string; + decimals!: number; + @ApiProperty() + logoUri!: string; @ApiProperty() name!: string; @ApiProperty() symbol!: string; - @ApiProperty({ enum: Object.values(TokenType) }) - type!: TokenType; +} + +export class NativeToken + extends BaseToken + implements Omit +{ + @ApiProperty({ enum: ['NATIVE_TOKEN'] }) + type!: 'NATIVE_TOKEN'; +} + +export class Erc20Token + extends BaseToken + implements Omit +{ + @ApiProperty({ enum: ['ERC20'] }) + type!: 'ERC20'; +} + +export class Erc721Token + extends BaseToken + implements Omit +{ + @ApiProperty({ enum: ['ERC721'] }) + type!: 'ERC721'; } diff --git a/src/routes/transactions/__tests__/controllers/list-incoming-transfers-by-safe.transactions.controller.spec.ts b/src/routes/transactions/__tests__/controllers/list-incoming-transfers-by-safe.transactions.controller.spec.ts index c1f56b7550..5c2669564f 100644 --- a/src/routes/transactions/__tests__/controllers/list-incoming-transfers-by-safe.transactions.controller.spec.ts +++ b/src/routes/transactions/__tests__/controllers/list-incoming-transfers-by-safe.transactions.controller.spec.ts @@ -23,8 +23,10 @@ import { toJson as nativeTokenTransferToJson, } from '@/domain/safe/entities/__tests__/native-token-transfer.builder'; import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import { + erc20TokenBuilder, + erc721TokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; import type { INetworkService } from '@/datasources/network/network.service.interface'; import { NetworkService } from '@/datasources/network/network.service.interface'; @@ -203,8 +205,7 @@ describe('List incoming transfers by Safe - Transactions Controller (Unit)', () .with('transferId', 'e1015fc6905') .with('value', faker.number.int({ min: 1 }).toString()) .build(); - const token = tokenBuilder() - .with('type', TokenType.Erc20) + const token = erc20TokenBuilder() .with('address', getAddress(erc20Transfer.tokenAddress)) .with('trusted', true) .build(); @@ -290,8 +291,7 @@ describe('List incoming transfers by Safe - Transactions Controller (Unit)', () .with('value', faker.number.int({ min: 1 }).toString()) .build(); const trusted = false; - const token = tokenBuilder() - .with('type', TokenType.Erc20) + const token = erc20TokenBuilder() .with('address', getAddress(erc20Transfer.tokenAddress)) .with('trusted', trusted) .build(); @@ -376,8 +376,7 @@ describe('List incoming transfers by Safe - Transactions Controller (Unit)', () .with('transferId', 'e1015fc6905') .with('value', faker.number.int({ min: 1 }).toString()) .build(); - const token = tokenBuilder() - .with('type', TokenType.Erc20) + const token = erc20TokenBuilder() .with('address', getAddress(erc20Transfer.tokenAddress)) .with('trusted', false) .build(); @@ -432,8 +431,7 @@ describe('List incoming transfers by Safe - Transactions Controller (Unit)', () .with('to', safe.address) .with('transferId', 'e1015fc6905') .build(); - const token = tokenBuilder() - .with('type', TokenType.Erc721) + const token = erc721TokenBuilder() .with('address', getAddress(erc721Transfer.tokenAddress)) .build(); networkService.get.mockImplementation(({ url }) => { diff --git a/src/routes/transactions/__tests__/controllers/list-multisig-transactions-by-safe.transactions.controller.spec.ts b/src/routes/transactions/__tests__/controllers/list-multisig-transactions-by-safe.transactions.controller.spec.ts index 3d4d8890b3..98523817c7 100644 --- a/src/routes/transactions/__tests__/controllers/list-multisig-transactions-by-safe.transactions.controller.spec.ts +++ b/src/routes/transactions/__tests__/controllers/list-multisig-transactions-by-safe.transactions.controller.spec.ts @@ -22,8 +22,10 @@ import { toJson as multisigTransactionToJson, } from '@/domain/safe/entities/__tests__/multisig-transaction.builder'; import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import { + erc20TokenBuilder, + erc721TokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; import type { INetworkService } from '@/datasources/network/network.service.interface'; import { NetworkService } from '@/datasources/network/network.service.interface'; @@ -227,8 +229,7 @@ describe('List multisig transactions by Safe - Transactions Controller (Unit)', confirmationBuilder().build(), ]) .build(); - const token = tokenBuilder() - .with('type', TokenType.Erc20) + const token = erc20TokenBuilder() .with('address', getAddress(multisigTransaction.to)) .build(); networkService.get.mockImplementation(({ url }) => { @@ -313,8 +314,7 @@ describe('List multisig transactions by Safe - Transactions Controller (Unit)', it('Should get a ERC721 transfer mapped to the expected format', async () => { const chain = chainBuilder().build(); const safe = safeBuilder().build(); - const token = tokenBuilder() - .with('type', TokenType.Erc721) + const token = erc721TokenBuilder() .with('address', '0x7Af3460d552f832fD7E2DE973c628ACeA59B0712') .build(); const multisigTransaction = multisigTransactionBuilder() diff --git a/src/routes/transactions/entities/transaction-details/multisig-execution-details.entity.ts b/src/routes/transactions/entities/transaction-details/multisig-execution-details.entity.ts index dfce7dbd44..f7dc4ea420 100644 --- a/src/routes/transactions/entities/transaction-details/multisig-execution-details.entity.ts +++ b/src/routes/transactions/entities/transaction-details/multisig-execution-details.entity.ts @@ -1,6 +1,15 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { + ApiExtraModels, + ApiProperty, + ApiPropertyOptional, + getSchemaPath, +} from '@nestjs/swagger'; import { AddressInfo } from '@/routes/common/entities/address-info.entity'; -import { Token } from '@/routes/balances/entities/token.entity'; +import { + Erc20Token, + Erc721Token, + NativeToken, +} from '@/routes/balances/entities/token.entity'; import { ExecutionDetails, ExecutionDetailsType, @@ -25,6 +34,7 @@ export class MultisigConfirmationDetails { } } +@ApiExtraModels(NativeToken, Erc20Token, Erc721Token) export class MultisigExecutionDetails extends ExecutionDetails { @ApiProperty({ enum: [ExecutionDetailsType.Multisig] }) override type = ExecutionDetailsType.Multisig; @@ -54,8 +64,15 @@ export class MultisigExecutionDetails extends ExecutionDetails { confirmations: Array; @ApiProperty({ type: AddressInfo, isArray: true }) rejectors: Array; - @ApiPropertyOptional({ type: Token, nullable: true }) - gasTokenInfo: Token | null; + @ApiPropertyOptional({ + oneOf: [ + { $ref: getSchemaPath(NativeToken) }, + { $ref: getSchemaPath(Erc20Token) }, + { $ref: getSchemaPath(Erc721Token) }, + ], + nullable: true, + }) + gasTokenInfo: NativeToken | Erc20Token | Erc721Token | null; @ApiProperty() trusted: boolean; @ApiPropertyOptional({ type: AddressInfo, nullable: true }) @@ -77,7 +94,7 @@ export class MultisigExecutionDetails extends ExecutionDetails { confirmationsRequired: number, confirmations: Array, rejectors: Array, - gasTokenInfo: Token | null, + gasTokenInfo: NativeToken | Erc20Token | Erc721Token | null, trusted: boolean, proposer: AddressInfo | null, proposedByDelegate: AddressInfo | null, diff --git a/src/routes/transactions/helpers/swap-order.helper.ts b/src/routes/transactions/helpers/swap-order.helper.ts index 4a15948805..a058f3c037 100644 --- a/src/routes/transactions/helpers/swap-order.helper.ts +++ b/src/routes/transactions/helpers/swap-order.helper.ts @@ -9,7 +9,7 @@ import { TokenRepositoryModule, } from '@/domain/tokens/token.repository.interface'; import { ISwapsRepository } from '@/domain/swaps/swaps.repository'; -import { Token, TokenType } from '@/domain/tokens/entities/token.entity'; +import { Token } from '@/domain/tokens/entities/token.entity'; import { KnownOrder, Order, @@ -110,6 +110,8 @@ export class SwapOrderHelper { } /** + * TODO: Investigate if needed after token-specific entities. This was required for decimals. + * * Retrieves a token object based on the provided Ethereum chain ID and token address. * If the specified address is the placeholder for the native currency of the chain, * it fetches the chain's native currency details from the {@link IChainsRepository}. @@ -142,7 +144,7 @@ export class SwapOrderHelper { logoUri: nativeCurrency.logoUri, name: nativeCurrency.name, symbol: nativeCurrency.symbol, - type: TokenType.NativeToken, + type: 'NATIVE_TOKEN', trusted: true, }; } else { diff --git a/src/routes/transactions/mappers/common/erc20-transfer.mapper.ts b/src/routes/transactions/mappers/common/erc20-transfer.mapper.ts index ca5adfa9e5..3da6b3cfd8 100644 --- a/src/routes/transactions/mappers/common/erc20-transfer.mapper.ts +++ b/src/routes/transactions/mappers/common/erc20-transfer.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ModuleTransaction } from '@/domain/safe/entities/module-transaction.entity'; import { MultisigTransaction } from '@/domain/safe/entities/multisig-transaction.entity'; -import { Token } from '@/domain/tokens/entities/token.entity'; +import { Erc20Token } from '@/domain/tokens/entities/token.entity'; import { AddressInfoHelper } from '@/routes/common/address-info/address-info.helper'; import { NULL_ADDRESS } from '@/routes/common/constants'; import { TransferTransactionInfo } from '@/routes/transactions/entities/transfer-transaction-info.entity'; @@ -18,7 +18,7 @@ export class Erc20TransferMapper { ) {} async mapErc20Transfer( - token: Token, + token: Erc20Token, chainId: string, transaction: MultisigTransaction | ModuleTransaction, humanDescription: string | null, diff --git a/src/routes/transactions/mappers/common/erc721-transfer.mapper.ts b/src/routes/transactions/mappers/common/erc721-transfer.mapper.ts index 4cd763c6fa..23cc72e2b6 100644 --- a/src/routes/transactions/mappers/common/erc721-transfer.mapper.ts +++ b/src/routes/transactions/mappers/common/erc721-transfer.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { ModuleTransaction } from '@/domain/safe/entities/module-transaction.entity'; import { MultisigTransaction } from '@/domain/safe/entities/multisig-transaction.entity'; -import { Token } from '@/domain/tokens/entities/token.entity'; +import { Erc721Token } from '@/domain/tokens/entities/token.entity'; import { AddressInfoHelper } from '@/routes/common/address-info/address-info.helper'; import { NULL_ADDRESS } from '@/routes/common/constants'; import { TransferTransactionInfo } from '@/routes/transactions/entities/transfer-transaction-info.entity'; @@ -18,7 +18,7 @@ export class Erc721TransferMapper { ) {} async mapErc721Transfer( - token: Token, + token: Erc721Token, chainId: string, transaction: MultisigTransaction | ModuleTransaction, humanDescription: string | null, diff --git a/src/routes/transactions/mappers/common/human-descriptions.mapper.spec.ts b/src/routes/transactions/mappers/common/human-descriptions.mapper.spec.ts index 87688a1ba2..7001ad3a2f 100644 --- a/src/routes/transactions/mappers/common/human-descriptions.mapper.spec.ts +++ b/src/routes/transactions/mappers/common/human-descriptions.mapper.spec.ts @@ -112,7 +112,7 @@ describe('Human descriptions mapper (Unit)', () => { ); expect(humanDescription).toEqual( - `Send ${formatUnits(mockAmount, token.decimals!)} ${token.symbol} to ${truncateAddress(mockAddress)}`, + `Send ${formatUnits(mockAmount, token.decimals)} ${token.symbol} to ${truncateAddress(mockAddress)}`, ); }); @@ -189,7 +189,7 @@ describe('Human descriptions mapper (Unit)', () => { ); expect(humanDescription).toEqual( - `Send ${formatUnits(mockAmount, token.decimals!)} ${token.symbol} to ${truncateAddress(mockAddress)} via ${mockSafeAppName}`, + `Send ${formatUnits(mockAmount, token.decimals)} ${token.symbol} to ${truncateAddress(mockAddress)} via ${mockSafeAppName}`, ); }); }); diff --git a/src/routes/transactions/mappers/common/transaction-info.mapper.ts b/src/routes/transactions/mappers/common/transaction-info.mapper.ts index aa7b7c1d9a..b880f8aeba 100644 --- a/src/routes/transactions/mappers/common/transaction-info.mapper.ts +++ b/src/routes/transactions/mappers/common/transaction-info.mapper.ts @@ -4,7 +4,6 @@ import { MultisigTransaction } from '@/domain/safe/entities/multisig-transaction import { Operation } from '@/domain/safe/entities/operation.entity'; import { TokenRepository } from '@/domain/tokens/token.repository'; import { ITokenRepository } from '@/domain/tokens/token.repository.interface'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; import { DataDecodedParameter } from '@/routes/data-decode/entities/data-decoded-parameter.entity'; import { DataDecoded } from '@/routes/data-decode/entities/data-decoded.entity'; import { SettingsChangeTransaction } from '@/routes/transactions/entities/settings-change-transaction.entity'; @@ -175,14 +174,14 @@ export class MultisigTransactionInfoMapper { .catch(() => null); switch (token?.type) { - case TokenType.Erc20: + case 'ERC20': return this.erc20TransferMapper.mapErc20Transfer( token, chainId, transaction, humanDescription, ); - case TokenType.Erc721: + case 'ERC721': return this.erc721TransferMapper.mapErc721Transfer( token, chainId, diff --git a/src/routes/transactions/mappers/transfers/swap-transfer-info.mapper.spec.ts b/src/routes/transactions/mappers/transfers/swap-transfer-info.mapper.spec.ts index 03dc13ce88..61da53653d 100644 --- a/src/routes/transactions/mappers/transfers/swap-transfer-info.mapper.spec.ts +++ b/src/routes/transactions/mappers/transfers/swap-transfer-info.mapper.spec.ts @@ -119,7 +119,7 @@ describe('SwapTransferInfoMapper', () => { mockSwapsRepository.getOrders.mockResolvedValue([order]); mockSwapOrderHelper.getToken.mockResolvedValue({ ...token, - decimals: token.decimals!, + decimals: token.decimals, }); mockSwapOrderHelper.getOrderExplorerUrl.mockReturnValue( new URL(explorerUrl), @@ -202,7 +202,7 @@ describe('SwapTransferInfoMapper', () => { mockSwapsRepository.getOrders.mockResolvedValue([order]); mockSwapOrderHelper.getToken.mockResolvedValue({ ...token, - decimals: token.decimals!, + decimals: token.decimals, }); mockSwapOrderHelper.getOrderExplorerUrl.mockReturnValue( new URL(explorerUrl), @@ -364,7 +364,7 @@ describe('SwapTransferInfoMapper', () => { mockSwapsRepository.getOrders.mockResolvedValue(orders); mockSwapOrderHelper.getToken.mockResolvedValue({ ...token, - decimals: token.decimals!, + decimals: token.decimals, }); mockSwapOrderHelper.getOrderExplorerUrl.mockReturnValue( new URL(explorerUrl), diff --git a/src/routes/transactions/mappers/transfers/transfer-info.mapper.spec.ts b/src/routes/transactions/mappers/transfers/transfer-info.mapper.spec.ts index 4f8d81d32f..6aacc7543e 100644 --- a/src/routes/transactions/mappers/transfers/transfer-info.mapper.spec.ts +++ b/src/routes/transactions/mappers/transfers/transfer-info.mapper.spec.ts @@ -4,7 +4,11 @@ import { erc20TransferBuilder } from '@/domain/safe/entities/__tests__/erc20-tra import { erc721TransferBuilder } from '@/domain/safe/entities/__tests__/erc721-transfer.builder'; import { nativeTokenTransferBuilder } from '@/domain/safe/entities/__tests__/native-token-transfer.builder'; import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; +import { + erc20TokenBuilder, + erc721TokenBuilder, + tokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; import type { AddressInfoHelper } from '@/routes/common/address-info/address-info.helper'; import { AddressInfo } from '@/routes/common/entities/address-info.entity'; import { @@ -54,7 +58,7 @@ describe('Transfer Info mapper (Unit)', () => { const transfer = erc20TransferBuilder().build(); const safe = safeBuilder().build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc20TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .build(); addressInfoHelper.getOrDefault.mockResolvedValue(addressInfo); @@ -90,7 +94,7 @@ describe('Transfer Info mapper (Unit)', () => { const transfer = erc721TransferBuilder().build(); const safe = safeBuilder().build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc721TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .build(); addressInfoHelper.getOrDefault.mockResolvedValue(addressInfo); diff --git a/src/routes/transactions/mappers/transfers/transfer-info.mapper.ts b/src/routes/transactions/mappers/transfers/transfer-info.mapper.ts index c8178d8a64..47665f98f3 100644 --- a/src/routes/transactions/mappers/transfers/transfer-info.mapper.ts +++ b/src/routes/transactions/mappers/transfers/transfer-info.mapper.ts @@ -103,31 +103,36 @@ export class TransferInfoMapper { ): Promise { if (domainTransfer.type === 'ERC20_TRANSFER') { const { tokenAddress, value } = domainTransfer; - const token: Token | null = await this.getToken( - chainId, - tokenAddress, - ).catch(() => null); + const token = await this.getToken(chainId, tokenAddress).catch( + () => null, + ); + if (token?.type !== 'ERC20') { + throw Error('Token is not ERC-20'); + } return new Erc20Transfer( tokenAddress, value, - token?.name, - token?.symbol, - token?.logoUri, - token?.decimals, - token?.trusted, + token.name, + token.symbol, + token.logoUri, + token.decimals, + token.trusted, ); } else if (domainTransfer.type === 'ERC721_TRANSFER') { const { tokenAddress, tokenId } = domainTransfer; const token = await this.getToken(chainId, tokenAddress).catch( () => null, ); + if (token?.type !== 'ERC721') { + throw Error('Token is not ERC-721'); + } return new Erc721Transfer( tokenAddress, tokenId, - token?.name, - token?.symbol, - token?.logoUri, - token?.trusted, + token.name, + token.symbol, + token.logoUri, + token.trusted, ); } else if (domainTransfer.type === 'ETHER_TRANSFER') { return new NativeCoinTransfer(domainTransfer.value); diff --git a/src/routes/transactions/mappers/transfers/transfer.mapper.spec.ts b/src/routes/transactions/mappers/transfers/transfer.mapper.spec.ts index 431253ac0a..b4a390f493 100644 --- a/src/routes/transactions/mappers/transfers/transfer.mapper.spec.ts +++ b/src/routes/transactions/mappers/transfers/transfer.mapper.spec.ts @@ -7,8 +7,10 @@ import { OrderKind, OrderStatus, } from '@/domain/swaps/entities/order.entity'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import { + erc20TokenBuilder, + erc721TokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; import type { TokenRepository } from '@/domain/tokens/token.repository'; import type { ILoggingService } from '@/logging/logging.interface'; import type { AddressInfoHelper } from '@/routes/common/address-info/address-info.helper'; @@ -108,7 +110,7 @@ describe('Transfer mapper (Unit)', () => { .with('from', safe.address) .build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc721TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .build(); swapTransferInfoMapper.mapSwapTransferInfo.mockRejectedValue( @@ -151,7 +153,7 @@ describe('Transfer mapper (Unit)', () => { .with('from', safe.address) .build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc20TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .with('trusted', true) .build(); @@ -192,7 +194,7 @@ describe('Transfer mapper (Unit)', () => { .with('from', safe.address) .build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc20TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .with('trusted', true) .build(); @@ -219,7 +221,7 @@ describe('Transfer mapper (Unit)', () => { .with('from', safe.address) .build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc20TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .with('trusted', true) .build(); @@ -270,7 +272,7 @@ describe('Transfer mapper (Unit)', () => { .with('from', safe.address) .build(); const addressInfo = new AddressInfo(faker.finance.ethereumAddress()); - const token = tokenBuilder() + const token = erc20TokenBuilder() .with('address', getAddress(transfer.tokenAddress)) .with('trusted', trusted) .build(); @@ -334,7 +336,7 @@ describe('Transfer mapper (Unit)', () => { transfer.tokenInfo.decimals, transfer.tokenInfo.trusted, ); - const sellToken = tokenBuilder().build() as TokenInfo & { + const sellToken = erc20TokenBuilder().build() as TokenInfo & { decimals: number; }; swapTransferInfoMapper.mapSwapTransferInfo.mockResolvedValue({ @@ -388,7 +390,7 @@ describe('Transfer mapper (Unit)', () => { addressInfoHelper.getOrDefault.mockResolvedValue(addressInfo); tokenRepository.getToken.mockResolvedValue({ ...transfer.tokenInfo, - type: TokenType.Erc20, + type: 'ERC20', }); const actual = await mapper.mapTransfers({ @@ -459,7 +461,7 @@ describe('Transfer mapper (Unit)', () => { transfer.tokenInfo.decimals, transfer.tokenInfo.trusted, ); - const sellToken = tokenBuilder().build() as TokenInfo & { + const sellToken = erc20TokenBuilder().build() as TokenInfo & { decimals: number; }; swapTransferInfoMapper.mapSwapTransferInfo.mockResolvedValue({ @@ -513,7 +515,7 @@ describe('Transfer mapper (Unit)', () => { addressInfoHelper.getOrDefault.mockResolvedValue(addressInfo); tokenRepository.getToken.mockResolvedValue({ ...transfer.tokenInfo, - type: TokenType.Erc20, + type: 'ERC20', }); const actual = await mapper.mapTransfers({ @@ -582,7 +584,7 @@ describe('Transfer mapper (Unit)', () => { transfer.tokenInfo.decimals, transfer.tokenInfo.trusted, ); - const sellToken = tokenBuilder().build() as TokenInfo & { + const sellToken = erc20TokenBuilder().build() as TokenInfo & { decimals: number; }; swapTransferInfoMapper.mapSwapTransferInfo.mockResolvedValue({ @@ -636,7 +638,7 @@ describe('Transfer mapper (Unit)', () => { addressInfoHelper.getOrDefault.mockResolvedValue(addressInfo); tokenRepository.getToken.mockResolvedValue({ ...transfer.tokenInfo, - type: TokenType.Erc20, + type: 'ERC20', }); const actual = await mapper.mapTransfers({ @@ -661,14 +663,14 @@ describe('Transfer mapper (Unit)', () => { const erc721Transfer = erc721TransferBuilder() .with('from', safe.address) .build(); - const erc721Token = tokenBuilder() + const erc721Token = erc721TokenBuilder() .with('address', getAddress(erc721Transfer.tokenAddress)) .build(); const trustedErc20TransferWithValue = erc20TransferBuilder() .with('value', '1') .with('from', safe.address) .build(); - const trustedErc20Token = tokenBuilder() + const trustedErc20Token = erc20TokenBuilder() .with('address', getAddress(trustedErc20TransferWithValue.tokenAddress)) .with('trusted', true) .build(); @@ -680,7 +682,7 @@ describe('Transfer mapper (Unit)', () => { .with('value', '1') .with('from', safe.address) .build(); - const untrustedErc20Token = tokenBuilder() + const untrustedErc20Token = erc20TokenBuilder() .with('address', getAddress(trustedErc20TransferWithValue.tokenAddress)) .with('trusted', false) .build(); diff --git a/src/routes/transactions/transactions-history.controller.spec.ts b/src/routes/transactions/transactions-history.controller.spec.ts index b8b3126071..93dca705b6 100644 --- a/src/routes/transactions/transactions-history.controller.spec.ts +++ b/src/routes/transactions/transactions-history.controller.spec.ts @@ -36,8 +36,10 @@ import { toJson as nativeTokenTransferToJson, } from '@/domain/safe/entities/__tests__/native-token-transfer.builder'; import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import { + erc20TokenBuilder, + erc721TokenBuilder, +} from '@/domain/tokens/__tests__/token.builder'; import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; import type { Transfer } from '@/domain/safe/entities/transfer.entity'; import type { INetworkService } from '@/datasources/network/network.service.interface'; @@ -602,8 +604,7 @@ describe('Transactions History Controller (Unit)', () => { nativeTokenTransferToJson(nativeTokenTransfer) as Transfer, ]) .build(); - const tokenResponse = tokenBuilder() - .with('type', TokenType.Erc20) + const tokenResponse = erc20TokenBuilder() .with('address', getAddress(multisigTransaction.to)) .build(); networkService.get.mockImplementation(({ url }) => { @@ -957,8 +958,8 @@ describe('Transactions History Controller (Unit)', () => { it('Untrusted token transfers are ignored by default', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const untrustedToken = tokenBuilder().with('trusted', false).build(); - const trustedToken = tokenBuilder().with('trusted', true).build(); + const untrustedToken = erc20TokenBuilder().with('trusted', false).build(); + const trustedToken = erc20TokenBuilder().with('trusted', true).build(); // Use same date so that groups are created deterministically const date = faker.date.recent(); const transfers = [ @@ -1033,7 +1034,7 @@ describe('Transactions History Controller (Unit)', () => { it('Should return an empty array with no date labels if all the token transfers are untrusted', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const untrustedToken = tokenBuilder().with('trusted', false).build(); + const untrustedToken = erc20TokenBuilder().with('trusted', false).build(); // Use same date so that groups are created deterministically const date = faker.date.recent(); const oneDayAfter = new Date(date.getTime() + 1000 * 60 * 60 * 24); @@ -1117,8 +1118,8 @@ describe('Transactions History Controller (Unit)', () => { it('Should not return a date label if all the token transfers for that date are untrusted', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const untrustedToken = tokenBuilder().with('trusted', false).build(); - const trustedToken = tokenBuilder().with('trusted', true).build(); + const untrustedToken = erc20TokenBuilder().with('trusted', false).build(); + const trustedToken = erc20TokenBuilder().with('trusted', true).build(); // Use same date so that groups are created deterministically const date = faker.date.recent(); const oneDayAfter = new Date(date.getTime() + 1000 * 60 * 60 * 24); @@ -1247,8 +1248,8 @@ describe('Transactions History Controller (Unit)', () => { it('Untrusted transfers are returned when trusted=false', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const untrustedToken = tokenBuilder().with('trusted', false).build(); - const trustedToken = tokenBuilder().with('trusted', true).build(); + const untrustedToken = erc20TokenBuilder().with('trusted', false).build(); + const trustedToken = erc20TokenBuilder().with('trusted', true).build(); // Use same date so that groups are created deterministically const date = faker.date.recent(); const transfers = [ @@ -1332,7 +1333,7 @@ describe('Transactions History Controller (Unit)', () => { it('Nested transfers with a value of zero are not returned', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const trustedToken = tokenBuilder().with('trusted', true).build(); + const trustedToken = erc20TokenBuilder().with('trusted', true).build(); // Use same date so that groups are created deterministically const date = faker.date.recent(); const transfers = [ @@ -1406,14 +1407,10 @@ describe('Transactions History Controller (Unit)', () => { it('ERC721 transfers marked as non-trusted are returned', async () => { const safe = safeBuilder().build(); const chain = chainBuilder().build(); - const notTrustedErc721 = tokenBuilder() + const notTrustedErc721 = erc721TokenBuilder() .with('trusted', false) - .with('type', TokenType.Erc721) - .build(); - const trustedErc721 = tokenBuilder() - .with('trusted', true) - .with('type', TokenType.Erc721) .build(); + const trustedErc721 = erc721TokenBuilder().with('trusted', true).build(); // Use the same date so that groups are created deterministically const date = faker.date.recent(); const transfers = [ diff --git a/src/routes/transactions/transactions-history.imitation-transactions.controller.spec.ts b/src/routes/transactions/transactions-history.imitation-transactions.controller.spec.ts index bd8b249458..4bb3604b51 100644 --- a/src/routes/transactions/transactions-history.imitation-transactions.controller.spec.ts +++ b/src/routes/transactions/transactions-history.imitation-transactions.controller.spec.ts @@ -24,8 +24,7 @@ import { toJson as multisigTransactionToJson, } from '@/domain/safe/entities/__tests__/multisig-transaction.builder'; import { safeBuilder } from '@/domain/safe/entities/__tests__/safe.builder'; -import { tokenBuilder } from '@/domain/tokens/__tests__/token.builder'; -import { TokenType } from '@/domain/tokens/entities/token.entity'; +import { erc20TokenBuilder } from '@/domain/tokens/__tests__/token.builder'; import { TestLoggingModule } from '@/logging/__tests__/test.logging.module'; import type { ERC20Transfer, @@ -136,7 +135,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = } const multisigExecutionDate = new Date('2024-03-20T09:41:25Z'); - const multisigToken = tokenBuilder().with('type', TokenType.Erc20).build(); + const multisigToken = erc20TokenBuilder().build(); // Use value higher than BigInt(2) as we use tolerance +/- BigInt(1) to signify outside tolerance // later in tests, and values of 0 are not mapped const testValueBuffer = valueTolerance + faker.number.bigInt({ min: 2 }); @@ -145,7 +144,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = min: testValueBuffer, max: testValueBuffer + valueTolerance, }), - multisigToken.decimals!, + multisigToken.decimals, ); const multisigTransfer = { ...erc20TransferBuilder() @@ -203,9 +202,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = transfers: [erc20TransferToJson(multisigTransfer) as Transfer], } as MultisigTransaction; - const notImitatedMultisigToken = tokenBuilder() - .with('type', TokenType.Erc20) - .build(); + const notImitatedMultisigToken = erc20TokenBuilder().build(); const notImitatedMultisigTransfer = { ...erc20TransferBuilder() .with('executionDate', multisigExecutionDate) @@ -264,8 +261,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = const imitationAddress = getImitationAddress(multisigTransfer.to); const imitationExecutionDate = new Date('2024-03-20T09:42:58Z'); - const imitationToken = tokenBuilder() - .with('type', TokenType.Erc20) + const imitationToken = erc20TokenBuilder() .with('decimals', multisigToken.decimals) .build(); @@ -1333,7 +1329,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = describe('Intolerant value', () => { const intolerantDiff = parseUnits( valueTolerance * BigInt(2), - multisigToken.decimals!, + multisigToken.decimals, ); const valueIntolerantIncomingTransaction = ((): EthereumTransaction => { const transaction = structuredClone(imitationIncomingTransaction); @@ -2480,7 +2476,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = }); it('should detect imitation tokens using differing decimals', async () => { - const differentDecimals = multisigToken.decimals! + 1; + const differentDecimals = multisigToken.decimals + 1; const differentValue = multisigTransfer.value + '0'; const imitationWithDifferentDecimalsAddress = getImitationAddress( multisigTransfer.to, @@ -2488,8 +2484,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = const imitationWithDifferentDecimalsExecutionDate = new Date( '2024-03-20T09:42:58Z', ); - const imitationWithDifferentDecimalsToken = tokenBuilder() - .with('type', TokenType.Erc20) + const imitationWithDifferentDecimalsToken = erc20TokenBuilder() .with('decimals', differentDecimals) .build(); @@ -2665,7 +2660,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = describe('Echo transfers', () => { const multisigExecutionDate = new Date('2024-03-20T09:42:58Z'); - const multisigToken = tokenBuilder().with('type', TokenType.Erc20).build(); + const multisigToken = erc20TokenBuilder().build(); const multisigTransfer = { ...erc20TransferBuilder() .with('executionDate', multisigExecutionDate) @@ -2676,7 +2671,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = parseUnits( // Value vastly above echo limit for testing flagging (echoLimit * faker.number.bigInt({ min: 3, max: 9 })).toString(), - multisigToken.decimals!, + multisigToken.decimals, ).toString(), ) .build(), @@ -2729,8 +2724,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = transfers: [erc20TransferToJson(multisigTransfer) as Transfer], } as MultisigTransaction; - const notImitatedMultisigToken = tokenBuilder() - .with('type', TokenType.Erc20) + const notImitatedMultisigToken = erc20TokenBuilder() .with('decimals', multisigToken.decimals) .build(); const notImitatedMultisigTransfer = { @@ -2806,7 +2800,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = 'value', parseUnits( faker.number.bigInt({ min: 1, max: echoLimit }).toString(), - multisigToken.decimals!, + multisigToken.decimals, ).toString(), ) .with('executionDate', imitationExecutionDate) @@ -3587,7 +3581,7 @@ describe('Transactions History Controller (Unit) - Imitation Transactions', () = 'value', parseUnits( faker.number.bigInt({ min: echoLimit }).toString(), - multisigToken.decimals!, + multisigToken.decimals, ).toString(), ) .with('executionDate', aboveLimitExecutionDate) From 7a733b24366fe167feb2960157b6c6d56d3af7ac Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Wed, 29 Jan 2025 09:54:48 +0100 Subject: [PATCH 2/3] Update default decimals --- src/domain/tokens/entities/token.entity.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/domain/tokens/entities/token.entity.ts b/src/domain/tokens/entities/token.entity.ts index 9ed99ef662..e63d99742b 100644 --- a/src/domain/tokens/entities/token.entity.ts +++ b/src/domain/tokens/entities/token.entity.ts @@ -2,7 +2,15 @@ import { z } from 'zod'; import { buildPageSchema } from '@/domain/entities/schemas/page.schema.factory'; import { AddressSchema } from '@/validation/entities/schemas/address.schema'; -const DEFAULT_ERC20_DECIMALS = 18; +/** + * ERC-20 decimals are optional + * @see https://eips.ethereum.org/EIPS/eip-20#decimals + */ +const DEFAULT_ERC20_DECIMALS = 0; +/** + * ERC-721 decimals should return uint8(0) or be optional + * @see https://eips.ethereum.org/EIPS/eip-721#backwards-compatibility + */ const DEFAULT_ERC721_DECIMALS = 0; const BaseTokenSchema = z.object({ From 5e56eb627c1ed1be83a0b2ea168a3f518d770687 Mon Sep 17 00:00:00 2001 From: Aaron Cook Date: Wed, 29 Jan 2025 17:23:34 +0100 Subject: [PATCH 3/3] Fix test --- src/domain/tokens/entities/__tests__/token.entity.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/tokens/entities/__tests__/token.entity.spec.ts b/src/domain/tokens/entities/__tests__/token.entity.spec.ts index 3d9d53d99c..bd6712b7f0 100644 --- a/src/domain/tokens/entities/__tests__/token.entity.spec.ts +++ b/src/domain/tokens/entities/__tests__/token.entity.spec.ts @@ -70,14 +70,14 @@ describe('Token', () => { ]); }); - it('should default ERC-20 decimals to 18', () => { + it('should default ERC-20 decimals to 0', () => { const token = erc20TokenBuilder().build(); // @ts-expect-error - inferred type does not allow undefined decimals delete token.decimals; const result = TokenSchema.safeParse(token); - expect(result.success && result.data.decimals).toBe(18); + expect(result.success && result.data.decimals).toBe(0); }); it('should default ERC-721 decimals to 0', () => {