Skip to content

Commit

Permalink
feat: 🎸 Add support for off-chain settlement legs
Browse files Browse the repository at this point in the history
* Modifies endpoints
  1. `POST venues/create` to support optional property `signers` in the request body
  (`CreateVenueDto`) to specify list of addresses allowed to sign off chain receipts
  for instructions of the venue
  2. `POST venues/:id/instructions/create`
    a. to support new optional attribute `endAfterBlock` in request body
    (`CreateInstructionDto`)to allow creation of instruction that can be executed
    manually after this given block.
    b. to add support for off chain legs by adding new optional type
    `OffChainLegDto` to type of legs
  3. `POST instructions/:id/affirm` to support optional attributes `portfolios`
  and `receipts` in the request body (`AffirmInstructionDto`) to specify specific
  portfolios to affirm or the details of the off chain leg receipt to be used to
  affirm off chain legs

* Adds new endpoints
  1. `POST venues/:id/add-signers` and `POST venues/:id/remove-signers` to
  add/remove allowed signers from a Venue
  2. `GET accounts/:id/receipts` to get all off chain receipts redeemed by an
  account
  3. `GET instructions/:id/offchain-affirmations` to fetch all off chain
  affirmations status for all off chain legs in an instruction
  4. `GET instructions/:id/offchain-affirmations/:legId` to fetch off chain
  affirmation status for a specific leg in an instruction
  5. `POST instructions/:id/execute-manually` to execute an instruction manually
  • Loading branch information
prashantasdeveloper committed Jul 5, 2024
1 parent b97e686 commit 7aa196c
Show file tree
Hide file tree
Showing 37 changed files with 1,446 additions and 386 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"Isin",
"metatype",
"nand",
"offChain",
"Permissioned",
"polkadot",
"Polymesh",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"@polymeshassociation/fireblocks-signing-manager": "^2.5.0",
"@polymeshassociation/hashicorp-vault-signing-manager": "^3.4.0",
"@polymeshassociation/local-signing-manager": "^3.3.0",
"@polymeshassociation/polymesh-sdk": "24.5.0",
"@polymeshassociation/polymesh-sdk": "24.7.0-alpha.2",
"@polymeshassociation/signing-manager-types": "^3.2.0",
"class-transformer": "0.5.1",
"class-validator": "^0.14.0",
Expand Down
11 changes: 11 additions & 0 deletions src/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,15 @@ describe('AccountsController', () => {
expect(result).toEqual({ identity: fakeIdentityModel });
});
});

describe('getOffChainReceipts', () => {
it('should call the service and return AccountDetailsModel', async () => {
const mockResponse = [new BigNumber(1), new BigNumber(2)];
mockAccountsService.fetchOffChainReceipts.mockReturnValue(mockResponse);

const result = await controller.getOffChainReceipts({ account: '5xdd' });

expect(result).toEqual({ results: ['1', '2'] });
});
});
});
26 changes: 26 additions & 0 deletions src/accounts/accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/sw
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { ExtrinsicModel } from '~/common/models/extrinsic.model';
import { PaginatedResultsModel } from '~/common/models/paginated-results.model';
import { ResultsModel } from '~/common/models/results.model';
import { TransactionQueueModel } from '~/common/models/transaction-queue.model';
import { handleServiceResult, TransactionResponseModel } from '~/common/utils';
import { createIdentityModel, createSignerModel } from '~/identities/identities.util';
Expand Down Expand Up @@ -343,4 +344,29 @@ export class AccountsController {

return new AccountDetailsModel({ identity: identityModel, multiSig: multiSigDetailsModel });
}

@ApiOperation({
summary: 'Get all redeemed off chain receipt UIDs',
description: 'This endpoint will provide list of all off chain receipt UIDs used by an Account',
})
@ApiParam({
name: 'account',
description: 'The Account address for which the receipts are to be fetched',
type: 'string',
example: '5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV',
})
@ApiArrayResponse('string', {
description: 'List of receipt UIDs used by the Account',
paginated: false,
example: ['10001', '10002'],
})
@Get('/:id/receipts')
public async getOffChainReceipts(
@Param() { account }: AccountParamsDto
): Promise<ResultsModel<string>> {
const results = await this.accountsService.fetchOffChainReceipts(account);
return new ResultsModel({
results: results.map(result => result.toString()),
});
}
}
15 changes: 15 additions & 0 deletions src/accounts/accounts.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,4 +382,19 @@ describe('AccountsService', () => {
expect(result).toStrictEqual(fakeResult);
});
});

describe('fetchOffChainReceipts', () => {
it('should return the off chain Receipts redeemed by an Account', async () => {
const mockAccount = new MockAccount();

const findOneSpy = jest.spyOn(service, 'findOne');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
findOneSpy.mockResolvedValue(mockAccount as any);
const mockResult = [new BigNumber(1)];
mockAccount.getOffChainReceipts.mockResolvedValue(mockResult);

const result = await service.fetchOffChainReceipts('address');
expect(result).toEqual(mockResult);
});
});
});
6 changes: 6 additions & 0 deletions src/accounts/accounts.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import {
Account,
AccountBalance,
Expand Down Expand Up @@ -142,4 +143,9 @@ export class AccountsService {
multiSigDetails,
};
}

public async fetchOffChainReceipts(address: string): Promise<BigNumber[]> {
const account = await this.findOne(address);
return account.getOffChainReceipts();
}
}
2 changes: 1 addition & 1 deletion src/checkpoints/checkpoints.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ describe('CheckpointsController', () => {
expect(result).toEqual([new CheckpointDetailsModel({ id, totalSupply, createdAt })]);
});
});

describe('getComplexity', () => {
it('should return the transaction details', async () => {
const maxComplexity = new BigNumber(10);
Expand Down
5 changes: 5 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@ export enum ProcessMode {

AMQP = 'AMQP',
}

export enum LegType {
offChain = 'offChain',
onChain = 'onChain',
}
1 change: 1 addition & 0 deletions src/portfolios/dto/portfolio-movement.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class PortfolioMovementDto {

@ApiPropertyOptional({
description: 'NFT IDs to move from a collection',
type: 'string',
example: ['1'],
isArray: true,
})
Expand Down
32 changes: 32 additions & 0 deletions src/settlements/dto/affirm-instruction.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* istanbul ignore file */

import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsOptional, ValidateNested } from 'class-validator';

import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { PortfolioDto } from '~/portfolios/dto/portfolio.dto';
import { OffChainAffirmationReceiptDto } from '~/settlements/dto/offchain-affirmation-receipt.dto';

export class AffirmInstructionDto extends TransactionBaseDto {
@ApiPropertyOptional({
description: 'List of portfolios that the signer controls and wants to affirm the instruction',
type: () => PortfolioDto,
isArray: true,
})
@IsOptional()
@ValidateNested({ each: true })
@Type(() => PortfolioDto)
readonly portfolios?: PortfolioDto[];

@ApiPropertyOptional({
description:
'List of off chain receipts required for affirming off chain legs(if any) in the instruction',
type: () => OffChainAffirmationReceiptDto,
isArray: true,
})
@IsOptional()
@ValidateNested({ each: true })
@Type(() => OffChainAffirmationReceiptDto)
readonly receipts?: OffChainAffirmationReceiptDto[];
}
28 changes: 28 additions & 0 deletions src/settlements/dto/asset-leg.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* istanbul ignore file */

import { ApiProperty } from '@nestjs/swagger';
import { IsEnum } from 'class-validator';

import { IsTicker } from '~/common/decorators/validation';
import { LegType } from '~/common/types';

export class AssetLegDto {
@ApiProperty({
description: 'Ticker of the Asset',
example: 'TICKER',
})
@IsTicker()
readonly asset: string;

@ApiProperty({
description: 'Indicator to know if the transfer is on chain or off chain',
enum: LegType,
type: 'string',
})
@IsEnum(LegType)
readonly type: LegType;

constructor(dto: AssetLegDto) {
Object.assign(this, dto);
}
}
47 changes: 42 additions & 5 deletions src/settlements/dto/create-instruction.dto.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
/* istanbul ignore file */
import { ApiPropertyOptional } from '@nestjs/swagger';

import { ApiExtraModels, ApiPropertyOptional } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { Type } from 'class-transformer';
import { IsByteLength, IsDate, IsOptional, IsString, ValidateNested } from 'class-validator';

import { ApiPropertyOneOf } from '~/common/decorators/swagger';
import { ToBigNumber } from '~/common/decorators/transformation';
import { IsBigNumber, IsDid } from '~/common/decorators/validation';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { LegType } from '~/common/types';
import { AssetLegDto } from '~/settlements/dto/asset-leg.dto';
import { LegDto } from '~/settlements/dto/leg.dto';
import { OffChainLegDto } from '~/settlements/dto/offchain-leg.dto';

@ApiExtraModels(LegDto, OffChainLegDto, AssetLegDto)
export class CreateInstructionDto extends TransactionBaseDto {
@ApiPropertyOneOf({
description: 'Array of legs which can be either LegDto or OffChainLegDto',
union: [LegDto, OffChainLegDto],
isArray: true,
})
@ValidateNested({ each: true })
@Type(() => LegDto)
readonly legs: LegDto[];
@Type(() => AssetLegDto, {
keepDiscriminatorProperty: true,
discriminator: {
property: 'type',
subTypes: [
{
value: OffChainLegDto,
name: LegType.offChain,
},
{
value: LegDto,
name: LegType.onChain,
},
],
},
})
readonly legs: (LegDto | OffChainLegDto)[];

@ApiPropertyOptional({
description: 'Date at which the trade was agreed upon (optional, for offchain trades)',
description: 'Date at which the trade was agreed upon (optional, for off chain trades)',
example: new Date('10/14/1987').toISOString(),
})
@IsOptional()
@IsDate()
readonly tradeDate?: Date;

@ApiPropertyOptional({
description: 'Date at which the trade was executed (optional, for offchain trades)',
description: 'Date at which the trade was executed (optional, for off chain trades)',
example: new Date('10/14/1987').toISOString(),
})
@IsOptional()
Expand All @@ -41,6 +67,17 @@ export class CreateInstructionDto extends TransactionBaseDto {
@ToBigNumber()
readonly endBlock?: BigNumber;

@ApiPropertyOptional({
type: 'string',
description:
'Block after which the Instruction can be manually executed. If not passed, the Instruction will be executed when all parties affirm or as soon as one party rejects',
example: '123',
})
@IsOptional()
@IsBigNumber()
@ToBigNumber()
readonly endAfterBlock?: BigNumber;

@ApiPropertyOptional({
description: 'Identifier string to help differentiate instructions. Maximum 32 bytes',
example: 'Transfer of GROWTH Asset',
Expand Down
10 changes: 10 additions & 0 deletions src/settlements/dto/create-venue.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ export class CreateVenueDto extends TransactionBaseDto {
})
@IsEnum(VenueType)
readonly type: VenueType;

@ApiProperty({
description:
'Optional list of signers to be allowed to sign off chain receipts for instructions in this Venue',
type: 'string',
isArray: true,
example: ['5GwwYnwCYcJ1Rkop35y7SDHAzbxrCkNUDD4YuCUJRPPXbvyV'],
})
@IsString({ each: true })
readonly signers?: string[];
}
17 changes: 17 additions & 0 deletions src/settlements/dto/execute-instruction.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* istanbul ignore file */

import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsOptional } from 'class-validator';

import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';

export class ExecuteInstructionDto extends TransactionBaseDto {
@ApiPropertyOptional({
description: 'Set to `true` to skip affirmation check, useful for batch transactions',
type: 'boolean',
example: false,
})
@IsOptional()
@IsBoolean()
readonly skipAffirmationCheck?: boolean;
}
13 changes: 13 additions & 0 deletions src/settlements/dto/leg-id-params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* istanbul ignore file */

import { BigNumber } from '@polymeshassociation/polymesh-sdk';

import { ToBigNumber } from '~/common/decorators/transformation';
import { IsBigNumber } from '~/common/decorators/validation';
import { IdParamsDto } from '~/common/dto/id-params.dto';

export class LegIdParamsDto extends IdParamsDto {
@IsBigNumber()
@ToBigNumber()
readonly legId: BigNumber;
}
41 changes: 32 additions & 9 deletions src/settlements/dto/leg.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { BigNumber } from '@polymeshassociation/polymesh-sdk';
import { InstructionFungibleLeg, InstructionNftLeg } from '@polymeshassociation/polymesh-sdk/types';
import { Type } from 'class-transformer';
import { ValidateIf, ValidateNested } from 'class-validator';
import { IsEnum, ValidateIf, ValidateNested } from 'class-validator';

import { ToBigNumber } from '~/common/decorators/transformation';
import { IsBigNumber, IsTicker } from '~/common/decorators/validation';
import { IsBigNumber } from '~/common/decorators/validation';
import { AppValidationError } from '~/common/errors';
import { LegType } from '~/common/types';
import { PortfolioDto } from '~/portfolios/dto/portfolio.dto';
import { AssetLegDto } from '~/settlements/dto/asset-leg.dto';

export class LegDto {
export class LegDto extends AssetLegDto {
@ApiPropertyOptional({
description: 'Amount of the fungible Asset to be transferred',
type: 'string',
Expand Down Expand Up @@ -54,10 +58,29 @@ export class LegDto {
@Type(() => PortfolioDto)
readonly to: PortfolioDto;

@ApiProperty({
description: 'Asset ticker',
example: 'TICKER',
})
@IsTicker()
readonly asset: string;
@ApiProperty({ enum: LegType, default: LegType.onChain })
@IsEnum(LegType)
readonly type = LegType.onChain;

public toLeg(): InstructionFungibleLeg | InstructionNftLeg {
const { amount, nfts, asset, from, to } = this;
if (amount) {
return {
from: from.toPortfolioLike(),
to: to.toPortfolioLike(),
asset,
amount,
};
}

if (nfts) {
return {
nfts,
asset,
from: from.toPortfolioLike(),
to: to.toPortfolioLike(),
};
}
throw new AppValidationError('Either nfts/amount should be specific for a on-chain leg');
}
}
Loading

0 comments on commit 7aa196c

Please sign in to comment.