Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { TransactionHelper } from 'src/subdomains/supporting/payment/services/tr
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
import { BuyCryptoWebhookService } from '../../../process/services/buy-crypto-webhook.service';
import { BuyCryptoService } from '../../../process/services/buy-crypto.service';
import { Swap } from '../swap.entity';
import { SwapRepository } from '../swap.repository';
import { SwapService } from '../swap.service';

Expand Down Expand Up @@ -89,6 +90,24 @@ describe('SwapService', () => {
expect(service).toBeDefined();
});

it('should match the deposit address case-insensitively in getSwapByDepositAddressIgnoreCase', async () => {
const expected = Object.assign(new Swap(), { id: 1 });
const queryBuilder = {
select: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(expected),
};
jest.spyOn(swapRepo, 'createQueryBuilder').mockReturnValue(queryBuilder as any);

const result = await service.getSwapByDepositAddressIgnoreCase('0xabcdef');

expect(queryBuilder.where).toHaveBeenCalledWith('LOWER(deposit.address) = LOWER(:address)', {
address: '0xabcdef',
});
expect(result).toBe(expected);
});

describe('createDepositTx', () => {
const mockRequest = {
id: 1,
Expand Down
12 changes: 12 additions & 0 deletions src/subdomains/core/buy-crypto/routes/swap/swap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,18 @@ export class SwapService {
return swap;
}

// case-insensitive deposit-address variant for the compliance search only: admins may paste a non-checksummed (lower-case) EVM address, while addresses are stored EIP-55 checksummed
async getSwapByDepositAddressIgnoreCase(address: string): Promise<Swap> {
return this.swapRepo
.createQueryBuilder('swap')
.select('swap')
.leftJoinAndSelect('swap.deposit', 'deposit')
.leftJoinAndSelect('swap.user', 'user')
.leftJoinAndSelect('user.userData', 'userData')
.where('LOWER(deposit.address) = LOWER(:address)', { address })
.getOne();
}

async getSwapByKey(key: string, value: any, onlyDefaultRelation = false): Promise<Swap> {
const query = this.swapRepo
.createQueryBuilder('swap')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { PayInService } from 'src/subdomains/supporting/payin/services/payin.ser
import { TransactionHelper } from 'src/subdomains/supporting/payment/services/transaction-helper';
import { TransactionRequestService } from 'src/subdomains/supporting/payment/services/transaction-request.service';
import { BuyFiatService } from '../../process/services/buy-fiat.service';
import { Sell } from '../sell.entity';
import { SellRepository } from '../sell.repository';
import { SellService } from '../sell.service';

Expand Down Expand Up @@ -89,6 +90,24 @@ describe('SellService', () => {
expect(service).toBeDefined();
});

it('should match the deposit address case-insensitively in getSellByDepositAddressIgnoreCase', async () => {
const expected = Object.assign(new Sell(), { id: 1 });
const queryBuilder = {
select: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(expected),
};
jest.spyOn(sellRepo, 'createQueryBuilder').mockReturnValue(queryBuilder as any);

const result = await service.getSellByDepositAddressIgnoreCase('0xabcdef');

expect(queryBuilder.where).toHaveBeenCalledWith('LOWER(deposit.address) = LOWER(:address)', {
address: '0xabcdef',
});
expect(result).toBe(expected);
});

describe('createDepositTx', () => {
const mockRequest = {
id: 1,
Expand Down
12 changes: 12 additions & 0 deletions src/subdomains/core/sell-crypto/route/sell.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,18 @@ export class SellService {
return this.sellRepo.findOne(merge(defaultOptions, options));
}

// case-insensitive deposit-address variant for the compliance search only: admins may paste a non-checksummed (lower-case) EVM address, while addresses are stored EIP-55 checksummed
async getSellByDepositAddressIgnoreCase(address: string): Promise<Sell> {
return this.sellRepo
.createQueryBuilder('sell')
.select('sell')
.leftJoinAndSelect('sell.deposit', 'deposit')
.leftJoinAndSelect('sell.user', 'user')
.leftJoinAndSelect('user.userData', 'userData')
.where('LOWER(deposit.address) = LOWER(:address)', { address })
.getOne();
}

async getSellByKey(key: string, value: any, onlyDefaultRelation = false): Promise<Sell> {
const query = this.sellRepo
.createQueryBuilder('sell')
Expand Down
6 changes: 3 additions & 3 deletions src/subdomains/generic/support/support.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,12 +1074,12 @@ export class SupportService {
};

if (Config.formats.address.test(key)) {
const user = await this.userService.getUserByKey('address', key, true);
const user = await this.userService.getUserByAddressIgnoreCase(key);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSSQL used a case-insensitive collation, so address lookups worked regardless of case before the Postgres migration — this is a regression, not a new requirement. Instead of three dedicated IgnoreCase methods, consider using ILIKE :param in place of = :param in getUserByKey, getSellByKey, and getSwapByKey. That restores pre-Postgres behaviour across the board and avoids duplicating the query logic.

if (user) return { type: ComplianceSearchType.USER_ADDRESS, userData: user.userData };

return Promise.all([
this.sellService.getSellByKey('deposit.address', key, true),
this.swapService.getSwapByKey('deposit.address', key, true),
this.sellService.getSellByDepositAddressIgnoreCase(key),
this.swapService.getSwapByDepositAddressIgnoreCase(key),
]).then((s) => {
return { type: ComplianceSearchType.DEPOSIT_ADDRESS, userData: s.find((s) => s)?.userData };
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FeeService } from 'src/subdomains/supporting/payment/services/fee.servi
import { UserDataRepository } from '../../user-data/user-data.repository';
import { UserDataService } from '../../user-data/user-data.service';
import { WalletService } from '../../wallet/wallet.service';
import { User } from '../user.entity';
import { UserRepository } from '../user.repository';
import { UserService } from '../user.service';

Expand Down Expand Up @@ -82,4 +83,20 @@ describe('UserService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});

it('should match the address case-insensitively in getUserByAddressIgnoreCase', async () => {
const expected = Object.assign(new User(), { id: 1, address: '0xAbCdEf' });
const queryBuilder = {
select: jest.fn().mockReturnThis(),
leftJoinAndSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(expected),
};
jest.spyOn(userRepo, 'createQueryBuilder').mockReturnValue(queryBuilder as any);

const result = await service.getUserByAddressIgnoreCase('0xabcdef');

expect(queryBuilder.where).toHaveBeenCalledWith('LOWER(user.address) = LOWER(:address)', { address: '0xabcdef' });
expect(result).toBe(expected);
});
});
10 changes: 10 additions & 0 deletions src/subdomains/generic/user/models/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ export class UserService {
return this.userRepo.findOne({ where: { address }, relations });
}

// case-insensitive variant for the compliance search only: admins may paste a non-checksummed (lower-case) EVM address, while addresses are stored EIP-55 checksummed
async getUserByAddressIgnoreCase(address: string): Promise<User> {
return this.userRepo
.createQueryBuilder('user')
.select('user')
.leftJoinAndSelect('user.userData', 'userData')
.where('LOWER(user.address) = LOWER(:address)', { address })
.getOne();
}

async getUserByKey(key: string, value: any, onlyDefaultRelation = false): Promise<User> {
const query = this.userRepo
.createQueryBuilder('user')
Expand Down