Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3b0c07c
chore: 🤖 bump SDK to v31.0.0-beta.7
prashantasdeveloper Aug 14, 2026
25cd4fb
feat: 🎸 remove subsidize account endpoint
prashantasdeveloper Aug 14, 2026
cfcda5b
feat: 🎸 remove instruction affirmation withdrawal endpoints
prashantasdeveloper Aug 14, 2026
7ed4bb8
feat: 🎸 remove CDD claims endpoint
prashantasdeveloper Aug 14, 2026
43a863a
fix: 🐛 replace TransferError members removed in v31
prashantasdeveloper Aug 14, 2026
f02191b
feat: 🎸 return pending Instruction from transfer funds
prashantasdeveloper Aug 14, 2026
f2f4f48
fix: 🐛 grant test admins DID Registrar status on chain v8
prashantasdeveloper Aug 14, 2026
4dd6064
test: 💍 update expected transaction groups for v31
prashantasdeveloper Aug 14, 2026
9bfbaaf
docs: ✏️ update supported chain versions
prashantasdeveloper Aug 14, 2026
46de6a8
feat: 🎸 surface reserved and frozen fields on Account balance
prashantasdeveloper Aug 14, 2026
85d2654
docs: ✏️ remove stale CDD claim wording from transferPolyx
prashantasdeveloper Aug 14, 2026
75f0a2c
fix: 🐛 make createCdd and expiry optional on RegisterIdentityDto
prashantasdeveloper Aug 14, 2026
cebcf10
docs: ✏️ remove stale chain v7/v8 wording from endpoint descriptions
prashantasdeveloper Aug 14, 2026
a99bb63
chore: 🤖 remove unreferenced ScopeClaimProofDto
prashantasdeveloper Aug 14, 2026
204289c
feat: 🎸 add portfolio-level asset pre-approval
prashantasdeveloper Aug 14, 2026
cee6786
feat: 🎸 add instruction lock, unlock and relock status endpoints
prashantasdeveloper Aug 14, 2026
b4e034b
feat: 🎸 add Instruction leg status endpoint
prashantasdeveloper Aug 14, 2026
42d8762
feat: 🎸 add DID registrar registration endpoint
prashantasdeveloper Aug 14, 2026
e5f94e7
feat: 🎸 add Venue signer count endpoint
prashantasdeveloper Aug 14, 2026
0cb5015
feat: 🎸 add issued-in-funding-round endpoint
prashantasdeveloper Aug 14, 2026
63e6167
feat: 🎸 add next checkpoint endpoint
prashantasdeveloper Aug 14, 2026
eb3a610
feat: 🎸 add Corporate Action documents endpoint
prashantasdeveloper Aug 14, 2026
49aafd2
chore: 🤖 add missing getDocuments to MockCorporateActionsService
prashantasdeveloper Aug 14, 2026
0a735b8
feat: 🎸 add ticker registration config endpoint
prashantasdeveloper Aug 14, 2026
c4313e4
feat: 🎸 support creating SettleAfterLock Instructions
prashantasdeveloper Aug 14, 2026
ed6f14b
chore: 🤖 bump SDK to v31.0.0 (stable)
prashantasdeveloper Aug 19, 2026
e5079c8
fix: 🐛 require expiresAt on off-chain affirmation receipts
prashantasdeveloper Aug 19, 2026
0a5f89a
refactor: 💡 dedupe swagger decorators on portfolio pre-approval endpo…
prashantasdeveloper Aug 19, 2026
a6515b0
chore: 🤖 throw NotFoundException from getNextCheckpoint instead of null
prashantasdeveloper Aug 24, 2026
1a8e6f8
chore: 🤖 preserve null on TickerRegistrationConfigModel.registrationL…
prashantasdeveloper Aug 24, 2026
9bbf11b
chore: 🤖 restore missing 400 doc for registerIdentity expiry validation
prashantasdeveloper Aug 24, 2026
35cda7a
chore: 🤖 remove dead code left over from CDD claims removal
prashantasdeveloper Aug 24, 2026
659df7e
chore: 🤖 apply prettier formatting
prashantasdeveloper Aug 24, 2026
d83b488
chore: 🤖 exclude dist/ from docker build context
prashantasdeveloper Aug 24, 2026
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
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.git
.env*
node_modules/
node_modules/
/dist
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

A REST API wrapper for the Polymesh blockchain.

This version is compatible with chain versions 7.3.x - 8.x
This version is compatible with chain versions 8.0.x - 8.1.x. Chain v7 is no longer supported — the SDK throws on initialization if pointed at a v7 node.

## Setup

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"@polymeshassociation/fireblocks-signing-manager": "^3.0.0",
"@polymeshassociation/hashicorp-vault-signing-manager": "^4.1.0",
"@polymeshassociation/local-signing-manager": "^4.1.1",
"@polymeshassociation/polymesh-sdk": "v30.0.0",
"@polymeshassociation/polymesh-types": "^7.4.0",
"@polymeshassociation/polymesh-sdk": "31.0.0",
"@polymeshassociation/polymesh-types": "^7.5.0",
"@polymeshassociation/signing-manager-types": "^3.7.1",
"class-transformer": "0.5.1",
"class-validator": "^0.14.0",
Expand Down
2 changes: 2 additions & 0 deletions src/accounts/accounts.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ describe('AccountsController', () => {
free: new BigNumber(10),
locked: new BigNumber(1),
total: new BigNumber(11),
reserved: new BigNumber(1),
frozen: new BigNumber(0),
};
mockAccountsService.getAccountBalance.mockResolvedValue(mockResult);

Expand Down
19 changes: 9 additions & 10 deletions src/accounts/accounts.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { ModifyPermissionsDto } from '~/accounts/dto/modify-permissions.dto';
import { RevokePermissionsDto } from '~/accounts/dto/revoke-permissions.dto';
import { TransactionHistoryFiltersDto } from '~/accounts/dto/transaction-history-filters.dto';
import { TransferPolyxDto } from '~/accounts/dto/transfer-polyx.dto';
import { AccountBalanceModel } from '~/accounts/models/account-balance.model';
import {
AccountCollectionModel,
createAccountCollectionModel,
Expand All @@ -36,7 +37,6 @@ import { AccountDetailsModel } from '~/accounts/models/account-details.model';
import { MultiSigDetailsModel } from '~/accounts/models/multi-sig-details.model';
import { PermissionsModel } from '~/accounts/models/permissions.model';
import { AssetBalanceModel } from '~/assets/models/asset-balance.model';
import { BalanceModel } from '~/assets/models/balance.model';
import { ApiArrayResponse, ApiTransactionResponse } from '~/common/decorators/';
import { TransactionBaseDto } from '~/common/dto/transaction-base-dto';
import { ExtrinsicModel } from '~/common/models/extrinsic.model';
Expand Down Expand Up @@ -106,7 +106,8 @@ export class AccountsController {

@ApiOperation({
summary: 'Get POLYX balance of an Account',
description: 'This endpoint provides the free, locked and total POLYX balance of an Account',
description:
'This endpoint provides the free, locked and total POLYX balance of an Account. Note: as of chain v8, `locked` also includes funds on hold (e.g. bonded for staking), which is reflected in the `reserved` and `frozen` fields',
})
@ApiParam({
name: 'account',
Expand All @@ -116,12 +117,12 @@ export class AccountsController {
})
@ApiOkResponse({
description: 'Free, locked and total POLYX balance of the Account',
type: BalanceModel,
type: AccountBalanceModel,
})
@Get(':account/balance')
async getAccountBalance(@Param() { account }: AccountParamsDto): Promise<BalanceModel> {
async getAccountBalance(@Param() { account }: AccountParamsDto): Promise<AccountBalanceModel> {
const accountBalance = await this.accountsService.getAccountBalance(account);
return new BalanceModel(accountBalance);
return new AccountBalanceModel(accountBalance);
}

@ApiOperation({
Expand All @@ -136,7 +137,6 @@ export class AccountsController {
description:
'<ul>' +
"<li>The destination Account doesn't have an associated Identity</li>" +
'<li>The receiver Identity has an invalid CDD claim</li>' +
'<li>Insufficient free balance</li>' +
'</ul>',
})
Expand Down Expand Up @@ -232,8 +232,7 @@ export class AccountsController {

@ApiOperation({
summary: 'Get pending subsidies for an Account',
description:
'Returns pending subsidy requests awaiting acceptance by the beneficiary account on chain v8',
description: 'Returns pending subsidy requests awaiting acceptance by the beneficiary account',
})
@ApiParam({
name: 'account',
Expand All @@ -258,7 +257,7 @@ export class AccountsController {

@ApiOperation({
summary: 'Get fungible asset balances held by an Account',
description: 'Returns balances of all fungible assets held in the Account on chain v8',
description: 'Returns balances of all fungible assets held in the Account',
})
@ApiParam({
name: 'account',
Expand Down Expand Up @@ -291,7 +290,7 @@ export class AccountsController {

@ApiOperation({
summary: 'Get NFT collections held by an Account',
description: 'Returns NFT collections and holdings for the Account on chain v8',
description: 'Returns NFT collections and holdings for the Account',
})
@ApiParam({
name: 'account',
Expand Down
32 changes: 32 additions & 0 deletions src/accounts/models/account-balance.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* istanbul ignore file */

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

import { BalanceModel } from '~/assets/models/balance.model';
import { FromBigNumber } from '~/common/decorators/transformation';

export class AccountBalanceModel extends BalanceModel {
@ApiProperty({
type: 'string',
description:
'POLYX placed on hold by the protocol, e.g. bonded for staking. Not part of `free` and cannot be spent until released',
example: '100',
})
@FromBigNumber()
readonly reserved: BigNumber;

@ApiProperty({
type: 'string',
description:
'Minimum balance (out of `total`) that must remain in the Account due to freezes/locks (e.g. vesting). May overlap with `reserved`',
example: '0',
})
@FromBigNumber()
readonly frozen: BigNumber;

constructor(model: AccountBalanceModel) {
super(model);
Object.assign(this, model);
}
}
45 changes: 44 additions & 1 deletion src/assets/assets.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AssetsService } from '~/assets/assets.service';
import { AssetDocumentDto } from '~/assets/dto/asset-document.dto';
import { SetStatsDto } from '~/assets/dto/transfer-restrictions/set-stats.dto';
import { SetTransferRestrictionsDto } from '~/assets/dto/transfer-restrictions/set-transfer-restrictions.dto';
import { IssuedInFundingRoundModel } from '~/assets/models/issued-in-funding-round.model';
import { StatAccreditedClaimModel } from '~/assets/models/stat-claim-accredited.model';
import { StatAffiliateClaimModel } from '~/assets/models/stat-claim-affiliate.model';
import { StatJurisdictionClaimModel } from '~/assets/models/stat-claim-jurisdiction.model';
Expand All @@ -36,7 +37,7 @@ import { ProcessMode } from '~/common/types';
import { MetadataService } from '~/metadata/metadata.service';
import { PortfolioDto } from '~/portfolios/dto/portfolio.dto';
import { processedTxResult, testValues } from '~/test-utils/consts';
import { MockAsset, MockAuthorizationRequest } from '~/test-utils/mocks';
import { MockAsset, MockAuthorizationRequest, MockInstruction } from '~/test-utils/mocks';
import { MockAssetService, mockMetadataServiceProvider } from '~/test-utils/service-mocks';

const { signer, did, txResult, assetId } = testValues;
Expand Down Expand Up @@ -437,6 +438,33 @@ describe('AssetsController', () => {
expect(result).toEqual(processedTxResult);
expect(mockAssetsService.transferFunds).toHaveBeenCalledWith(body);
});

it('should return the pending Instruction when the transfer does not settle immediately', async () => {
const from = new AssetHolderDto({
type: AssetHolderType.account,
address: 'fromAddress',
});
const to = new AssetHolderDto({
type: AssetHolderType.account,
address: 'toAddress',
});
const amount = new BigNumber(100);
const body = { signer, from, to, asset: assetId, amount };

const mockInstruction = new MockInstruction();

mockAssetsService.transferFunds.mockResolvedValue({
...txResult,
result: mockInstruction,
});

const result = await controller.transferFunds(body);

expect(result).toEqual({
...processedTxResult,
instruction: mockInstruction,
});
});
});

describe('approveAllowance', () => {
Expand Down Expand Up @@ -519,6 +547,21 @@ describe('AssetsController', () => {
});
});

describe('getIssuedInFundingRound', () => {
it('should call the service and return the amount issued in the funding round', async () => {
mockAssetsService.getIssuedInFundingRound.mockResolvedValue(new BigNumber(1000));

const result = await controller.getIssuedInFundingRound({
asset: assetId,
round: 'Series A',
});

expect(result).toEqual(
new IssuedInFundingRoundModel({ fundingRound: 'Series A', issued: new BigNumber(1000) })
);
});
});

describe('addRequiredMediators', () => {
it('should call the service and return the results', async () => {
const mediators = ['someDid'];
Expand Down
54 changes: 47 additions & 7 deletions src/assets/assets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import {
ApiTags,
ApiUnprocessableEntityResponse,
} from '@nestjs/swagger';
import { Asset } from '@polymeshassociation/polymesh-sdk/types';
import { Asset, Instruction } from '@polymeshassociation/polymesh-sdk/types';

import { AssetsService } from '~/assets/assets.service';
import { createAssetDetailsModel } from '~/assets/assets.util';
import { ApproveAllowanceDto } from '~/assets/dto/approve-allowance.dto';
import { AssetParamsDto } from '~/assets/dto/asset-params.dto';
import { ControllerTransferDto } from '~/assets/dto/controller-transfer.dto';
import { CreateAssetDto } from '~/assets/dto/create-asset.dto';
import { FundingRoundParamsDto } from '~/assets/dto/funding-round-params.dto';
import { GetAllowanceParamsDto } from '~/assets/dto/get-allowance-params.dto';
import { IssueDto } from '~/assets/dto/issue.dto';
import { LinkTickerDto } from '~/assets/dto/link-ticker.dto';
Expand All @@ -34,7 +35,9 @@ import { AssetDocumentModel } from '~/assets/models/asset-document.model';
import { AssetStatModel } from '~/assets/models/asset-stat.model';
import { CreatedAssetModel } from '~/assets/models/created-asset.model';
import { IdentityBalanceModel } from '~/assets/models/identity-balance.model';
import { IssuedInFundingRoundModel } from '~/assets/models/issued-in-funding-round.model';
import { RequiredMediatorsModel } from '~/assets/models/required-mediators.model';
import { TransferFundsResultModel } from '~/assets/models/transfer-funds-result.model';
import { TransferRestrictionsModel } from '~/assets/models/transfer-restrictions.model';
import { TransferRestrictionsValueModel } from '~/assets/models/transfer-restrictions-values.model';
import { VenueFilteringDetailsModel } from '~/assets/models/venue-filtering-details.model';
Expand Down Expand Up @@ -84,16 +87,24 @@ export class AssetsController {
@ApiOperation({
summary: 'Transfer funds between asset holders',
description:
'Transfers fungible tokens or NFTs between asset holders (accounts or portfolios) owned by the same identity',
'Transfers fungible tokens or NFTs between asset holders (accounts or portfolios). The holders may belong to different Identities, in which case a settlement Instruction is created and auto affirmed on behalf of the sender. It settles in the same transaction if the receiving Identity affirms automatically, otherwise the returned Instruction is left pending the receiver affirmation',
})
@ApiTransactionResponse({
description: 'Details about the transaction',
type: TransactionQueueModel,
description:
'Details about the transaction, along with the pending Instruction if the transfer did not settle immediately',
type: TransferFundsResultModel,
})
@Post('transfer-funds')
public async transferFunds(@Body() params: TransferFundsDto): Promise<TransactionResponseModel> {
const result = await this.assetsService.transferFunds(params);
return handleServiceResult(result);

const resolver: TransactionResolver<Instruction | undefined> = ({
result: instruction,
transactions,
details,
}) => new TransferFundsResultModel({ instruction, transactions, details });

return handleServiceResult(result, resolver);
}

@ApiTags('nfts')
Expand Down Expand Up @@ -569,8 +580,7 @@ export class AssetsController {

@ApiOperation({
summary: 'Approve fungible asset allowance',
description:
'Approves a spender account allowance for transferring fungible asset tokens on chain v8',
description: 'Approves a spender account allowance for transferring fungible asset tokens',
})
@ApiParam({
name: 'asset',
Expand Down Expand Up @@ -665,6 +675,36 @@ export class AssetsController {
return new RequiredMediatorsModel({ mediators });
}

@ApiOperation({
summary: 'Get the total amount issued in a funding round',
description:
'This endpoint returns the total amount of the Asset issued in the given funding round',
})
@ApiParam({
name: 'asset',
description: 'The Asset (Ticker/Asset ID) whose issued amount is to be fetched',
type: 'string',
example: '3616b82e-8e10-80ae-dc95-2ea28b9db8b3',
})
@ApiParam({
name: 'round',
description: 'The name of the funding round',
type: 'string',
example: 'Series A',
})
@ApiOkResponse({
description: 'The total amount of the Asset issued in the funding round',
type: IssuedInFundingRoundModel,
})
@Get(':asset/funding-rounds/:round/issued')
public async getIssuedInFundingRound(
@Param() { asset, round }: FundingRoundParamsDto
): Promise<IssuedInFundingRoundModel> {
const issued = await this.assetsService.getIssuedInFundingRound(asset, round);

return new IssuedInFundingRoundModel({ fundingRound: round, issued });
}

@ApiOperation({
summary: 'Add required mediators',
description:
Expand Down
15 changes: 15 additions & 0 deletions src/assets/assets.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,21 @@ describe('AssetsService', () => {
});
});

describe('getIssuedInFundingRound', () => {
it('should return the amount issued in the funding round', async () => {
const mockAsset = new MockAsset();
mockAsset.getIssuedInFundingRound.mockResolvedValue(new BigNumber(1000));

const findOneSpy = jest.spyOn(service, 'findOne');
findOneSpy.mockResolvedValue(mockAsset as unknown as FungibleAsset);

const result = await service.getIssuedInFundingRound('TICKER', 'Series A');

expect(result).toEqual(new BigNumber(1000));
expect(mockAsset.getIssuedInFundingRound).toHaveBeenCalledWith('Series A');
});
});

describe('addRequiredMediators', () => {
it('should run a addRequiredMediators procedure and return the transaction results', async () => {
const mockTransaction = setupMockTransaction(
Expand Down
11 changes: 10 additions & 1 deletion src/assets/assets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
HistoricAgentOperation,
Identity,
IdentityBalance,
Instruction,
NftCollection,
ResultSet,
TransferRestrictionParams,
Expand Down Expand Up @@ -265,7 +266,7 @@ export class AssetsService {
);
}

public async transferFunds(params: TransferFundsDto): ServiceReturn<void> {
public async transferFunds(params: TransferFundsDto): ServiceReturn<Instruction | undefined> {
const { options, args } = extractTxOptions(params);
const { from, to, asset, amount, nfts, memo } = args;

Expand Down Expand Up @@ -323,6 +324,14 @@ export class AssetsService {
});
}

public async getIssuedInFundingRound(
assetInput: string,
fundingRound: string
): Promise<BigNumber> {
const asset = await this.findOne(assetInput);
return asset.getIssuedInFundingRound(fundingRound);
}

public async addRequiredMediators(
assetInput: string,
params: RequiredMediatorsDto
Expand Down
10 changes: 10 additions & 0 deletions src/assets/dto/funding-round-params.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* istanbul ignore file */

import { IsString } from 'class-validator';

import { AssetParamsDto } from '~/assets/dto/asset-params.dto';

export class FundingRoundParamsDto extends AssetParamsDto {
@IsString()
readonly round: string;
}
Loading
Loading