Skip to content

Commit

Permalink
[OTE-141] implement post /compliance/geoblock (#1129)
Browse files Browse the repository at this point in the history
  • Loading branch information
dydxwill committed Mar 5, 2024
1 parent f6b594c commit e226a00
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import {
ComplianceReason,
ComplianceStatus,
ComplianceStatusFromDatabase,
ComplianceStatusTable,
dbHelpers,
testMocks,
testConstants,
ComplianceReason,
ComplianceStatusTable,
ComplianceStatusFromDatabase,
testMocks,
} from '@dydxprotocol-indexer/postgres';
import { getIpAddr } from '../../../../src/lib/utils';
import { sendRequest } from '../../../helpers/helpers';
import { RequestMethod } from '../../../../src/types';
import { stats } from '@dydxprotocol-indexer/base';
import { logger, stats } from '@dydxprotocol-indexer/base';
import { redis } from '@dydxprotocol-indexer/redis';
import { ratelimitRedis } from '../../../../src/caches/rate-limiters';
import { ComplianceControllerHelper } from '../../../../src/controllers/api/v4/compliance-controller';
import config from '../../../../src/config';
import { DateTime } from 'luxon';
import { ComplianceAction } from '../../../../src/controllers/api/v4/compliance-v2-controller';
import { ExtendedSecp256k1Signature, Secp256k1, sha256 } from '@cosmjs/crypto';
import { getGeoComplianceReason } from '../../../../src/helpers/compliance/compliance-utils';
import { isRestrictedCountryHeaders } from '@dydxprotocol-indexer/compliance';

jest.mock('@dydxprotocol-indexer/compliance');
jest.mock('../../../../src/helpers/compliance/compliance-utils');

jest.mock('../../../../src/lib/utils', () => ({
...jest.requireActual('../../../../src/lib/utils'),
Expand Down Expand Up @@ -231,7 +236,21 @@ describe('ComplianceV2Controller', () => {
});

describe('POST /geoblock', () => {
let getGeoComplianceReasonSpy: jest.SpyInstance;
let isRestrictedCountryHeadersSpy: jest.SpyInstance;

const body: any = {
address: testConstants.defaultAddress,
message: 'Test message',
action: ComplianceAction.ONBOARD,
signedMessage: sha256(Buffer.from('msg')),
pubkey: new Uint8Array([/* public key bytes */]),
timestamp: 1620000000,
};

beforeEach(async () => {
getGeoComplianceReasonSpy = getGeoComplianceReason as unknown as jest.Mock;
isRestrictedCountryHeadersSpy = isRestrictedCountryHeaders as unknown as jest.Mock;
ipAddrMock.mockReturnValue(ipAddr);
await testMocks.seedData();
jest.mock('@cosmjs/crypto', () => ({
Expand All @@ -257,12 +276,8 @@ describe('ComplianceV2Controller', () => {
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
...body,
address: '0x123', // Non-dYdX address
message: 'Test message',
action: ComplianceAction.ONBOARD,
signedMessage: sha256(Buffer.from('msg')),
pubkey: new Uint8Array([/* public key bytes */]),
timestamp: 1620000000,
},
expectedStatus: 400,
});
Expand All @@ -273,11 +288,7 @@ describe('ComplianceV2Controller', () => {
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
address: testConstants.defaultAddress,
message: 'Test message',
action: ComplianceAction.ONBOARD,
signedMessage: sha256(Buffer.from('msg')),
pubkey: new Uint8Array([/* public key bytes */]),
...body,
timestamp: 1619996600, // More than 30 seconds difference
},
expectedStatus: 400,
Expand All @@ -291,36 +302,220 @@ describe('ComplianceV2Controller', () => {
await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
address: testConstants.defaultAddress,
message: 'Test message',
action: ComplianceAction.ONBOARD,
signedMessage: sha256(Buffer.from('msg')),
pubkey: new Uint8Array([/* public key bytes */]),
timestamp: 1620000000,
},
body,
expectedStatus: 400,
});
});

it('should process valid request', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
});

expect(response.status).toEqual(200);
expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
});

it('should set status to BLOCKED for ONBOARD action from a restricted country with no existing compliance status', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.BLOCKED,
reason: ComplianceReason.US_GEO,
}));

expect(response.body.status).toEqual(ComplianceStatus.BLOCKED);
expect(response.body.reason).toEqual(ComplianceReason.US_GEO);
});

it('should set status to FIRST_STRIKE for CONNECT action from a restricted country with no existing compliance status', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
address: testConstants.defaultAddress,
message: 'Test message',
action: ComplianceAction.ONBOARD,
signedMessage: sha256(Buffer.from('msg')),
pubkey: new Uint8Array([/* public key bytes */]),
timestamp: 1620000000, // Valid timestamp
...body,
action: ComplianceAction.CONNECT,
},
expectedStatus: 200,
});

expect(response.status).toEqual(200);
const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.FIRST_STRIKE,
reason: ComplianceReason.US_GEO,
}));

expect(response.body.status).toEqual(ComplianceStatus.FIRST_STRIKE);
expect(response.body.reason).toEqual(ComplianceReason.US_GEO);
});

it('should set status to COMPLIANT for any action from a non-restricted country with no existing compliance status', async () => {
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
isRestrictedCountryHeadersSpy.mockReturnValue(false);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.COMPLIANT,
}));

expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
});

it('should update status to FIRST_STRIKE for CONNECT action from a restricted country with existing COMPLIANT status', async () => {
await ComplianceStatusTable.create({
address: testConstants.defaultAddress,
status: ComplianceStatus.COMPLIANT,
});
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
...body,
action: ComplianceAction.CONNECT,
},
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.FIRST_STRIKE,
reason: ComplianceReason.US_GEO,
}));

expect(response.body.status).toEqual(ComplianceStatus.FIRST_STRIKE);
expect(response.body.reason).toEqual(ComplianceReason.US_GEO);
});

it('should be a no-op for ONBOARD action with existing COMPLIANT status', async () => {
const loggerError = jest.spyOn(logger, 'error');
await ComplianceStatusTable.create({
address: testConstants.defaultAddress,
status: ComplianceStatus.COMPLIANT,
});
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.COMPLIANT,
}));

expect(loggerError).toHaveBeenCalledWith(expect.objectContaining({
at: 'ComplianceV2Controller POST /geoblock',
message: 'Invalid action for current compliance status',
}));
expect(response.body.status).toEqual(ComplianceStatus.COMPLIANT);
});

it('should be a no-op for ONBOARD action with existing FIRST_STRIKE status', async () => {
const loggerError = jest.spyOn(logger, 'error');
await ComplianceStatusTable.create({
address: testConstants.defaultAddress,
status: ComplianceStatus.FIRST_STRIKE,
reason: ComplianceReason.US_GEO,
});
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body,
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.FIRST_STRIKE,
reason: ComplianceReason.US_GEO,
}));

expect(loggerError).toHaveBeenCalledWith(expect.objectContaining({
at: 'ComplianceV2Controller POST /geoblock',
message: 'Invalid action for current compliance status',
}));
expect(response.body.status).toEqual(ComplianceStatus.FIRST_STRIKE);
expect(response.body.reason).toEqual(ComplianceReason.US_GEO);
});

it('should update status to CLOSE_ONLY for CONNECT action from a restricted country with existing FIRST_STRIKE status', async () => {
await ComplianceStatusTable.create({
address: testConstants.defaultAddress,
status: ComplianceStatus.FIRST_STRIKE,
reason: ComplianceReason.US_GEO,
});
(Secp256k1.verifySignature as jest.Mock).mockResolvedValueOnce(true);
getGeoComplianceReasonSpy.mockReturnValueOnce(ComplianceReason.US_GEO);
isRestrictedCountryHeadersSpy.mockReturnValue(true);

const response: any = await sendRequest({
type: RequestMethod.POST,
path: '/v4/compliance/geoblock',
body: {
...body,
action: ComplianceAction.CONNECT,
},
expectedStatus: 200,
});

const data: ComplianceStatusFromDatabase[] = await ComplianceStatusTable.findAll({}, [], {});
expect(data).toHaveLength(1);
expect(data[0]).toEqual(expect.objectContaining({
address: testConstants.defaultAddress,
status: ComplianceStatus.CLOSE_ONLY,
reason: ComplianceReason.US_GEO,
}));

expect(response.body.status).toEqual(ComplianceStatus.CLOSE_ONLY);
expect(response.body.reason).toEqual(ComplianceReason.US_GEO);
});
});
});
Loading

0 comments on commit e226a00

Please sign in to comment.