Skip to content
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
4 changes: 2 additions & 2 deletions envs/8.0
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ POLYMESH_CHAIN_WS_PORT=9944
POLYMESH_CHAIN_RPC_PORT=9933
POLYMESH_CHAIN_P2P_PORT=30333

POLYMESH_REST_API_IMAGE=polymeshassociation/polymesh-rest-api:v8.1.0
POLYMESH_REST_API_IMAGE=polymeshassociation/polymesh-rest-api:v9.0.0-alpha.1
POLYMESH_REST_API_LOCAL_SM_PORT=3004
POLYMESH_REST_API_VAULT_SM_PORT=3005

POLYMESH_SUBQUERY_IMAGE=polymeshassociation/polymesh-subquery:v19.6.0
POLYMESH_SUBQUERY_IMAGE=polymeshassociation/polymesh-subquery:v19.7.0-alpha.2

POLYMESH_SUBQUERY_GRAPHQL_IMAGE=onfinality/subql-query:v2.25.0
POLYMESH_SUBQUERY_GRAPHQL_PORT=3000
Expand Down
4 changes: 2 additions & 2 deletions envs/local
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ POLYMESH_CHAIN_WS_PORT=9944
POLYMESH_CHAIN_RPC_PORT=9933
POLYMESH_CHAIN_P2P_PORT=30333

POLYMESH_REST_API_IMAGE=polymeshassociation/polymesh-rest-api:local
POLYMESH_REST_API_IMAGE=polymeshassociation/polymesh-rest-api:v9.0.0-alpha.1
POLYMESH_REST_API_LOCAL_SM_PORT=3004
POLYMESH_REST_API_VAULT_SM_PORT=3005

POLYMESH_SUBQUERY_IMAGE=polymeshassociation/polymesh-subquery:v19.6.0
POLYMESH_SUBQUERY_IMAGE=polymeshassociation/polymesh-subquery:v19.7.0-alpha.2

POLYMESH_SUBQUERY_GRAPHQL_IMAGE=onfinality/subql-query:v2.25.0
POLYMESH_SUBQUERY_GRAPHQL_PORT=3000
Expand Down
5 changes: 4 additions & 1 deletion tests/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ const config: Config.InitialOptions = {
"/node_modules/(?![@polymeshassociation/src]).+\\.js$",
],
testMatch: ["**/__tests__/**/*.(ts|tsx)"],
testPathIgnorePatterns: ["dist", ".history", "utils.ts"],
// testPathIgnorePatterns entries are unanchored regexes matched against the full path, so a
// bare "dist" also excludes any test whose name merely contains that substring (e.g. a
// "distributions" test) — anchor on the path separator to only exclude the actual dist/ dir.
testPathIgnorePatterns: ["/dist/", "/\\.history/", "/utils\\.ts$"],
moduleNameMapper: {
"~/(.*)": "<rootDir>/src/$1",
},
Expand Down
53 changes: 53 additions & 0 deletions tests/src/__tests__/rest/accounts/balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { BigNumber } from '@polymeshassociation/polymesh-sdk';

import { TestFactory } from '~/helpers';
import { RestClient } from '~/rest';
import { Identity } from '~/rest/identities/interfaces';

const handles = ['holder'];
let factory: TestFactory;

/*
Chain v8 changed how a POLYX balance is derived. `free` is now what the Account can actually
spend, and the raw chain values are exposed as `reserved` and `frozen`.
*/
describe('Account Balance', () => {
let restClient: RestClient;
let holder: Identity;
let address: string;

beforeAll(async () => {
factory = await TestFactory.create({ handles });
({ restClient } = factory);
holder = factory.getSignerIdentity(handles[0]);
address = holder.primaryAccount.account.address;
});

afterAll(async () => {
await factory.close();
});

it('should expose the full POLYX balance breakdown', async () => {
const balance = await restClient.accounts.getBalance(address);

expect(balance).toMatchObject({
free: expect.any(String),
locked: expect.any(String),
total: expect.any(String),
reserved: expect.any(String),
frozen: expect.any(String),
});
});

it('should keep the balance components consistent', async () => {
const { free, locked, total, reserved, frozen } = await restClient.accounts.getBalance(
address
);

expect(new BigNumber(total)).toEqual(new BigNumber(free).plus(locked));
expect(new BigNumber(free).gt(0)).toBe(true);
expect(new BigNumber(reserved).gte(0)).toBe(true);
expect(new BigNumber(frozen).gte(0)).toBe(true);
expect(new BigNumber(locked).gte(reserved)).toBe(true);
});
});
3 changes: 2 additions & 1 deletion tests/src/__tests__/rest/accounts/treasuryBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ describe('Accounts Treasury Balance', () => {
expect(treasuryBalance).toBeDefined();
expect(treasuryBalance).toHaveProperty('balance');
expect(typeof treasuryBalance.balance).toBe('string');
expect(treasuryBalance.balance).toMatch(/^\d+$/);
// chain v8 balances can carry sub-unit precision (e.g. "49999999.999999")
expect(treasuryBalance.balance).toMatch(/^\d+(\.\d+)?$/);
});
});
});
81 changes: 81 additions & 0 deletions tests/src/__tests__/rest/assets/fundingRound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { TestFactory } from '~/helpers';
import { RestClient } from '~/rest';
import { createAssetParams, issueAssetParams } from '~/rest/assets';
import { ProcessMode } from '~/rest/common';
import { Identity } from '~/rest/identities/interfaces';

const handles = ['issuer'];
let factory: TestFactory;

describe('GET /assets/:asset/funding-rounds/:round/issued', () => {
let restClient: RestClient;
let signer: string;
let issuer: Identity;
let assetId: string;

const firstRound = 'Series A';
const initialSupply = '10000';

beforeAll(async () => {
factory = await TestFactory.create({ handles });
({ restClient } = factory);
issuer = factory.getSignerIdentity(handles[0]);
signer = issuer.signer;

const assetParams = createAssetParams(
{
options: { processMode: ProcessMode.Submit, signer },
},
{
initialSupply,
fundingRound: firstRound,
}
);
assetId = await restClient.assets.createAndGetAssetId(assetParams);
});

afterAll(async () => {
await factory.close();
});

it('should report the initial supply against the initial funding round', async () => {
const result = await restClient.assets.getIssuedInFundingRound(assetId, firstRound);

expect(result).toEqual({
fundingRound: firstRound,
issued: initialSupply,
});
});

it('should report zero for a funding round the Asset never had', async () => {
const result = await restClient.assets.getIssuedInFundingRound(assetId, 'Never Happened');

expect(result).toEqual({
fundingRound: 'Never Happened',
issued: '0',
});
});

it('should accumulate further issuance into the current funding round', async () => {
const extra = '25';

const txData = await restClient.assets.issue(
assetId,
issueAssetParams(extra, {
options: { processMode: ProcessMode.Submit, signer },
})
);
expect(txData).toMatchObject({
transactions: expect.arrayContaining([
expect.objectContaining({ transactionTag: 'asset.issue' }),
]),
});

const result = await restClient.assets.getIssuedInFundingRound(assetId, firstRound);

expect(result).toEqual({
fundingRound: firstRound,
issued: '10025',
});
});
});
103 changes: 103 additions & 0 deletions tests/src/__tests__/rest/assets/transferFunds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { BigNumber } from '@polymeshassociation/polymesh-sdk';

import { expectBasicTxInfo } from '~/__tests__/rest/utils';
import { TestFactory } from '~/helpers';
import { RestClient } from '~/rest';
import { createAssetParams, transferFundsParams } from '~/rest/assets';
import { ProcessMode } from '~/rest/common';
import { Identity } from '~/rest/identities/interfaces';
import { RestSuccessResult } from '~/rest/interfaces';
import { portfolioParams } from '~/rest/portfolios';
import { awaitMiddlewareSyncedForRestApi } from '~/util';

const handles = ['issuer', 'investor'];
let factory: TestFactory;

describe('POST /assets/transfer-funds', () => {
let restClient: RestClient;
let signer: string;
let issuer: Identity;
let investor: Identity;
let assetId: string;

beforeAll(async () => {
factory = await TestFactory.create({ handles });
({ restClient } = factory);
issuer = factory.getSignerIdentity(handles[0]);
investor = factory.getSignerIdentity(handles[1]);
signer = issuer.signer;

const assetParams = createAssetParams({
options: { processMode: ProcessMode.Submit, signer },
});
assetId = await restClient.assets.createAndGetAssetId(assetParams);
});

afterAll(async () => {
await factory.close();
});

it('should settle immediately and return no Instruction for a same-Identity transfer', async () => {
const params = portfolioParams(factory.nextPortfolio(), {
options: { processMode: ProcessMode.Submit, signer },
});
const { portfolio } = await restClient.portfolios.createPortfolio(params);

const txData = await restClient.assets.transferFunds(
transferFundsParams(
assetId,
{ did: issuer.did, id: '0' },
{ did: issuer.did, id: portfolio.id },
'100',
{ options: { processMode: ProcessMode.Submit, signer } }
)
);

expect(txData).toMatchObject({
transactions: expect.arrayContaining([expect.objectContaining({ ...expectBasicTxInfo })]),
});
expect((txData as RestSuccessResult).instruction).toBeUndefined();

const portfolioData = await restClient.portfolios.getPortfolio(issuer.did, portfolio.id);
const hasAsset = portfolioData.assetBalances.find((balance) => balance.asset === assetId);
expect(hasAsset?.total).toBe('100');
});

it('should return a pending Instruction for a cross-Identity transfer awaiting affirmation', async () => {
// Receivers auto-affirm by default; opt the investor into mandatory affirmation so the
// transfer is guaranteed to come back as a pending Instruction rather than settle inline.
await restClient.identities.setMandatoryReceiverAffirmation(investor.did, {
requirement: 'Required',
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});

const txData = await restClient.assets.transferFunds(
transferFundsParams(
assetId,
{ did: issuer.did, id: '0' },
{ did: investor.did, id: '0' },
'50',
{ options: { processMode: ProcessMode.Submit, signer } }
)
);

const instructionId = (txData as RestSuccessResult).instruction as string;
expect(instructionId).toEqual(expect.any(String));

await awaitMiddlewareSyncedForRestApi(
txData as RestSuccessResult,
restClient,
new BigNumber(1)
);

const details = await restClient.settlements.getInstruction(instructionId);
expect(details).toMatchObject({ status: 'Pending' });

const affirmResult = await restClient.settlements.affirmInstruction(instructionId, {
options: { processMode: ProcessMode.Submit, signer: investor.signer },
});
expect(affirmResult).toMatchObject({
transactions: expect.arrayContaining([expect.objectContaining({ ...expectBasicTxInfo })]),
});
});
});
24 changes: 24 additions & 0 deletions tests/src/__tests__/rest/checkpoints/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ describe('Checkpoints Controller', () => {
});

describe('Checkpoint Schedule Management', () => {
it('should 404 when the asset has no active schedules', async () => {
const result = await restClient.checkpoints.getNextCheckpoint(assetId);

expect(result).toMatchObject({
statusCode: 404,
message: expect.any(String),
});
});

it('should create a checkpoint schedule', async () => {
// Create a checkpoint schedule
const createScheduleTx = (await restClient.checkpoints.createSchedule(
Expand Down Expand Up @@ -346,6 +355,21 @@ describe('Checkpoints Controller', () => {
);
});

it('should get the closest upcoming checkpoint across the asset schedules', async () => {
const nextCheckpoint = await restClient.checkpoints.getNextCheckpoint(assetId);

expect(nextCheckpoint).toMatchObject({
nextAt: expect.any(String),
totalPending: expect.any(String),
schedules: expect.arrayContaining([
expect.objectContaining({
id: scheduleId,
nextAt: expect.any(String),
}),
]),
});
});

it('should delete a schedule', async () => {
// Delete the schedule
const deleteScheduleTx = await restClient.checkpoints.deleteSchedule(assetId, scheduleId, {
Expand Down
Loading
Loading