Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 🎸 Add support for off-chain settlement legs #283

Merged
merged 3 commits into from
Jul 18, 2024
Merged
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
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
polymath-eric marked this conversation as resolved.
Show resolved Hide resolved
): 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();
}
prashantasdeveloper marked this conversation as resolved.
Show resolved Hide resolved
prashantasdeveloper marked this conversation as resolved.
Show resolved Hide resolved
}
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,
polymath-eric marked this conversation as resolved.
Show resolved Hide resolved
})
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 */

polymath-eric marked this conversation as resolved.
Show resolved Hide resolved
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[];
}
24 changes: 24 additions & 0 deletions src/settlements/dto/asset-leg.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* 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',
})
prashantasdeveloper marked this conversation as resolved.
Show resolved Hide resolved
@IsEnum(LegType)
readonly type: LegType;
}
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');
}
prashantasdeveloper marked this conversation as resolved.
Show resolved Hide resolved
}
Loading
Loading