Skip to content

Commit e66e21f

Browse files
committed
feat: 🎸 venue filtering
1 parent e951632 commit e66e21f

File tree

8 files changed

+414
-2
lines changed

8 files changed

+414
-2
lines changed

‎src/assets/assets.controller.spec.ts‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { AssetsService } from '~/assets/assets.service';
1919
import { AssetDocumentDto } from '~/assets/dto/asset-document.dto';
2020
import { SetTransferRestrictionsDto } from '~/assets/dto/transfer-restrictions/set-transfer-restrictions.dto';
2121
import { TransferRestrictionsValueModel } from '~/assets/models/transfer-restrictions-values.model';
22+
import { VenueFilteringDetailsModel } from '~/assets/models/venue-filtering-details.model';
2223
import { createAuthorizationRequestModel } from '~/authorizations/authorizations.util';
2324
import { PaginatedResultsModel } from '~/common/models/paginated-results.model';
2425
import { ProcessMode } from '~/common/types';
@@ -239,6 +240,70 @@ describe('AssetsController', () => {
239240
});
240241
});
241242

243+
describe('enableVenueFiltering', () => {
244+
it('should enable venue filtering for the asset', async () => {
245+
mockAssetsService.enableVenueFiltering.mockResolvedValue(txResult);
246+
const body = { signer };
247+
248+
const result = await controller.enableVenueFiltering({ asset: assetId }, body);
249+
250+
expect(result).toEqual(processedTxResult);
251+
expect(mockAssetsService.enableVenueFiltering).toHaveBeenCalledWith(assetId, body);
252+
});
253+
});
254+
255+
describe('disableVenueFiltering', () => {
256+
it('should disable venue filtering for the asset', async () => {
257+
mockAssetsService.disableVenueFiltering.mockResolvedValue(txResult);
258+
const body = { signer };
259+
260+
const result = await controller.disableVenueFiltering({ asset: assetId }, body);
261+
262+
expect(result).toEqual(processedTxResult);
263+
expect(mockAssetsService.disableVenueFiltering).toHaveBeenCalledWith(assetId, body);
264+
});
265+
});
266+
267+
describe('allowVenues', () => {
268+
it('should allow venues for the asset', async () => {
269+
mockAssetsService.allowVenues.mockResolvedValue(txResult);
270+
const body = { signer, venues: [new BigNumber(1)] };
271+
272+
const result = await controller.allowVenues({ asset: assetId }, body);
273+
274+
expect(result).toEqual(processedTxResult);
275+
expect(mockAssetsService.allowVenues).toHaveBeenCalledWith(assetId, body);
276+
});
277+
});
278+
279+
describe('disallowVenues', () => {
280+
it('should disallow venues for the asset', async () => {
281+
mockAssetsService.disallowVenues.mockResolvedValue(txResult);
282+
const body = { signer, venues: [new BigNumber(2)] };
283+
284+
const result = await controller.disallowVenues({ asset: assetId }, body);
285+
286+
expect(result).toEqual(processedTxResult);
287+
expect(mockAssetsService.disallowVenues).toHaveBeenCalledWith(assetId, body);
288+
});
289+
});
290+
291+
describe('getVenueFilteringDetails', () => {
292+
it('should return the venue filtering details', async () => {
293+
const details = {
294+
isEnabled: true,
295+
allowedVenues: [new BigNumber(1)],
296+
disallowedVenues: [],
297+
};
298+
mockAssetsService.getVenueFilteringDetails.mockResolvedValue(details);
299+
300+
const result = await controller.getVenueFilteringDetails({ asset: assetId });
301+
302+
expect(result).toEqual(new VenueFilteringDetailsModel(details));
303+
expect(mockAssetsService.getVenueFilteringDetails).toHaveBeenCalledWith(assetId);
304+
});
305+
});
306+
242307
describe('createAsset', () => {
243308
it('should call the service and return the results', async () => {
244309
const input = {

‎src/assets/assets.controller.ts‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@ import { RedeemTokensDto } from '~/assets/dto/redeem-tokens.dto';
2323
import { RequiredMediatorsDto } from '~/assets/dto/required-mediators.dto';
2424
import { SetAssetDocumentsDto } from '~/assets/dto/set-asset-documents.dto';
2525
import { SetTransferRestrictionsDto } from '~/assets/dto/transfer-restrictions/set-transfer-restrictions.dto';
26+
import { VenueIdsDto } from '~/assets/dto/venue-ids.dto';
2627
import { AgentOperationModel } from '~/assets/models/agent-operation.model';
2728
import { AssetDetailsModel } from '~/assets/models/asset-details.model';
2829
import { AssetDocumentModel } from '~/assets/models/asset-document.model';
2930
import { CreatedAssetModel } from '~/assets/models/created-asset.model';
3031
import { IdentityBalanceModel } from '~/assets/models/identity-balance.model';
3132
import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model';
3233
import { TransferRestrictionsValueModel } from '~/assets/models/transfer-restrictions-values.model';
34+
import { VenueFilteringDetailsModel } from '~/assets/models/venue-filtering-details.model';
3335
import { authorizationRequestResolver } from '~/authorizations/authorizations.util';
3436
import { CreatedAuthorizationRequestModel } from '~/authorizations/models/created-authorization-request.model';
3537
import {
@@ -233,6 +235,121 @@ export class AssetsController {
233235
return handleServiceResult(result);
234236
}
235237

238+
@ApiOperation({
239+
summary: 'Enable venue filtering for an Asset',
240+
description:
241+
'When enabled, only explicitly allowed venues can create instructions for the Asset',
242+
})
243+
@ApiParam({
244+
name: 'asset',
245+
description: 'The Asset (Ticker/Asset ID) whose venue filtering will be enabled',
246+
type: 'string',
247+
})
248+
@ApiTransactionResponse({
249+
description: 'Details of the transaction',
250+
type: TransactionQueueModel,
251+
})
252+
@Post(':asset/venue-filtering/enable')
253+
public async enableVenueFiltering(
254+
@Param() { asset }: AssetParamsDto,
255+
@Body() transactionBaseDto: TransactionBaseDto
256+
): Promise<TransactionResponseModel> {
257+
const result = await this.assetsService.enableVenueFiltering(asset, transactionBaseDto);
258+
259+
return handleServiceResult(result);
260+
}
261+
262+
@ApiOperation({
263+
summary: 'Disable venue filtering for an Asset',
264+
description: 'When disabled, any venue can create instructions for the Asset',
265+
})
266+
@ApiParam({
267+
name: 'asset',
268+
description: 'The Asset (Ticker/Asset ID) whose venue filtering will be disabled',
269+
type: 'string',
270+
})
271+
@ApiTransactionResponse({
272+
description: 'Details of the transaction',
273+
type: TransactionQueueModel,
274+
})
275+
@Post(':asset/venue-filtering/disable')
276+
public async disableVenueFiltering(
277+
@Param() { asset }: AssetParamsDto,
278+
@Body() transactionBaseDto: TransactionBaseDto
279+
): Promise<TransactionResponseModel> {
280+
const result = await this.assetsService.disableVenueFiltering(asset, transactionBaseDto);
281+
282+
return handleServiceResult(result);
283+
}
284+
285+
@ApiOperation({
286+
summary: 'Allow specific venues to settle instructions for an Asset',
287+
})
288+
@ApiParam({
289+
name: 'asset',
290+
description: 'The Asset (Ticker/Asset ID) whose venue allowlist will be updated',
291+
type: 'string',
292+
})
293+
@ApiTransactionResponse({
294+
description: 'Details of the transaction',
295+
type: TransactionQueueModel,
296+
})
297+
@Post(':asset/venue-filtering/allow')
298+
public async allowVenues(
299+
@Param() { asset }: AssetParamsDto,
300+
@Body() venueIdsDto: VenueIdsDto
301+
): Promise<TransactionResponseModel> {
302+
const result = await this.assetsService.allowVenues(asset, venueIdsDto);
303+
304+
return handleServiceResult(result);
305+
}
306+
307+
@ApiOperation({
308+
summary: 'Disallow specific venues from settling instructions for an Asset',
309+
})
310+
@ApiParam({
311+
name: 'asset',
312+
description: 'The Asset (Ticker/Asset ID) whose venue allowlist will be updated',
313+
type: 'string',
314+
})
315+
@ApiTransactionResponse({
316+
description: 'Details of the transaction',
317+
type: TransactionQueueModel,
318+
})
319+
@Post(':asset/venue-filtering/disallow')
320+
public async disallowVenues(
321+
@Param() { asset }: AssetParamsDto,
322+
@Body() venueIdsDto: VenueIdsDto
323+
): Promise<TransactionResponseModel> {
324+
const result = await this.assetsService.disallowVenues(asset, venueIdsDto);
325+
326+
return handleServiceResult(result);
327+
}
328+
329+
@ApiOperation({
330+
summary: 'Get venue filtering details for an Asset',
331+
description:
332+
'Returns the current filtering status alongside the allowed and disallowed venue identifiers',
333+
})
334+
@ApiParam({
335+
name: 'asset',
336+
description:
337+
'The Asset (Ticker/Asset ID) whose venue filtering configuration will be retrieved',
338+
type: 'string',
339+
})
340+
@ApiOkResponse({
341+
description: 'Venue filtering configuration for the Asset',
342+
type: VenueFilteringDetailsModel,
343+
})
344+
@Get(':asset/venue-filtering')
345+
public async getVenueFilteringDetails(
346+
@Param() { asset }: AssetParamsDto
347+
): Promise<VenueFilteringDetailsModel> {
348+
const details = await this.assetsService.getVenueFilteringDetails(asset);
349+
350+
return new VenueFilteringDetailsModel(details);
351+
}
352+
236353
@ApiOperation({
237354
summary: 'Issue more of an Asset',
238355
description: 'This endpoint issues more of a given Asset',

‎src/assets/assets.service.spec.ts‎

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
MockIdentity,
3030
MockPolymesh,
3131
MockTransaction,
32+
MockVenue,
3233
} from '~/test-utils/mocks';
3334
import {
3435
MockIdentitiesService,
@@ -37,7 +38,7 @@ import {
3738
} from '~/test-utils/service-mocks';
3839
import * as transactionsUtilModule from '~/transactions/transactions.util';
3940

40-
const { did, signer, assetId } = testValues;
41+
const { did, signer, assetId, ticker, txResult } = testValues;
4142

4243
jest.mock('@polymeshassociation/polymesh-sdk/utils', () => ({
4344
...jest.requireActual('@polymeshassociation/polymesh-sdk/utils'),
@@ -297,6 +298,107 @@ describe('AssetsService', () => {
297298
});
298299
});
299300

301+
describe('enableVenueFiltering', () => {
302+
it('should submit a transaction enabling venue filtering', async () => {
303+
const mockAsset = new MockAsset();
304+
mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset);
305+
mockTransactionsService.submit.mockResolvedValue(txResult);
306+
307+
const body = { signer };
308+
309+
const result = await service.enableVenueFiltering(ticker, body);
310+
311+
expect(result).toBe(txResult);
312+
expect(mockPolymeshApi.assets.getAsset).toHaveBeenCalledWith({ ticker });
313+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
314+
mockAsset.setVenueFiltering,
315+
{ enabled: true },
316+
expect.objectContaining({ signer })
317+
);
318+
});
319+
});
320+
321+
describe('disableVenueFiltering', () => {
322+
it('should submit a transaction disabling venue filtering', async () => {
323+
const mockAsset = new MockAsset();
324+
mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset);
325+
mockTransactionsService.submit.mockResolvedValue(txResult);
326+
327+
const body = { signer };
328+
329+
const result = await service.disableVenueFiltering(assetId, body);
330+
331+
expect(result).toBe(txResult);
332+
expect(mockPolymeshApi.assets.getAsset).toHaveBeenCalledWith({ assetId });
333+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
334+
mockAsset.setVenueFiltering,
335+
{ enabled: false },
336+
expect.objectContaining({ signer })
337+
);
338+
});
339+
});
340+
341+
describe('allowVenues', () => {
342+
it('should submit a transaction allowing venues', async () => {
343+
const mockAsset = new MockAsset();
344+
const venues = [new BigNumber(1)];
345+
mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset);
346+
mockTransactionsService.submit.mockResolvedValue(txResult);
347+
348+
const body = { signer, venues };
349+
350+
const result = await service.allowVenues(ticker, body);
351+
352+
expect(result).toBe(txResult);
353+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
354+
mockAsset.setVenueFiltering,
355+
{ allowedVenues: venues },
356+
expect.objectContaining({ signer })
357+
);
358+
});
359+
});
360+
361+
describe('disallowVenues', () => {
362+
it('should submit a transaction disallowing venues', async () => {
363+
const mockAsset = new MockAsset();
364+
const venues = [new BigNumber(2)];
365+
mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset);
366+
mockTransactionsService.submit.mockResolvedValue(txResult);
367+
368+
const body = { signer, venues };
369+
370+
const result = await service.disallowVenues(ticker, body);
371+
372+
expect(result).toBe(txResult);
373+
expect(mockTransactionsService.submit).toHaveBeenCalledWith(
374+
mockAsset.setVenueFiltering,
375+
{ disallowedVenues: venues },
376+
expect.objectContaining({ signer })
377+
);
378+
});
379+
});
380+
381+
describe('getVenueFilteringDetails', () => {
382+
it('should return the venue filtering details', async () => {
383+
const mockAsset = new MockAsset();
384+
const allowedVenues = [new MockVenue(), new MockVenue()];
385+
allowedVenues[0].id = new BigNumber(5);
386+
allowedVenues[1].id = new BigNumber(7);
387+
388+
mockAsset.getVenueFilteringDetails.mockResolvedValue({ isEnabled: true, allowedVenues });
389+
mockPolymeshApi.assets.getAsset.mockResolvedValue(mockAsset);
390+
391+
const result = await service.getVenueFilteringDetails(assetId);
392+
393+
expect(result).toEqual({
394+
isEnabled: true,
395+
allowedVenues: allowedVenues.map(({ id }) => id),
396+
disallowedVenues: [],
397+
});
398+
expect(mockAsset.getVenueFilteringDetails).toHaveBeenCalled();
399+
});
400+
});
401+
300402
describe('createAsset', () => {
301403
const createBody = {
302404
signer,
@@ -357,7 +459,6 @@ describe('AssetsService', () => {
357459
});
358460

359461
describe('transferOwnership', () => {
360-
const ticker = 'TICKER';
361462
const body = {
362463
signer,
363464
target: '0x1000',

0 commit comments

Comments
 (0)