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
52 changes: 52 additions & 0 deletions chain-api/src/types/ChainObject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/
import { BigNumber } from "bignumber.js";
import { Transform } from "class-transformer";
import { IsString } from "class-validator";

import { ChainKey } from "../utils";
import { BigNumberProperty } from "../validators";
Expand All @@ -37,6 +38,57 @@ class TestClass extends ChainObject {
}
}

class TestClassWithConstructorField extends ChainObject {
static INDEX_KEY = "test-ctor";

@ChainKey({ position: 0 })
@IsString()
owner: string;

plantedAt: number;

constructor() {
super();
this.plantedAt = 0;
}
}

it("should drop undeclared fields when deserializing a chain object", () => {
// Given
const stored = {
bigNum: "123",
category: "legendary",
quantityLocked: "5",
leftoverField: true
};

// When
const obj = ChainObject.deserialize(TestClass, stored);

// Then
expect(obj).toBeInstanceOf(TestClass);
expect(obj.bigNum).toEqual(new BigNumber("123"));
expect(obj).not.toHaveProperty("quantityLocked");
expect(obj).not.toHaveProperty("leftoverField");
});

it("should keep constructor-initialized fields that have no validator", () => {
// Given
const stored = {
owner: "client|user1",
plantedAt: 1_700_000_000_000,
leftoverField: true
};

// When
const obj = ChainObject.deserialize(TestClassWithConstructorField, stored);

// Then
expect(obj.plantedAt).toEqual(1_700_000_000_000);
expect(obj.owner).toEqual("client|user1");
expect(obj).not.toHaveProperty("leftoverField");
});

it("should use custom serializers while constructing composite key", () => {
// Given
const bigNumStr = "730750818665451215712927172538123444058715062271"; // MAX_SAFE_INTEGER^3
Expand Down
7 changes: 5 additions & 2 deletions chain-api/src/types/ChainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import {
ValidationFailedError,
deserialize,
getValidationErrorMessages,
serialize
serialize,
stripUnknownProperties
} from "../utils";
import { ClassConstructor, Inferred } from "./dtos";

Expand Down Expand Up @@ -79,7 +80,9 @@ export abstract class ChainObject {
constructor: ClassConstructor<Inferred<T, ChainObject>>,
object: string | Record<string, unknown> | Record<string, unknown>[]
): T {
return deserialize<T, ChainObject>(constructor, object);
const result = deserialize<T, ChainObject>(constructor, object);
stripUnknownProperties(result as object);
return result;
}

public getCompositeKey(): string {
Expand Down
17 changes: 17 additions & 0 deletions chain-api/src/types/RangedChainObject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,23 @@ class TestRangedClass extends RangedChainObject {
}
}

it("should drop undeclared fields when deserializing a ranged chain object", () => {
// Given
const stored = {
isNft: true,
category: "legendary",
leftoverField: "drop-me"
};

// When
const obj = RangedChainObject.deserialize(TestRangedClass, stored);

// Then
expect(obj).toBeInstanceOf(TestRangedClass);
expect(obj.isNft).toEqual(true);
expect(obj).not.toHaveProperty("leftoverField");
});

it("should use custom serializers while constructing ranged key", () => {
// Given
const obj = new TestRangedClass(true, "legendary");
Expand Down
12 changes: 10 additions & 2 deletions chain-api/src/types/RangedChainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { instanceToPlain } from "class-transformer";
import { ValidationError, validate } from "class-validator";
import "reflect-metadata";

import { ChainKeyMetadata, ValidationFailedError, deserialize, serialize } from "../utils";
import {
ChainKeyMetadata,
ValidationFailedError,
deserialize,
serialize,
stripUnknownProperties
} from "../utils";
import { ChainObject, ObjectValidationFailedError } from "./ChainObject";
import { ClassConstructor, Inferred } from "./dtos";

Expand Down Expand Up @@ -51,7 +57,9 @@ export abstract class RangedChainObject {
constructor: ClassConstructor<Inferred<T, RangedChainObject>>,
object: string | Record<string, unknown> | Record<string, unknown>[]
): T {
return deserialize<T, RangedChainObject>(constructor, object);
const result = deserialize<T, RangedChainObject>(constructor, object);
stripUnknownProperties(result as object);
return result;
}

public getRangedKey(): string {
Expand Down
23 changes: 23 additions & 0 deletions chain-api/src/types/TokenBalance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,29 @@ describe("legacy state compatibility", () => {
expect(errors).toEqual([]);
});

it("should drop unknown legacy fields when deserializing a TokenBalance", () => {
// Given - stored balance JSON with fields removed from TokenBalance
const storedBalance = {
owner: "client|user1",
collection: "test-collection",
category: "test-category",
type: "test-type",
additionalKey: "test-additional-key",
quantity: "10",
quantityLocked: "5",
quantityInUse: "2"
};

// When
const balance = TokenBalance.deserialize(TokenBalance, storedBalance);

// Then
expect(balance).toBeInstanceOf(TokenBalance);
expect(balance).not.toHaveProperty("quantityLocked");
expect(balance).not.toHaveProperty("quantityInUse");
expect(balance.getQuantityTotal()).toEqual(new BigNumber("10"));
});

it("should build TokenBalanceWithMetadata from a balance with unknown legacy fields", async () => {
// Given - stored balance JSON with fields removed before the SDK was open-sourced
const storedBalance = {
Expand Down
32 changes: 31 additions & 1 deletion chain-api/src/types/TokenInstanceMetadata.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import {
FetchTokenInstanceMetadataWithPaginationDto,
MAX_METADATA_ATTRIBUTES,
MAX_METADATA_CUSTOM_FIELDS,
SetTokenInstanceMetadataDto
SetTokenInstanceMetadataDto,
TokenInstanceMetadata
} from "./TokenInstanceMetadata";

const tokenInstance = {
Expand Down Expand Up @@ -151,6 +152,35 @@ describe("SetTokenInstanceMetadataDto array caps", () => {
});
});

describe("TokenInstanceMetadata deserialize", () => {
it("should keep customFields and drop leftover fields", () => {
// Given
const stored = {
collection: tokenInstance.collection,
category: tokenInstance.category,
type: tokenInstance.type,
additionalKey: tokenInstance.additionalKey,
instance: "1",
project: "TestProject",
name: "Test Elixir #1",
customFields: [{ key: "gameId", value: "elixir-001" }],
leftoverField: "should-be-dropped",
createdBy: "client|admin",
lastModifiedBy: "client|admin",
created: 1,
lastModified: 1
};

// When
const metadata = TokenInstanceMetadata.deserialize(TokenInstanceMetadata, stored);

// Then
expect(metadata).toBeInstanceOf(TokenInstanceMetadata);
expect(metadata).not.toHaveProperty("leftoverField");
expect(metadata.customFields).toEqual([expect.objectContaining({ key: "gameId", value: "elixir-001" })]);
});
});

describe("FetchTokenInstanceMetadataWithPaginationDto instance", () => {
async function paginationErrorsFor(instance: string): Promise<string[]> {
const dto = plainToInstance(FetchTokenInstanceMetadataWithPaginationDto, {
Expand Down
8 changes: 8 additions & 0 deletions chain-api/src/types/TokenInstanceMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,14 @@ export class FetchTokenInstanceMetadataWithPaginationDto extends ChainCallDTO {
}

export class FetchTokenInstanceMetadataResponse extends ChainCallDTO {
constructor(params?: { results: TokenInstanceMetadata[]; nextPageBookmark?: string }) {
super();
if (params) {
this.results = params.results;
this.nextPageBookmark = params.nextPageBookmark;
}
}

@JSONSchema({ description: "List of token instance metadata documents." })
@ValidateNested({ each: true })
@Type(() => TokenInstanceMetadata)
Expand Down
52 changes: 36 additions & 16 deletions chain-api/src/types/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ import {
} from "class-validator";
import { JSONSchema } from "class-validator-jsonschema";

import {
BigNumberIsNotNegative,
BigNumberIsPositive,
BigNumberProperty,
IsUserRef,
ValidateNestedAllowUnknown
} from "../validators";
import { BigNumberIsNotNegative, BigNumberIsPositive, BigNumberProperty, IsUserRef } from "../validators";
import { TokenBalance } from "./TokenBalance";
import { TokenBalanceTargets } from "./TokenBalanceTargets";
import { TokenClass, TokenClassKey } from "./TokenClass";
Expand Down Expand Up @@ -116,10 +110,16 @@ export class FetchTokenClassesWithPaginationDto extends ChainCallDTO {
}

export class FetchTokenClassesResponse extends ChainCallDTO {
constructor(params?: { results: TokenClass[]; nextPageBookmark?: string }) {
super();
if (params) {
this.results = params.results;
this.nextPageBookmark = params.nextPageBookmark;
}
}

@JSONSchema({ description: "List of Token Classes." })
// Token classes read of chain may carry legacy fields absent from the current
// class definition, so unknown properties must not be rejected.
@ValidateNestedAllowUnknown({ each: true })
@ValidateNested({ each: true })
@Type(() => TokenClass)
results: TokenClass[];

Expand Down Expand Up @@ -476,12 +476,18 @@ export class FetchBalancesWithPaginationDto extends ChainCallDTO {
description: "Response DTO containing a TokenBalance and the balance's corresponding TokenClass."
})
export class TokenBalanceWithMetadata extends ChainCallDTO {
constructor(params?: { balance: TokenBalance; token: TokenClass }) {
super();
if (params) {
this.balance = params.balance;
this.token = params.token;
}
}

@JSONSchema({
description: "A TokenBalance read of chain."
})
// Balances read of chain may carry fields removed from TokenBalance long ago
// (pre-open-source legacy state), so unknown properties must not be rejected.
@ValidateNestedAllowUnknown()
@ValidateNested()
@Type(() => TokenBalance)
@IsObject()
balance: TokenBalance;
Expand All @@ -495,10 +501,16 @@ export class TokenBalanceWithMetadata extends ChainCallDTO {
}

export class FetchBalancesWithPaginationResponse extends ChainCallDTO {
constructor(params?: { results: TokenBalance[]; nextPageBookmark?: string }) {
super();
if (params) {
this.results = params.results;
this.nextPageBookmark = params.nextPageBookmark;
}
}

@JSONSchema({ description: "List of balances with token metadata." })
// Balances read of chain may carry legacy fields absent from the current
// class definition, so unknown properties must not be rejected.
@ValidateNestedAllowUnknown({ each: true })
@ValidateNested({ each: true })
@Type(() => TokenBalance)
results: TokenBalance[];

Expand All @@ -509,6 +521,14 @@ export class FetchBalancesWithPaginationResponse extends ChainCallDTO {
}

export class FetchBalancesWithTokenMetadataResponse extends ChainCallDTO {
constructor(params?: { results: TokenBalanceWithMetadata[]; nextPageBookmark?: string }) {
super();
if (params) {
this.results = params.results;
this.nextPageBookmark = params.nextPageBookmark;
}
}

@JSONSchema({ description: "List of balances with token metadata." })
@ValidateNested({ each: true })
@Type(() => TokenBalanceWithMetadata)
Expand Down
1 change: 1 addition & 0 deletions chain-api/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export * from "./error";
export * from "../ethers/type-utils";
export * from "./randomUniqueKey";

export { stripUnknownProperties } from "./stripUnknownProperties";
export {
deserialize,
serialize,
Expand Down
Loading
Loading