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
11 changes: 11 additions & 0 deletions chain-api/src/types/ChainObject.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ it("allows $ only as the first character of a string key part", () => {
expect(() => ChainObject.getStringKeyFromParts(["a\u0000b"])).toThrow(/NUL/);
});

it("rejects an empty string key part", () => {
// "a" + "" + "b" would otherwise produce the same string as "a" + "$b"
expect(() => ChainObject.getStringKeyFromParts(["a", "", "b"])).toThrow(/index 1 is empty/);
expect(() => ChainObject.getStringKeyFromParts(["a", undefined as unknown as string, "b"])).toThrow(
/index 1 is empty/
);
expect(ChainObject.getStringKeyFromParts(["a", "$b"])).toEqual("a$$b");
expect(() => ChainObject.getStringKeyFromParts(["a", ""])).toThrow(/index 1 is empty/);
expect(() => ChainObject.getStringKeyFromParts([""])).toThrow(/index 0 is empty/);
});

it("rejects a NUL character in a joined query prefix", () => {
expect(ChainObject.joinKeyParts(["a", "b_c", null])).toEqual("a\u0000b_c\u0000");
expect(() => ChainObject.joinKeyParts(["a\u0000b", "c"])).toThrow(/NUL/);
Expand Down
4 changes: 4 additions & 0 deletions chain-api/src/types/ChainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,12 @@ export abstract class ChainObject {
public static getStringKeyFromParts(parts: string[]): string {
// "$" may be the first character of a part. "aaa" + "$bb" is "aaa$$bb".
// A "$" later in the part is rejected, so "aaa$" + "bb" cannot produce the same string.
// An empty part is rejected, so "aaa" + "" + "bb" cannot produce "aaa$$bb" either.
const texts = parts.map((part, index) => {
const text = part === null || part === undefined ? "" : String(part);
if (text.length === 0) {
throw new InvalidCompositeKeyError(`string key part at index ${index} is empty`);
}
if (text.includes(ChainObject.MIN_UNICODE_RUNE_VALUE)) {
throw new InvalidCompositeKeyError(`string key part at index ${index} contains a NUL character`);
}
Expand Down
22 changes: 5 additions & 17 deletions chain-api/src/types/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,9 +375,13 @@ export class UpdateTokenClassDto extends SubmitCallDTO {
}

@JSONSchema({
description: "Contains parameters for fetching balances. Each parameter is optional."
description:
"Contains parameters for fetching balances. Each parameter is optional. " +
"The response contains at most 1000 balances. Use a paginated balance query for more."
})
export class FetchBalancesDto extends ChainCallDTO {
public static readonly MAX_RESULTS = 1000;

@JSONSchema({
description: "Person who owns the balance. If the value is missing, chaincode caller is used."
})
Expand Down Expand Up @@ -411,22 +415,6 @@ export class FetchBalancesDto extends ChainCallDTO {
})
@IsOptional()
additionalKey?: string;

@JSONSchema({
description: "Page bookmark. If it is undefined, then the first page is returned."
})
@IsOptional()
@IsNotEmpty()
bookmark?: string;

@JSONSchema({
description: "Page size. When omitted, the query uses the total results limit. Maximum 10000."
})
@IsOptional()
@Max(10 * 1000)
@Min(1)
@IsInt()
limit?: number;
}

@JSONSchema({
Expand Down
451 changes: 187 additions & 264 deletions chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions chain-cli/chaincode-template/e2e/burnNFT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import {
BurnTokensDto,
FetchBalancesDto,
TokenBalance,
TokenBurn,
TokenClassKey,
TokenInstanceKey,
Expand Down Expand Up @@ -88,10 +89,13 @@ describe("NFT Burn scenario", () => {
});

// When
const user1checkResponse = await client.assets.evaluateTransaction("FetchBalances", user1TokenInstances);
const page = user1checkResponse.Data as { results?: Array<{ instanceIds?: BigNumber[] }> };
const user1checkResponse = await client.assets.evaluateTransaction(
"FetchBalances",
user1TokenInstances,
TokenBalance
);

// Then
expect(page.results?.[0].instanceIds).toEqual([]);
expect((user1checkResponse.Data ?? [])[0].instanceIds).toEqual([]);
});
});
3 changes: 1 addition & 2 deletions chain-cli/chaincode-template/e2e/setupTransferFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ export async function setupTransferFees(client: AdminChainClients, users: ChainU
"FetchBalances",
user.sign(userGalaBalancesDto.withOperation(mockedOperation("GalaChainToken:FetchBalances")))
);
const page = userGalaBalanceResponse.Data as { results?: Array<{ quantity?: BigNumber }> };
expect(new BigNumber(page.results?.[0]?.quantity ?? 0).isGreaterThan(0)).toBe(true);
expect(new BigNumber(userGalaBalanceResponse.Data?.[0]?.quantity ?? 0).isGreaterThan(0)).toBe(true);
}
}
6 changes: 2 additions & 4 deletions chain-cli/chaincode-template/e2e/simpleNFT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,10 @@ describe("Simple NFT scenario", () => {
// When
const user1checkResponse = await client.assets.evaluateTransaction("FetchBalances", user1BalancesDto);
const user2checkResponse = await client.assets.evaluateTransaction("FetchBalances", user2BalancesDto);
const user1Page = user1checkResponse.Data as { results?: Array<{ instanceIds?: BigNumber[] }> };
const user2Page = user2checkResponse.Data as { results?: Array<{ instanceIds?: BigNumber[] }> };

// Then
expect(new BigNumber(user1Page.results?.[0].instanceIds?.[0] ?? 0).isEqualTo(1)).toBe(true);
expect(new BigNumber(user2Page.results?.[0].instanceIds?.[0] ?? 0).isEqualTo(2)).toBe(true);
expect(new BigNumber((user1checkResponse.Data ?? [])[0].instanceIds?.[0] ?? 0).isEqualTo(1)).toBe(true);
expect(new BigNumber((user2checkResponse.Data ?? [])[0].instanceIds?.[0] ?? 0).isEqualTo(2)).toBe(true);
});

it("transfer NFT between users", async () => {
Expand Down
14 changes: 8 additions & 6 deletions chain-cli/chaincode-template/e2e/tokenOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import {
ApplyRequestsDto,
CreateTokenClassDto,
FetchBalancesDto,
FetchBalancesWithPaginationResponse,
GetMyProfileDto,
GrantAllowanceDto,
MintTokenDto,
TokenAllowance,
TokenBalance,
TokenClassKey,
TokenInstance,
TokenInstanceKey,
Expand Down Expand Up @@ -148,26 +148,28 @@ export async function createTransferDto(
* @param client - Chain client for blockchain queries
* @param nftClassKey - Token class identifier
* @param owner - User identity key to query balances for
* @returns Promise resolving to the token instance ids, sorted numerically
* @returns Promise resolving to array of token instance IDs sorted numerically
*
* @example
* ```typescript
* const instances = await fetchNFTInstances(client, nftClassKey, user.identityKey);
* console.log(instances); // ["1", "3", "7"]
* ```
*/
export async function fetchNFTInstances(
client: ChainClient,
nftClassKey: TokenClassKey,
owner: string
): Promise<BigNumber[]> {
): Promise<string[]> {
const dto = await createValidDTO(FetchBalancesDto, {
...instanceToPlain(nftClassKey),
owner: asValidUserRef(owner)
});

const resp = await client.evaluateTransaction("FetchBalances", dto, FetchBalancesWithPaginationResponse);
const balance = resp.Data?.results?.[0] as unknown as { instanceIds?: BigNumber[] } | undefined;
return (balance?.instanceIds ?? []).sort((a, b) => a.comparedTo(b));
const resp = await client.evaluateTransaction("FetchBalances", dto, TokenBalance);
expect(resp).toEqual(transactionSuccess([expect.anything()]));

return (resp.Data ?? [])[0].instanceIds.sort((a, b) => a.comparedTo(b));
}

/**
Expand Down
20 changes: 9 additions & 11 deletions chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import {
FetchAllowancesResponse,
FetchBalancesDto,
FetchBalancesWithPaginationDto,
FetchBalancesWithPaginationResponse,
FetchBalancesWithTokenMetadataResponse,
FetchBurnsDto,
FetchFeeScheduleDto,
Expand Down Expand Up @@ -406,21 +405,20 @@ export default class GalaChainTokenContract extends GalaContract {
@GalaTransaction({
type: EVALUATE,
in: FetchBalancesDto,
out: FetchBalancesWithPaginationResponse,
out: { arrayOf: TokenBalance },
description:
"Returns a page of balances. Provide limit and bookmark to page. " +
"When limit is omitted, the query uses the total results limit."
`Deprecated: returns at most ${FetchBalancesDto.MAX_RESULTS} matching balances, without pagination. ` +
"Use FetchBalancesWithTokenMetadata, which supports pagination, instead.",
deprecated: true
})
public async FetchBalances(
ctx: GalaChainContext,
dto: FetchBalancesDto
): Promise<FetchBalancesWithPaginationResponse> {
return fetchBalancesWithPagination(ctx, {
public async FetchBalances(ctx: GalaChainContext, dto: FetchBalancesDto): Promise<TokenBalance[]> {
const page = await fetchBalancesWithPagination(ctx, {
...dto,
owner: await resolveUserAlias(ctx, dto.owner ?? ctx.callingUser),
bookmark: dto.bookmark,
limit: dto.limit
bookmark: undefined,
limit: FetchBalancesDto.MAX_RESULTS
});
return page.results.slice(0, FetchBalancesDto.MAX_RESULTS);
}

@GalaTransaction({
Expand Down
Loading
Loading