From c832555369d00ac9287098069fc96d93c93702a2 Mon Sep 17 00:00:00 2001 From: Jeff Eganhouse Date: Mon, 6 Jul 2026 16:04:54 -0400 Subject: [PATCH] Token Instance Metadata --- chain-api/src/types/TokenInstanceMetadata.ts | 421 ++++ chain-api/src/types/index.ts | 1 + chain-api/src/validators/decorators.ts | 26 + .../e2e/__snapshots__/api.spec.ts.snap | 1815 ++++++++++++++++- .../e2e/tokenInstanceMetadata.spec.ts | 264 +++ .../src/token/GalaChainTokenContract.ts | 71 + .../src/token/__snapshots__/api.spec.ts.snap | 1815 ++++++++++++++++- chain-connect/src/chainApis/TokenApi.ts | 52 + chain-connect/src/types/tokenApi.ts | 19 + chain-test/src/data/nft.ts | 56 +- .../src/__test__/GalaChainTokenContract.ts | 69 + chaincode/src/index.ts | 1 + .../TokenInstanceMetadataError.ts | 48 + .../deleteTokenInstanceMetadata.spec.ts | 139 ++ .../deleteTokenInstanceMetadata.ts | 62 + .../fetchTokenInstanceMetadata.spec.ts | 151 ++ .../fetchTokenInstanceMetadata.ts | 102 + chaincode/src/tokenInstanceMetadata/index.ts | 56 + .../setTokenInstanceMetadata.spec.ts | 250 +++ .../setTokenInstanceMetadata.ts | 109 + .../tokenInstanceMetadataHelpers.ts | 75 + 21 files changed, 5383 insertions(+), 219 deletions(-) create mode 100644 chain-api/src/types/TokenInstanceMetadata.ts create mode 100644 chain-cli/chaincode-template/e2e/tokenInstanceMetadata.spec.ts create mode 100644 chaincode/src/tokenInstanceMetadata/TokenInstanceMetadataError.ts create mode 100644 chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.spec.ts create mode 100644 chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.ts create mode 100644 chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.spec.ts create mode 100644 chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.ts create mode 100644 chaincode/src/tokenInstanceMetadata/index.ts create mode 100644 chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.spec.ts create mode 100644 chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.ts create mode 100644 chaincode/src/tokenInstanceMetadata/tokenInstanceMetadataHelpers.ts diff --git a/chain-api/src/types/TokenInstanceMetadata.ts b/chain-api/src/types/TokenInstanceMetadata.ts new file mode 100644 index 0000000000..d8fe0ed764 --- /dev/null +++ b/chain-api/src/types/TokenInstanceMetadata.ts @@ -0,0 +1,421 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BigNumber } from "bignumber.js"; +import { Type } from "class-transformer"; +import { + IsDefined, + IsIn, + IsInt, + IsNotEmpty, + IsOptional, + IsPositive, + IsString, + Max, + MaxLength, + Min, + ValidateIf, + ValidateNested +} from "class-validator"; +import { JSONSchema } from "class-validator-jsonschema"; + +import { ChainKey } from "../utils"; +import { + BigNumberIsInteger, + BigNumberIsNotNegative, + BigNumberProperty, + IsStringOrNumber, + IsUserAlias +} from "../validators"; +import { ChainObject } from "./ChainObject"; +import { TokenInstanceKey } from "./TokenInstance"; +import { UserAlias } from "./UserAlias"; +import { ChainCallDTO, SubmitCallDTO } from "./dtos"; + +// Display types defined by the OpenSea metadata standard for numeric traits +export const METADATA_ATTRIBUTE_DISPLAY_TYPES = [ + "number", + "boost_number", + "boost_percentage", + "date" +] as const; + +@JSONSchema({ + description: "Single trait of a token instance, following the OpenSea metadata standard attribute format." +}) +export class TokenInstanceMetadataAttribute extends ChainCallDTO { + @IsNotEmpty() + @MaxLength(200) + public trait_type: string; + + @JSONSchema({ + description: "Trait value. Either a string or a finite number (strings limited to 500 characters)." + }) + @IsDefined() + @IsStringOrNumber(500) + public value: string | number; + + @JSONSchema({ + description: `Optional OpenSea display type for numeric traits: ${METADATA_ATTRIBUTE_DISPLAY_TYPES.join( + ", " + )}.` + }) + @IsOptional() + @IsIn(METADATA_ATTRIBUTE_DISPLAY_TYPES) + public display_type?: string; +} + +@JSONSchema({ + description: + "Arbitrary key-value entry for game or product specific token instance metadata " + + "that does not fit the OpenSea standard fields." +}) +export class TokenInstanceMetadataCustomField extends ChainCallDTO { + @IsNotEmpty() + @MaxLength(200) + public key: string; + + @IsNotEmpty() + @MaxLength(2000) + public value: string; +} + +@JSONSchema({ + description: + "Ownership record of a project's token instance metadata within a token class. " + + "The user who first creates metadata for a given project on any instance of a token class " + + "becomes the owner of that project's metadata for the whole class." +}) +export class TokenInstanceMetadataProject extends ChainObject { + public static INDEX_KEY = "GCTIMP"; + + @ChainKey({ position: 0 }) + @IsNotEmpty() + public collection: string; + + @ChainKey({ position: 1 }) + @IsNotEmpty() + public category: string; + + @ChainKey({ position: 2 }) + @IsNotEmpty() + public type: string; + + @ChainKey({ position: 3 }) + @IsDefined() + public additionalKey: string; + + @ChainKey({ position: 4 }) + @IsNotEmpty() + @MaxLength(200) + public project: string; + + @IsUserAlias() + public owner: UserAlias; + + @IsPositive() + public created: number; +} + +@JSONSchema({ + description: + "Metadata document for a single NFT token instance, scoped to a project. A token instance " + + "may have multiple metadata documents, one per project. Field names follow the OpenSea " + + "metadata standard (snake_case) for direct off-chain format parity." +}) +export class TokenInstanceMetadata extends ChainObject { + public static INDEX_KEY = "GCTIM"; + + @ChainKey({ position: 0 }) + @IsNotEmpty() + public collection: string; + + @ChainKey({ position: 1 }) + @IsNotEmpty() + public category: string; + + @ChainKey({ position: 2 }) + @IsNotEmpty() + public type: string; + + @ChainKey({ position: 3 }) + @IsDefined() + public additionalKey: string; + + @ChainKey({ position: 4 }) + @IsNotEmpty() + @BigNumberIsInteger() + @BigNumberIsNotNegative() + @BigNumberProperty() + public instance: BigNumber; + + @JSONSchema({ + description: "Identifier of the game or product this metadata document belongs to." + }) + @ChainKey({ position: 5 }) + @IsNotEmpty() + @MaxLength(200) + public project: string; + + @IsOptional() + @MaxLength(200) + public name?: string; + + @IsOptional() + @MaxLength(1000) + public description?: string; + + @IsOptional() + @MaxLength(500) + public image?: string; + + @IsOptional() + @MaxLength(500) + public external_url?: string; + + @IsOptional() + @MaxLength(500) + public animation_url?: string; + + @JSONSchema({ + description: "Background color as a six-character hexadecimal without a pre-pended #." + }) + @IsOptional() + @MaxLength(6) + public background_color?: string; + + @IsOptional() + @MaxLength(500) + public youtube_url?: string; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => TokenInstanceMetadataAttribute) + public attributes?: TokenInstanceMetadataAttribute[]; + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => TokenInstanceMetadataCustomField) + public customFields?: TokenInstanceMetadataCustomField[]; + + @IsUserAlias() + public createdBy: UserAlias; + + @IsUserAlias() + public lastModifiedBy: UserAlias; + + @IsPositive() + public created: number; + + @IsPositive() + public lastModified: number; +} + +@JSONSchema({ + description: + "Full-document upsert of a project's metadata for a single NFT token instance. The first user " + + "to create metadata for a given project on a token class becomes the owner of that project's " + + "metadata for the class; only the owner can update it afterwards. Replaces any existing " + + "metadata document of the project for the instance." +}) +export class SetTokenInstanceMetadataDto extends SubmitCallDTO { + @JSONSchema({ + description: "Key of the NFT token instance to attach the metadata to." + }) + @ValidateNested() + @Type(() => TokenInstanceKey) + @IsNotEmpty() + tokenInstance: TokenInstanceKey; + + @JSONSchema({ + description: "Identifier of the game or product this metadata document belongs to." + }) + @IsNotEmpty() + @MaxLength(200) + project: string; + + @IsOptional() + @MaxLength(200) + name?: string; + + @IsOptional() + @MaxLength(1000) + description?: string; + + @IsOptional() + @MaxLength(500) + image?: string; + + @IsOptional() + @MaxLength(500) + external_url?: string; + + @IsOptional() + @MaxLength(500) + animation_url?: string; + + @JSONSchema({ + description: "Background color as a six-character hexadecimal without a pre-pended #." + }) + @IsOptional() + @MaxLength(6) + background_color?: string; + + @IsOptional() + @MaxLength(500) + youtube_url?: string; + + @JSONSchema({ + description: "Traits following the OpenSea metadata standard attribute format." + }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => TokenInstanceMetadataAttribute) + attributes?: TokenInstanceMetadataAttribute[]; + + @JSONSchema({ + description: "Arbitrary key-value entries for game or product specific metadata." + }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => TokenInstanceMetadataCustomField) + customFields?: TokenInstanceMetadataCustomField[]; +} + +@JSONSchema({ + description: + "Fetches metadata documents of a single NFT token instance. If project is provided, fetches " + + "the document of that project only. Otherwise, fetches documents of all projects for the instance." +}) +export class FetchTokenInstanceMetadataDto extends ChainCallDTO { + @JSONSchema({ + description: "Key of the NFT token instance to fetch the metadata of." + }) + @ValidateNested() + @Type(() => TokenInstanceKey) + @IsNotEmpty() + tokenInstance: TokenInstanceKey; + + @JSONSchema({ + description: "Optional project identifier to fetch a single project's metadata document." + }) + @IsOptional() + @IsNotEmpty() + project?: string; +} + +@JSONSchema({ + description: + "Fetch token instance metadata documents currently available in world state. Supports " + + "filtering, pagination, and optionality of instance key properties." +}) +export class FetchTokenInstanceMetadataWithPaginationDto extends ChainCallDTO { + static readonly MAX_LIMIT = 10 * 1000; + static readonly DEFAULT_LIMIT = 1000; + + @JSONSchema({ + description: "Token collection. Optional, but required if category is provided." + }) + @ValidateIf((o) => !!o.category) + @IsNotEmpty() + collection?: string; + + @JSONSchema({ + description: "Token category. Optional, but required if type is provided." + }) + @ValidateIf((o) => !!o.type) + @IsNotEmpty() + category?: string; + + @JSONSchema({ + description: "Token type. Optional, but required if additionalKey is provided." + }) + @ValidateIf((o) => !!o.additionalKey) + @IsNotEmpty() + type?: string; + + @JSONSchema({ + description: "Token additionalKey. Optional, but required if instance is provided." + }) + @ValidateIf((o) => !!o.instance) + @IsNotEmpty() + additionalKey?: string; + + @JSONSchema({ + description: "Token instance. Optional, but required if project is provided." + }) + @ValidateIf((o) => !!o.project) + @IsNotEmpty() + instance?: string; + + @JSONSchema({ + description: "Project identifier. Optional." + }) + @IsOptional() + @IsNotEmpty() + project?: string; + + @JSONSchema({ + description: "Page bookmark. If it is undefined, then the first page is returned." + }) + @IsOptional() + @IsNotEmpty() + bookmark?: string; + + @JSONSchema({ + description: + `Page size limit. ` + + `Defaults to ${FetchTokenInstanceMetadataWithPaginationDto.DEFAULT_LIMIT}, max possible value ${FetchTokenInstanceMetadataWithPaginationDto.MAX_LIMIT}. ` + + "Note you will likely get less results than the limit, because the limit is applied before additional filtering." + }) + @IsOptional() + @Max(FetchTokenInstanceMetadataWithPaginationDto.MAX_LIMIT) + @Min(1) + @IsInt() + limit?: number; +} + +export class FetchTokenInstanceMetadataResponse extends ChainCallDTO { + @JSONSchema({ description: "List of token instance metadata documents." }) + @ValidateNested({ each: true }) + @Type(() => TokenInstanceMetadata) + results: TokenInstanceMetadata[]; + + @JSONSchema({ description: "Next page bookmark." }) + @IsOptional() + @IsString() + nextPageBookmark?: string; +} + +@JSONSchema({ + description: + "Deletes a project's metadata document of a single NFT token instance. Callable only by " + + "the owner of the project's metadata for the token class." +}) +export class DeleteTokenInstanceMetadataDto extends SubmitCallDTO { + @JSONSchema({ + description: "Key of the NFT token instance to delete the metadata of." + }) + @ValidateNested() + @Type(() => TokenInstanceKey) + @IsNotEmpty() + tokenInstance: TokenInstanceKey; + + @JSONSchema({ + description: "Identifier of the game or product whose metadata document should be deleted." + }) + @IsNotEmpty() + @MaxLength(200) + project: string; +} diff --git a/chain-api/src/types/index.ts b/chain-api/src/types/index.ts index 709a627c7a..889a0ca30f 100644 --- a/chain-api/src/types/index.ts +++ b/chain-api/src/types/index.ts @@ -40,6 +40,7 @@ export * from "./logger"; export * from "./PublicKey"; export * from "./UserProfile"; export * from "./TokenInstance"; +export * from "./TokenInstanceMetadata"; export * from "./TokenClass"; export * from "./token"; export * from "./common"; diff --git a/chain-api/src/validators/decorators.ts b/chain-api/src/validators/decorators.ts index f3a48c3349..2c8c2009c2 100644 --- a/chain-api/src/validators/decorators.ts +++ b/chain-api/src/validators/decorators.ts @@ -234,6 +234,32 @@ export function BigNumberIsInteger(validationOptions?: ValidationOptions) { }; } +export function IsStringOrNumber(maxStringLength?: number, validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + name: "IsStringOrNumber", + target: object.constructor, + propertyName, + constraints: [maxStringLength], + options: validationOptions, + validator: { + validate(value: unknown, args: ValidationArguments) { + const [maxLength] = args.constraints; + if (typeof value === "string") { + return maxLength === undefined || value.length <= maxLength; + } + return typeof value === "number" && Number.isFinite(value); + }, + defaultMessage(args: ValidationArguments) { + const [maxLength] = args.constraints; + const lengthConstraint = maxLength === undefined ? "" : ` no longer than ${maxLength} characters`; + return `${args.property} must be a finite number or a string${lengthConstraint}`; + } + } + }); + }; +} + /** * @description Decorator that conditionally includes/excludes a property during serialization/deserialization * based on a condition function that evaluates other properties of the object. diff --git a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap index 668932cb37..18371fd2db 100644 --- a/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/e2e/__snapshots__/api.spec.ts.snap @@ -3676,6 +3676,242 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, + { + "description": " Transaction updates the chain (submit). Allowed roles: SUBMIT.", + "dtoSchema": { + "description": "Deletes a project's metadata document of a single NFT token instance. Callable only by the owner of the project's metadata for the token class.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product whose metadata document should be deleted.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "tokenInstance": { + "description": "Key of the NFT token instance to delete the metadata of. Object representing the chain identifier of token instance.", + "minLength": 1, + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "tokenInstance", + "project", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "DeleteTokenInstanceMetadata", + "responseSchema": { + "properties": { + "Data": { + "description": "Object representing the chain identifier of token instance.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, { "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", "dtoSchema": { @@ -5854,7 +6090,7 @@ The key is generated by the caller and should be unique for each DTO. You can us { "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", "dtoSchema": { - "description": "Fetch vesting info including balances of allocations", + "description": "Fetches metadata documents of a single NFT token instance. If project is provided, fetches the document of that project only. Otherwise, fetches documents of all projects for the instance.", "properties": { "dtoExpiresAt": { "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", @@ -5876,6 +6112,11 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "project": { + "description": "Optional project identifier to fetch a single project's metadata document.", + "minLength": 1, + "type": "string", + }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -5892,8 +6133,9 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "tokenClasses": { - "description": "The Vested Token Class to be Fetched. Object representing the chain identifier of token class.", + "tokenInstance": { + "description": "Key of the NFT token instance to fetch the metadata of. Object representing the chain identifier of token instance.", + "minLength": 1, "properties": { "additionalKey": { "not": { @@ -5917,6 +6159,11 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, "multisig": { "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", "items": {}, @@ -5960,6 +6207,7 @@ The key is generated by the caller and should be unique for each DTO. You can us "category", "type", "additionalKey", + "instance", ], "type": "object", }, @@ -5971,25 +6219,809 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, "required": [ - "tokenClasses", + "tokenInstance", ], "type": "object", }, "isWrite": false, - "methodName": "FetchVestingTokens", + "methodName": "FetchTokenInstanceMetadata", "responseSchema": { "properties": { "Data": { - "properties": { - "allocationBalances": { - "items": { - "properties": { - "additionalKey": { - "not": { - "type": "null", - }, - }, - "category": { + "items": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", + ], + "type": "object", + }, + "type": "array", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", + "dtoSchema": { + "description": "Fetch token instance metadata documents currently available in world state. Supports filtering, pagination, and optionality of instance key properties.", + "properties": { + "additionalKey": { + "description": "Token additionalKey. Optional, but required if instance is provided.", + "minLength": 1, + "type": "string", + }, + "bookmark": { + "description": "Page bookmark. If it is undefined, then the first page is returned.", + "minLength": 1, + "type": "string", + }, + "category": { + "description": "Token category. Optional, but required if type is provided.", + "minLength": 1, + "type": "string", + }, + "collection": { + "description": "Token collection. Optional, but required if category is provided.", + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Token instance. Optional, but required if project is provided.", + "minLength": 1, + "type": "string", + }, + "limit": { + "description": "Page size limit. Defaults to 1000, max possible value 10000. Note you will likely get less results than the limit, because the limit is applied before additional filtering.", + "maximum": 10000, + "minimum": 1, + "type": "number", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "project": { + "description": "Project identifier. Optional.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "description": "Token type. Optional, but required if additionalKey is provided.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "isWrite": false, + "methodName": "FetchTokenInstanceMetadataWithPagination", + "responseSchema": { + "properties": { + "Data": { + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "nextPageBookmark": { + "description": "Next page bookmark.", + "type": "string", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "results": { + "description": "List of token instance metadata documents.", + "items": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", + ], + "type": "object", + }, + "type": "array", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "results", + ], + "type": "object", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", + "dtoSchema": { + "description": "Fetch vesting info including balances of allocations", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "tokenClasses": { + "description": "The Vested Token Class to be Fetched. Object representing the chain identifier of token class.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "tokenClasses", + ], + "type": "object", + }, + "isWrite": false, + "methodName": "FetchVestingTokens", + "responseSchema": { + "properties": { + "Data": { + "properties": { + "allocationBalances": { + "items": { + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { "minLength": 1, "type": "string", }, @@ -9623,8 +10655,192 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "from": { - "description": "The current owner of tokens. If the value is missing, chaincode caller is used.", + "from": { + "description": "The current owner of tokens. If the value is missing, chaincode caller is used.", + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "quantity": { + "description": "The quantity of token units to be transferred. Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "to": { + "description": "Allowed value is a user alias ('client|', or 'eth|', or valid system-level username), or valid Ethereum address.", + "type": "string", + }, + "tokenInstance": { + "description": "Token instance of token to be transferred. In case of fungible tokens, tokenInstance.instance field should be set to 0. Object representing the chain identifier of token instance.", + "minLength": 1, + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "to", + "tokenInstance", + "quantity", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "RequestTransferToken", + "responseSchema": { + "properties": { + "Data": { + "type": "null", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction updates the chain (submit). Allowed roles: CURATOR.", + "dtoSchema": { + "description": "Configure GALA token properties on chain for use with supporting chaincalls.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, "type": "string", }, "multisig": { @@ -9638,11 +10854,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "quantity": { - "description": "The quantity of token units to be transferred. Number provided as a string.", - "minLength": 1, - "type": "string", - }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -9659,13 +10870,31 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "to": { - "description": "Allowed value is a user alias ('client|', or 'eth|', or valid system-level username), or valid Ethereum address.", + "type": { + "minLength": 1, "type": "string", }, - "tokenInstance": { - "description": "Token instance of token to be transferred. In case of fungible tokens, tokenInstance.instance field should be set to 0. Object representing the chain identifier of token instance.", + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "SetFeeProperties", + "responseSchema": { + "properties": { + "Data": { "properties": { "additionalKey": { "not": { @@ -9680,12 +10909,7 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "dtoExpiresAt": { - "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", - "type": "number", - }, - "dtoOperation": { - "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "id": { "minLength": 1, "type": "string", }, @@ -9694,45 +10918,13 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "multisig": { - "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", - "items": {}, - "minItems": 2, - "type": "array", - }, - "prefix": { - "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", - "minLength": 1, - "type": "string", - }, - "signature": { - "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. -Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", - "minLength": 1, - "type": "string", - }, - "signerAddress": { - "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", - "minLength": 1, - "type": "string", - }, - "signerPublicKey": { - "description": "Public key of the user who signed the DTO.", - "minLength": 1, - "type": "string", - }, "type": { "minLength": 1, "type": "string", }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, }, "required": [ + "id", "collection", "category", "type", @@ -9741,27 +10933,6 @@ The key is generated by the caller and should be unique for each DTO. You can us ], "type": "object", }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, - }, - "required": [ - "to", - "tokenInstance", - "quantity", - ], - "type": "object", - }, - "isWrite": true, - "methodName": "RequestTransferToken", - "responseSchema": { - "properties": { - "Data": { - "type": "null", - }, "Message": { "type": "string", }, @@ -9780,21 +10951,165 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": " Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": " Transaction updates the chain (submit). Allowed roles: SUBMIT.", "dtoSchema": { - "description": "Configure GALA token properties on chain for use with supporting chaincalls.", + "description": "Full-document upsert of a project's metadata for a single NFT token instance. The first user to create metadata for a given project on a token class becomes the owner of that project's metadata for the class; only the owner can update it afterwards. Replaces any existing metadata document of the project for the instance.", "properties": { - "additionalKey": { - "not": { - "type": "null", + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "description": "Traits following the OpenSea metadata standard attribute format.", + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", }, + "type": "array", }, - "category": { - "minLength": 1, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, "type": "string", }, - "collection": { - "minLength": 1, + "customFields": { + "description": "Arbitrary key-value entries for game or product specific metadata.", + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, "type": "string", }, "dtoExpiresAt": { @@ -9806,9 +11121,12 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "instance": { - "description": "Number provided as a string.", - "minLength": 1, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, "type": "string", }, "multisig": { @@ -9817,11 +11135,21 @@ The key is generated by the caller and should be unique for each DTO. You can us "minItems": 2, "type": "array", }, + "name": { + "maxLength": 200, + "type": "string", + }, "prefix": { "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", "minLength": 1, "type": "string", }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -9838,9 +11166,83 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "type": { + "tokenInstance": { + "description": "Key of the NFT token instance to attach the metadata to. Object representing the chain identifier of token instance.", "minLength": 1, - "type": "string", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", }, "uniqueKey": { "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. @@ -9848,27 +11250,115 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, }, "required": [ - "collection", - "category", - "type", - "additionalKey", - "instance", + "tokenInstance", + "project", ], "type": "object", }, "isWrite": true, - "methodName": "SetFeeProperties", + "methodName": "SetTokenInstanceMetadata", "responseSchema": { "properties": { "Data": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", "properties": { "additionalKey": { "not": { "type": "null", }, }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, "category": { "minLength": 1, "type": "string", @@ -9877,8 +11367,89 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "id": { - "minLength": 1, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, "type": "string", }, "instance": { @@ -9886,18 +11457,44 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, "type": { "minLength": 1, "type": "string", }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, }, "required": [ - "id", "collection", "category", "type", "additionalKey", "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", ], "type": "object", }, diff --git a/chain-cli/chaincode-template/e2e/tokenInstanceMetadata.spec.ts b/chain-cli/chaincode-template/e2e/tokenInstanceMetadata.spec.ts new file mode 100644 index 0000000000..8cc78a6169 --- /dev/null +++ b/chain-cli/chaincode-template/e2e/tokenInstanceMetadata.spec.ts @@ -0,0 +1,264 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + AllowanceType, + ChainUser, + CreateTokenClassDto, + DeleteTokenInstanceMetadataDto, + FetchTokenInstanceMetadataDto, + GrantAllowanceDto, + MintTokenDto, + SetTokenInstanceMetadataDto, + TokenClassKey, + TokenInstance, + TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataAttribute, + TokenInstanceMetadataCustomField, + createValidDTO, + createValidSubmitDTO +} from "@gala-chain/api"; +import { + AdminChainClients, + TestClients, + randomize, + transactionErrorCode, + transactionSuccess +} from "@gala-chain/test"; +import BigNumber from "bignumber.js"; +import { plainToInstance } from "class-transformer"; + +jest.setTimeout(30000); + +describe("Token instance metadata scenario", () => { + let client: AdminChainClients; + let user1: ChainUser; + let user2: ChainUser; + + const project = "project-alpha"; + + beforeAll(async () => { + client = await TestClients.createForAdmin(); + user1 = ChainUser.withRandomKeys(); + user2 = ChainUser.withRandomKeys(); + }); + + afterAll(async () => { + await client.disconnect(); + }); + + const nftClassKey: TokenClassKey = plainToInstance(TokenClassKey, { + collection: randomize("NFT"), + category: "Weapon", + type: "Axe", + additionalKey: "none" + }); + + const nftInstanceKey = () => TokenInstanceKey.nftKey(nftClassKey, 1); + + it("Curator should create NFT Class and mint an instance", async () => { + // Given + const createClassDto = await createValidSubmitDTO(CreateTokenClassDto, { + decimals: 0, + tokenClass: nftClassKey, + name: nftClassKey.collection, + symbol: nftClassKey.collection.slice(0, 20), + description: "This is a test description!", + isNonFungible: true, + image: "https://app.gala.games/_nuxt/img/gala-logo_horizontal_white.8b0409c.png", + maxSupply: new BigNumber(10) + }); + + const allowanceDto = await createValidSubmitDTO(GrantAllowanceDto, { + tokenInstance: TokenInstanceKey.nftKey(nftClassKey, TokenInstance.FUNGIBLE_TOKEN_INSTANCE).toQueryKey(), + allowanceType: AllowanceType.Mint, + quantities: [{ user: user1.identityKey, quantity: new BigNumber(1) }], + uses: new BigNumber(10) + }); + + const mintDto = await createValidSubmitDTO(MintTokenDto, { + owner: user1.identityKey, + tokenClass: nftClassKey, + quantity: new BigNumber(1) + }); + + // When + const createClassResponse = await client.assets.submitTransaction( + "CreateTokenClass", + createClassDto.signed(client.assets.privateKey), + TokenClassKey + ); + const allowanceResponse = await client.assets.submitTransaction( + "GrantAllowance", + allowanceDto.signed(client.assets.privateKey) + ); + const mintResponse = await client.assets.submitTransaction("MintToken", mintDto.signed(user1.privateKey)); + + // Then + expect(createClassResponse).toEqual(transactionSuccess(nftClassKey)); + expect(allowanceResponse).toEqual(transactionSuccess()); + expect(mintResponse).toEqual(transactionSuccess()); + }); + + it("User should set token instance metadata for their project", async () => { + // Given + const setDto = await createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project, + name: "Legendary Axe #1", + description: "An axe of legend.", + image: "https://app.gala.games/test-image-placeholder-url.png", + attributes: [ + plainToInstance(TokenInstanceMetadataAttribute, { + trait_type: "Sharpness", + value: 10, + display_type: "number" + }) + ], + customFields: [plainToInstance(TokenInstanceMetadataCustomField, { key: "gameId", value: "axe-001" })] + }); + + // When + const response = await client.assets.submitTransaction( + "SetTokenInstanceMetadata", + setDto.signed(user1.privateKey), + TokenInstanceMetadata + ); + + // Then + expect(response).toEqual( + transactionSuccess( + expect.objectContaining({ + project, + name: "Legendary Axe #1", + attributes: [expect.objectContaining({ trait_type: "Sharpness", value: 10 })] + }) + ) + ); + }); + + it("Another user should not modify the project's metadata", async () => { + // Given + const setDto = await createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project, + name: "Hijacked Axe" + }); + + // When + const response = await client.assets.submitTransaction( + "SetTokenInstanceMetadata", + setDto.signed(user2.privateKey) + ); + + // Then + expect(response).toEqual(transactionErrorCode(403)); + }); + + it("Another user should set metadata for a different project on the same instance", async () => { + // Given + const setDto = await createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project: "project-beta", + name: "Beta View of the Axe" + }); + + // When + const response = await client.assets.submitTransaction( + "SetTokenInstanceMetadata", + setDto.signed(user2.privateKey), + TokenInstanceMetadata + ); + + // Then + expect(response).toEqual( + transactionSuccess(expect.objectContaining({ project: "project-beta", name: "Beta View of the Axe" })) + ); + }); + + it("Anyone should fetch metadata of all projects for the instance", async () => { + // Given + const fetchDto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey() + }); + + // When + const response = await client.assets.evaluateTransaction( + "FetchTokenInstanceMetadata", + fetchDto, + TokenInstanceMetadata + ); + + // Then + expect(response).toEqual( + transactionSuccess( + expect.arrayContaining([ + expect.objectContaining({ project, name: "Legendary Axe #1" }), + expect.objectContaining({ project: "project-beta" }) + ]) + ) + ); + }); + + it("Project owner should replace metadata as a full document", async () => { + // Given + const replaceDto = await createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project, + name: "Renamed Axe" + }); + + // When + const response = await client.assets.submitTransaction( + "SetTokenInstanceMetadata", + replaceDto.signed(user1.privateKey), + TokenInstanceMetadata + ); + + // Then + const metadata = response.Data as TokenInstanceMetadata; + expect(response).toEqual(transactionSuccess(expect.objectContaining({ name: "Renamed Axe" }))); + expect(metadata.description).toBeUndefined(); + expect(metadata.attributes).toBeUndefined(); + }); + + it("Project owner should delete metadata, and project fetch should fail afterwards", async () => { + // Given + const deleteDto = await createValidSubmitDTO(DeleteTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project + }); + + const fetchDto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: nftInstanceKey(), + project + }); + + // When + const deleteResponse = await client.assets.submitTransaction( + "DeleteTokenInstanceMetadata", + deleteDto.signed(user1.privateKey) + ); + const fetchResponse = await client.assets.evaluateTransaction( + "FetchTokenInstanceMetadata", + fetchDto, + TokenInstanceMetadata + ); + + // Then + expect(deleteResponse).toEqual(transactionSuccess()); + expect(fetchResponse).toEqual(transactionErrorCode(404)); + }); +}); diff --git a/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts b/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts index 2676c7fd6c..8f3bb4b47b 100644 --- a/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts +++ b/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts @@ -19,6 +19,7 @@ import { CreateTokenClassDto, CreateVestingTokenDto, DeleteAllowancesDto, + DeleteTokenInstanceMetadataDto, FeeAuthorizationResDto, FeeCodeDefinition, FeeCodeDefinitionDto, @@ -43,6 +44,9 @@ import { FetchTokenClassesDto, FetchTokenClassesResponse, FetchTokenClassesWithPaginationDto, + FetchTokenInstanceMetadataDto, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationDto, FetchVestingTokenDto, FulfillMintDto, FullAllowanceCheckDto, @@ -56,6 +60,7 @@ import { MintTokenDto, MintTokenWithAllowanceDto, RefreshAllowancesDto, + SetTokenInstanceMetadataDto, SubmitCallDTO, TokenAllowance, TokenBalance, @@ -63,6 +68,7 @@ import { TokenClass, TokenClassKey, TokenInstanceKey, + TokenInstanceMetadata, TransferTokenDto, UnlockTokenDto, UnlockTokensDto, @@ -89,6 +95,7 @@ import { defineFeeSchedule, defineFeeSplitFormula, deleteAllowances, + deleteTokenInstanceMetadata, fetchAllowancesWithPagination, fetchBalances, fetchBalancesWithTokenMetadata, @@ -98,6 +105,8 @@ import { fetchFeeThresholdUsesWithPagination, fetchTokenClasses, fetchTokenClassesWithPagination, + fetchTokenInstanceMetadata, + fetchTokenInstanceMetadataWithPagination, fetchVestingToken, fulfillMintRequest, fullAllowanceCheck, @@ -115,6 +124,7 @@ import { resolveUserAlias, saveRequest, setGalaFeeProperties, + setTokenInstanceMetadata, transferToken, transferTokenFeeGate, unlockToken, @@ -202,6 +212,67 @@ export default class GalaChainTokenContract extends GalaContract { return fetchTokenClassesWithPagination(ctx, dto); } + @Submit({ + in: SetTokenInstanceMetadataDto, + out: TokenInstanceMetadata + }) + public SetTokenInstanceMetadata( + ctx: GalaChainContext, + dto: SetTokenInstanceMetadataDto + ): Promise { + return setTokenInstanceMetadata(ctx, { + tokenInstance: dto.tokenInstance, + project: dto.project, + name: dto.name, + description: dto.description, + image: dto.image, + external_url: dto.external_url, + animation_url: dto.animation_url, + background_color: dto.background_color, + youtube_url: dto.youtube_url, + attributes: dto.attributes, + customFields: dto.customFields + }); + } + + @GalaTransaction({ + type: EVALUATE, + in: FetchTokenInstanceMetadataDto, + out: { arrayOf: TokenInstanceMetadata } + }) + public FetchTokenInstanceMetadata( + ctx: GalaChainContext, + dto: FetchTokenInstanceMetadataDto + ): Promise { + return fetchTokenInstanceMetadata(ctx, { + tokenInstance: dto.tokenInstance, + project: dto.project + }); + } + + @GalaTransaction({ + type: EVALUATE, + in: FetchTokenInstanceMetadataWithPaginationDto, + out: FetchTokenInstanceMetadataResponse + }) + public FetchTokenInstanceMetadataWithPagination( + ctx: GalaChainContext, + dto: FetchTokenInstanceMetadataWithPaginationDto + ): Promise { + return fetchTokenInstanceMetadataWithPagination(ctx, dto); + } + + @Submit({ + in: DeleteTokenInstanceMetadataDto, + out: TokenInstanceKey + }) + public DeleteTokenInstanceMetadata( + ctx: GalaChainContext, + dto: DeleteTokenInstanceMetadataDto + ): Promise { + return deleteTokenInstanceMetadata(ctx, { tokenInstance: dto.tokenInstance, project: dto.project }); + } + @Submit({ in: GrantAllowanceDto, out: { arrayOf: TokenAllowance } diff --git a/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap b/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap index 2db28cac25..a69e7ed622 100644 --- a/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap +++ b/chain-cli/chaincode-template/src/token/__snapshots__/api.spec.ts.snap @@ -2347,6 +2347,242 @@ The key is generated by the caller and should be unique for each DTO. You can us "type": "object", }, }, + { + "description": " Transaction updates the chain (submit). Allowed roles: SUBMIT.", + "dtoSchema": { + "description": "Deletes a project's metadata document of a single NFT token instance. Callable only by the owner of the project's metadata for the token class.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product whose metadata document should be deleted.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "tokenInstance": { + "description": "Key of the NFT token instance to delete the metadata of. Object representing the chain identifier of token instance.", + "minLength": 1, + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "tokenInstance", + "project", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "DeleteTokenInstanceMetadata", + "responseSchema": { + "properties": { + "Data": { + "description": "Object representing the chain identifier of token instance.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, { "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", "dtoSchema": { @@ -4525,7 +4761,7 @@ The key is generated by the caller and should be unique for each DTO. You can us { "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", "dtoSchema": { - "description": "Fetch vesting info including balances of allocations", + "description": "Fetches metadata documents of a single NFT token instance. If project is provided, fetches the document of that project only. Otherwise, fetches documents of all projects for the instance.", "properties": { "dtoExpiresAt": { "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", @@ -4547,6 +4783,11 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "project": { + "description": "Optional project identifier to fetch a single project's metadata document.", + "minLength": 1, + "type": "string", + }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -4563,8 +4804,9 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "tokenClasses": { - "description": "The Vested Token Class to be Fetched. Object representing the chain identifier of token class.", + "tokenInstance": { + "description": "Key of the NFT token instance to fetch the metadata of. Object representing the chain identifier of token instance.", + "minLength": 1, "properties": { "additionalKey": { "not": { @@ -4588,6 +4830,11 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, "multisig": { "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", "items": {}, @@ -4631,6 +4878,7 @@ The key is generated by the caller and should be unique for each DTO. You can us "category", "type", "additionalKey", + "instance", ], "type": "object", }, @@ -4642,25 +4890,809 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, "required": [ - "tokenClasses", + "tokenInstance", ], "type": "object", }, "isWrite": false, - "methodName": "FetchVestingTokens", + "methodName": "FetchTokenInstanceMetadata", "responseSchema": { "properties": { "Data": { - "properties": { - "allocationBalances": { - "items": { - "properties": { - "additionalKey": { - "not": { - "type": "null", - }, - }, - "category": { + "items": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", + ], + "type": "object", + }, + "type": "array", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", + "dtoSchema": { + "description": "Fetch token instance metadata documents currently available in world state. Supports filtering, pagination, and optionality of instance key properties.", + "properties": { + "additionalKey": { + "description": "Token additionalKey. Optional, but required if instance is provided.", + "minLength": 1, + "type": "string", + }, + "bookmark": { + "description": "Page bookmark. If it is undefined, then the first page is returned.", + "minLength": 1, + "type": "string", + }, + "category": { + "description": "Token category. Optional, but required if type is provided.", + "minLength": 1, + "type": "string", + }, + "collection": { + "description": "Token collection. Optional, but required if category is provided.", + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Token instance. Optional, but required if project is provided.", + "minLength": 1, + "type": "string", + }, + "limit": { + "description": "Page size limit. Defaults to 1000, max possible value 10000. Note you will likely get less results than the limit, because the limit is applied before additional filtering.", + "maximum": 10000, + "minimum": 1, + "type": "number", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "project": { + "description": "Project identifier. Optional.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "description": "Token type. Optional, but required if additionalKey is provided.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "type": "object", + }, + "isWrite": false, + "methodName": "FetchTokenInstanceMetadataWithPagination", + "responseSchema": { + "properties": { + "Data": { + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "nextPageBookmark": { + "description": "Next page bookmark.", + "type": "string", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "results": { + "description": "List of token instance metadata documents.", + "items": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", + ], + "type": "object", + }, + "type": "array", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "results", + ], + "type": "object", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction is read only (evaluate). Allowed roles: EVALUATE.", + "dtoSchema": { + "description": "Fetch vesting info including balances of allocations", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "tokenClasses": { + "description": "The Vested Token Class to be Fetched. Object representing the chain identifier of token class.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "tokenClasses", + ], + "type": "object", + }, + "isWrite": false, + "methodName": "FetchVestingTokens", + "responseSchema": { + "properties": { + "Data": { + "properties": { + "allocationBalances": { + "items": { + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { "minLength": 1, "type": "string", }, @@ -8294,8 +9326,192 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "from": { - "description": "The current owner of tokens. If the value is missing, chaincode caller is used.", + "from": { + "description": "The current owner of tokens. If the value is missing, chaincode caller is used.", + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "quantity": { + "description": "The quantity of token units to be transferred. Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "to": { + "description": "Allowed value is a user alias ('client|', or 'eth|', or valid system-level username), or valid Ethereum address.", + "type": "string", + }, + "tokenInstance": { + "description": "Token instance of token to be transferred. In case of fungible tokens, tokenInstance.instance field should be set to 0. Object representing the chain identifier of token instance.", + "minLength": 1, + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "to", + "tokenInstance", + "quantity", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "RequestTransferToken", + "responseSchema": { + "properties": { + "Data": { + "type": "null", + }, + "Message": { + "type": "string", + }, + "Status": { + "description": "Indicates Error (0) or Success (1)", + "enum": [ + 0, + 1, + ], + }, + }, + "required": [ + "Status", + ], + "type": "object", + }, + }, + { + "description": " Transaction updates the chain (submit). Allowed roles: CURATOR.", + "dtoSchema": { + "description": "Configure GALA token properties on chain for use with supporting chaincalls.", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, "type": "string", }, "multisig": { @@ -8309,11 +9525,6 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "quantity": { - "description": "The quantity of token units to be transferred. Number provided as a string.", - "minLength": 1, - "type": "string", - }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -8330,13 +9541,31 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "to": { - "description": "Allowed value is a user alias ('client|', or 'eth|', or valid system-level username), or valid Ethereum address.", + "type": { + "minLength": 1, "type": "string", }, - "tokenInstance": { - "description": "Token instance of token to be transferred. In case of fungible tokens, tokenInstance.instance field should be set to 0. Object representing the chain identifier of token instance.", + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", + }, + "isWrite": true, + "methodName": "SetFeeProperties", + "responseSchema": { + "properties": { + "Data": { "properties": { "additionalKey": { "not": { @@ -8351,12 +9580,7 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "dtoExpiresAt": { - "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", - "type": "number", - }, - "dtoOperation": { - "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "id": { "minLength": 1, "type": "string", }, @@ -8365,45 +9589,13 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "multisig": { - "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", - "items": {}, - "minItems": 2, - "type": "array", - }, - "prefix": { - "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", - "minLength": 1, - "type": "string", - }, - "signature": { - "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. -Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", - "minLength": 1, - "type": "string", - }, - "signerAddress": { - "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", - "minLength": 1, - "type": "string", - }, - "signerPublicKey": { - "description": "Public key of the user who signed the DTO.", - "minLength": 1, - "type": "string", - }, "type": { "minLength": 1, "type": "string", }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, }, "required": [ + "id", "collection", "category", "type", @@ -8412,27 +9604,6 @@ The key is generated by the caller and should be unique for each DTO. You can us ], "type": "object", }, - "uniqueKey": { - "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. -The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", - "minLength": 1, - "type": "string", - }, - }, - "required": [ - "to", - "tokenInstance", - "quantity", - ], - "type": "object", - }, - "isWrite": true, - "methodName": "RequestTransferToken", - "responseSchema": { - "properties": { - "Data": { - "type": "null", - }, "Message": { "type": "string", }, @@ -8451,21 +9622,165 @@ The key is generated by the caller and should be unique for each DTO. You can us }, }, { - "description": " Transaction updates the chain (submit). Allowed roles: CURATOR.", + "description": " Transaction updates the chain (submit). Allowed roles: SUBMIT.", "dtoSchema": { - "description": "Configure GALA token properties on chain for use with supporting chaincalls.", + "description": "Full-document upsert of a project's metadata for a single NFT token instance. The first user to create metadata for a given project on a token class becomes the owner of that project's metadata for the class; only the owner can update it afterwards. Replaces any existing metadata document of the project for the instance.", "properties": { - "additionalKey": { - "not": { - "type": "null", + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "description": "Traits following the OpenSea metadata standard attribute format.", + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", }, + "type": "array", }, - "category": { - "minLength": 1, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, "type": "string", }, - "collection": { - "minLength": 1, + "customFields": { + "description": "Arbitrary key-value entries for game or product specific metadata.", + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, "type": "string", }, "dtoExpiresAt": { @@ -8477,9 +9792,12 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "instance": { - "description": "Number provided as a string.", - "minLength": 1, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, "type": "string", }, "multisig": { @@ -8488,11 +9806,21 @@ The key is generated by the caller and should be unique for each DTO. You can us "minItems": 2, "type": "array", }, + "name": { + "maxLength": 200, + "type": "string", + }, "prefix": { "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", "minLength": 1, "type": "string", }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, "signature": { "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", @@ -8509,9 +9837,83 @@ Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/bl "minLength": 1, "type": "string", }, - "type": { + "tokenInstance": { + "description": "Key of the NFT token instance to attach the metadata to. Object representing the chain identifier of token instance.", "minLength": 1, - "type": "string", + "properties": { + "additionalKey": { + "not": { + "type": "null", + }, + }, + "category": { + "minLength": 1, + "type": "string", + }, + "collection": { + "minLength": 1, + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "instance": { + "description": "Number provided as a string.", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "type": { + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "collection", + "category", + "type", + "additionalKey", + "instance", + ], + "type": "object", }, "uniqueKey": { "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. @@ -8519,27 +9921,115 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, }, "required": [ - "collection", - "category", - "type", - "additionalKey", - "instance", + "tokenInstance", + "project", ], "type": "object", }, "isWrite": true, - "methodName": "SetFeeProperties", + "methodName": "SetTokenInstanceMetadata", "responseSchema": { "properties": { "Data": { + "description": "Metadata document for a single NFT token instance, scoped to a project. A token instance may have multiple metadata documents, one per project. Field names follow the OpenSea metadata standard (snake_case) for direct off-chain format parity.", "properties": { "additionalKey": { "not": { "type": "null", }, }, + "animation_url": { + "maxLength": 500, + "type": "string", + }, + "attributes": { + "items": { + "description": "Single trait of a token instance, following the OpenSea metadata standard attribute format.", + "properties": { + "display_type": { + "description": "Optional OpenSea display type for numeric traits: number, boost_number, boost_percentage, date.", + "enum": [ + "number", + "boost_number", + "boost_percentage", + "date", + ], + "type": "string", + }, + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "trait_type": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "description": "Trait value. Either a string or a finite number (strings limited to 500 characters).", + "not": { + "type": "null", + }, + "type": "object", + }, + }, + "required": [ + "trait_type", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "background_color": { + "description": "Background color as a six-character hexadecimal without a pre-pended #.", + "maxLength": 6, + "type": "string", + }, "category": { "minLength": 1, "type": "string", @@ -8548,8 +10038,89 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, - "id": { - "minLength": 1, + "created": { + "exclusiveMinimum": 0, + "type": "number", + }, + "createdBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "customFields": { + "items": { + "description": "Arbitrary key-value entry for game or product specific token instance metadata that does not fit the OpenSea standard fields.", + "properties": { + "dtoExpiresAt": { + "description": "Unix epoch timestamp in milliseconds (ms) when the DTO expires. If this time is before the current time, the DTO is not valid.", + "type": "number", + }, + "dtoOperation": { + "description": "Full operation identifier that is called on chain with this DTO. The format is \`channelId_chaincodeId_methodName\`. It is required for multisig DTOs, and optional for single signed DTOs. ", + "minLength": 1, + "type": "string", + }, + "key": { + "maxLength": 200, + "minLength": 1, + "type": "string", + }, + "multisig": { + "description": "List of signatures for this DTO if there are multiple signers. If there are multiple signatures, 'signerAddress' is required, and it is not allowed to provide 'signature' or 'signerPublicKey' fields, and the signing scheme must be ETH.", + "items": {}, + "minItems": 2, + "type": "array", + }, + "prefix": { + "description": "Prefix for Metamask transaction signatures. Necessary to format payloads correctly to recover publicKey from web3 signatures.", + "minLength": 1, + "type": "string", + }, + "signature": { + "description": "Signature of the DTO signed with caller's private key to be verified with user's public key saved on chain. The 'signature' field is optional for DTO, but is required for a transaction to be executed on chain. +Please consult [GalaChain SDK documentation](https://github.com/GalaChain/sdk/blob/main/docs/authorization.md#signature-based-authorization) on how to create signatures.", + "minLength": 1, + "type": "string", + }, + "signerAddress": { + "description": "Address of the user who signed the DTO. Typically Ethereum address, or user alias.", + "minLength": 1, + "type": "string", + }, + "signerPublicKey": { + "description": "Public key of the user who signed the DTO.", + "minLength": 1, + "type": "string", + }, + "uniqueKey": { + "description": "Unique key of the DTO. It is used to prevent double execution of the same transaction on chain. The key is saved on chain and checked before execution. If a DTO with already saved key is used in transaction, the transaction will fail with UniqueTransactionConflict error, which is mapped to HTTP 409 Conflict error. In case of the error, no changes are saved to chain state. +The key is generated by the caller and should be unique for each DTO. You can use \`nanoid\` library, UUID scheme, or any tool to generate unique string keys.", + "minLength": 1, + "type": "string", + }, + "value": { + "maxLength": 2000, + "minLength": 1, + "type": "string", + }, + }, + "required": [ + "key", + "value", + ], + "type": "object", + }, + "type": "array", + }, + "description": { + "maxLength": 1000, + "type": "string", + }, + "external_url": { + "maxLength": 500, + "type": "string", + }, + "image": { + "maxLength": 500, "type": "string", }, "instance": { @@ -8557,18 +10128,44 @@ The key is generated by the caller and should be unique for each DTO. You can us "minLength": 1, "type": "string", }, + "lastModified": { + "exclusiveMinimum": 0, + "type": "number", + }, + "lastModifiedBy": { + "description": "Allowed value is string following the format of 'client|', or 'eth|', or valid system-level username.", + "type": "string", + }, + "name": { + "maxLength": 200, + "type": "string", + }, + "project": { + "description": "Identifier of the game or product this metadata document belongs to.", + "maxLength": 200, + "minLength": 1, + "type": "string", + }, "type": { "minLength": 1, "type": "string", }, + "youtube_url": { + "maxLength": 500, + "type": "string", + }, }, "required": [ - "id", "collection", "category", "type", "additionalKey", "instance", + "project", + "createdBy", + "lastModifiedBy", + "created", + "lastModified", ], "type": "object", }, diff --git a/chain-connect/src/chainApis/TokenApi.ts b/chain-connect/src/chainApis/TokenApi.ts index 0476a29177..23517cf6e8 100644 --- a/chain-connect/src/chainApis/TokenApi.ts +++ b/chain-connect/src/chainApis/TokenApi.ts @@ -17,6 +17,7 @@ import { BurnTokensDto, CreateTokenClassDto, DeleteAllowancesDto, + DeleteTokenInstanceMetadataDto, FetchAllowancesDto, FetchBalancesDto, FetchBalancesWithPaginationDto, @@ -24,6 +25,8 @@ import { FetchMintRequestsDto, FetchTokenClassesDto, FetchTokenClassesWithPaginationDto, + FetchTokenInstanceMetadataDto, + FetchTokenInstanceMetadataWithPaginationDto, FulfillMintDto, FullAllowanceCheckDto, GrantAllowanceDto, @@ -34,6 +37,7 @@ import { MintTokenDto, MintTokenWithAllowanceDto, RefreshAllowanceDto, + SetTokenInstanceMetadataDto, TransferTokenDto, UnlockTokenDto, UnlockTokensDto, @@ -46,6 +50,7 @@ import { BurnTokensRequest, CreateTokenClassRequest, DeleteAllowancesRequest, + DeleteTokenInstanceMetadataRequest, FetchAllowancesRequest, FetchAllowancesResponse, FetchBalancesRequest, @@ -56,6 +61,9 @@ import { FetchTokenClassesRequest, FetchTokenClassesResponse, FetchTokenClassesWithPaginationRequest, + FetchTokenInstanceMetadataRequest, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationRequest, FulfillMintRequest, FullAllowanceCheckRequest, FullAllowanceCheckResponse, @@ -68,12 +76,14 @@ import { MintTokenRequest, MintTokenWithAllowanceRequest, RefreshAllowanceRequest, + SetTokenInstanceMetadataRequest, TokenAllowance, TokenBalance, TokenBurn, TokenClass, TokenClassKey, TokenInstanceKey, + TokenInstanceMetadata, TransferTokenRequest, UnlockTokenRequest, UnlockTokensRequest, @@ -150,6 +160,48 @@ export class TokenApi extends GalaChainBaseApi { }); } + public SetTokenInstanceMetadata(dto: SetTokenInstanceMetadataRequest) { + return this.connection.submit({ + method: "SetTokenInstanceMetadata", + payload: dto, + sign: true, + url: this.chainCodeUrl, + requestConstructor: SetTokenInstanceMetadataDto, + responseConstructor: TokenInstanceMetadata + }); + } + + public FetchTokenInstanceMetadata(dto: FetchTokenInstanceMetadataRequest) { + return this.connection.submit, FetchTokenInstanceMetadataDto>({ + method: "FetchTokenInstanceMetadata", + payload: dto, + url: this.chainCodeUrl, + requestConstructor: FetchTokenInstanceMetadataDto, + responseConstructor: TokenInstanceMetadata + }); + } + + public FetchTokenInstanceMetadataWithPagination(dto: FetchTokenInstanceMetadataWithPaginationRequest) { + return this.connection.submit({ + method: "FetchTokenInstanceMetadataWithPagination", + payload: dto, + url: this.chainCodeUrl, + requestConstructor: FetchTokenInstanceMetadataWithPaginationDto, + responseConstructor: FetchTokenInstanceMetadataResponse + }); + } + + public DeleteTokenInstanceMetadata(dto: DeleteTokenInstanceMetadataRequest) { + return this.connection.submit({ + method: "DeleteTokenInstanceMetadata", + payload: dto, + sign: true, + url: this.chainCodeUrl, + requestConstructor: DeleteTokenInstanceMetadataDto, + responseConstructor: TokenInstanceKey + }); + } + public GrantAllowance(dto: GrantAllowanceRequest) { return this.connection.submit, GrantAllowanceDto>({ method: "GrantAllowance", diff --git a/chain-connect/src/types/tokenApi.ts b/chain-connect/src/types/tokenApi.ts index 600f5c5d04..8045d87fcc 100644 --- a/chain-connect/src/types/tokenApi.ts +++ b/chain-connect/src/types/tokenApi.ts @@ -20,6 +20,7 @@ import { BurnTokensDto, CreateTokenClassDto, DeleteAllowancesDto, + DeleteTokenInstanceMetadataDto, FetchAllowancesDto, FetchAllowancesResponse, FetchBalancesDto, @@ -30,6 +31,9 @@ import { FetchTokenClassesDto, FetchTokenClassesResponse, FetchTokenClassesWithPaginationDto, + FetchTokenInstanceMetadataDto, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationDto, FulfillMintDto, FullAllowanceCheckDto, FullAllowanceCheckResDto as FullAllowanceCheckResponse, @@ -43,6 +47,7 @@ import { MintTokenDto, MintTokenWithAllowanceDto, RefreshAllowanceDto, + SetTokenInstanceMetadataDto, TokenAllowance, TokenBalance as TokenBalanceDto, TokenBalanceWithMetadata, @@ -51,6 +56,8 @@ import { TokenClassKey, TokenHold, TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataProject, TransferTokenDto, UnlockTokenDto, UnlockTokensDto, @@ -68,6 +75,7 @@ type BatchMintTokenRequest = ConstructorArgs; type BurnTokensRequest = ConstructorArgs; type CreateTokenClassRequest = ConstructorArgs; type DeleteAllowancesRequest = ConstructorArgs; +type DeleteTokenInstanceMetadataRequest = ConstructorArgs; type FetchAllowancesRequest = ConstructorArgs; type FetchBalancesRequest = ConstructorArgs; type FetchBalancesWithPaginationRequest = ConstructorArgs; @@ -75,6 +83,9 @@ type FetchBurnsRequest = ConstructorArgs; type FetchMintRequestsRequest = ConstructorArgs; type FetchTokenClassesRequest = ConstructorArgs; type FetchTokenClassesWithPaginationRequest = ConstructorArgs; +type FetchTokenInstanceMetadataRequest = ConstructorArgs; +type FetchTokenInstanceMetadataWithPaginationRequest = + ConstructorArgs; type FulfillMintRequest = ConstructorArgs; type FullAllowanceCheckRequest = ConstructorArgs; type GrantAllowanceRequest = ConstructorArgs; @@ -85,6 +96,7 @@ type MintRequest = ConstructorArgs; type MintTokenRequest = ConstructorArgs; type MintTokenWithAllowanceRequest = ConstructorArgs; type RefreshAllowanceRequest = ConstructorArgs; +type SetTokenInstanceMetadataRequest = ConstructorArgs; type TransferTokenRequest = ConstructorArgs; type UnlockTokenRequest = ConstructorArgs; type UnlockTokensRequest = ConstructorArgs; @@ -129,6 +141,7 @@ export { BurnTokensRequest, CreateTokenClassRequest, DeleteAllowancesRequest, + DeleteTokenInstanceMetadataRequest, FetchAllowancesRequest, FetchAllowancesResponse, FetchBalancesRequest, @@ -139,6 +152,9 @@ export { FetchTokenClassesRequest, FetchTokenClassesResponse, FetchTokenClassesWithPaginationRequest, + FetchTokenInstanceMetadataRequest, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationRequest, FulfillMintRequest, FullAllowanceCheckRequest, FullAllowanceCheckResponse, @@ -151,6 +167,7 @@ export { MintTokenRequest, MintTokenWithAllowanceRequest, RefreshAllowanceRequest, + SetTokenInstanceMetadataRequest, TokenAllowance, TokenBalance, TokenBalanceWithMetadata, @@ -159,6 +176,8 @@ export { TokenClassKey, TokenHold, TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataProject, TransferTokenRequest, UnlockTokenRequest, UnlockTokensRequest, diff --git a/chain-test/src/data/nft.ts b/chain-test/src/data/nft.ts index f52c3de64f..f9be1900c2 100644 --- a/chain-test/src/data/nft.ts +++ b/chain-test/src/data/nft.ts @@ -20,9 +20,14 @@ import { TokenClass, TokenClassKey, TokenInstance, - TokenInstanceKey + TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataAttribute, + TokenInstanceMetadataCustomField, + TokenInstanceMetadataProject } from "@gala-chain/api"; import BigNumber from "bignumber.js"; +import { plainToInstance } from "class-transformer"; import users from "./users"; import { createInstanceFn, createPlainFn } from "./utils"; @@ -144,6 +149,48 @@ const tokenInstance1Plain = createPlainFn({ owner: users.testUser1.identityKey }); +/** + * Creates a plain token instance metadata project ownership record for testing. + * Marks admin as the owner of the "TestProject" metadata for the test NFT class. + * + * @param txUnixTime - Unix timestamp for the ownership record creation time + * @returns Plain object representing project metadata ownership + */ +const tokenInstance1MetadataProjectPlain = (txUnixTime: number) => ({ + ...tokenClassKeyPlain(), + project: "TestProject", + owner: users.admin.identityKey, + created: txUnixTime +}); + +/** + * Creates a plain NFT token instance metadata object for testing. + * Represents an OpenSea-style metadata document for instance #1 and project + * "TestProject", created by admin. + * + * @param txUnixTime - Unix timestamp for the metadata creation time + * @returns Plain object representing token instance metadata + */ +const tokenInstance1MetadataPlain = (txUnixTime: number) => ({ + ...tokenInstance1KeyPlain(), + project: "TestProject", + name: "Test Elixir #1", + description: "Generated via automated test suite.", + image: "https://app.gala.games/test-image-placeholder-url.png", + attributes: [ + plainToInstance(TokenInstanceMetadataAttribute, { + trait_type: "Potency", + value: 9, + display_type: "number" + }) + ], + customFields: [plainToInstance(TokenInstanceMetadataCustomField, { key: "gameId", value: "elixir-001" })], + createdBy: users.admin.identityKey, + lastModifiedBy: users.admin.identityKey, + created: txUnixTime, + lastModified: txUnixTime +}); + /** * Creates a plain NFT token balance object for testing. * Assigns NFT instance #1 to testUser1. @@ -221,6 +268,13 @@ export default { tokenInstance1Key: createInstanceFn(TokenInstanceKey, tokenInstance1KeyPlain()), tokenInstance1Plain, tokenInstance1: createInstanceFn(TokenInstance, tokenInstance1Plain()), + tokenInstance1MetadataPlain, + tokenInstance1Metadata: createInstanceFn(TokenInstanceMetadata, tokenInstance1MetadataPlain(1)), + tokenInstance1MetadataProjectPlain, + tokenInstance1MetadataProject: createInstanceFn( + TokenInstanceMetadataProject, + tokenInstance1MetadataProjectPlain(1) + ), tokenBalancePlain, tokenBalance: createInstanceFn(TokenBalance, tokenBalancePlain()), tokenBurnPlain, diff --git a/chaincode/src/__test__/GalaChainTokenContract.ts b/chaincode/src/__test__/GalaChainTokenContract.ts index 0d11c70f00..d6093e304f 100644 --- a/chaincode/src/__test__/GalaChainTokenContract.ts +++ b/chaincode/src/__test__/GalaChainTokenContract.ts @@ -22,6 +22,7 @@ import { CreateTokenClassDto, CreateVestingTokenDto, DeleteAllowancesDto, + DeleteTokenInstanceMetadataDto, EnsureTokenSwapIndexingDto, EnsureTokenSwapIndexingResponse, FeeAuthorizationResDto, @@ -46,6 +47,9 @@ import { FetchTokenClassesDto, FetchTokenClassesResponse, FetchTokenClassesWithPaginationDto, + FetchTokenInstanceMetadataDto, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationDto, FetchTokenSwapByRequestIdDto, FetchTokenSwapsByInstanceDto, FetchTokenSwapsByUserDto, @@ -65,6 +69,7 @@ import { MintTokenWithAllowanceDto, RefreshAllowancesDto, RequestTokenSwapDto, + SetTokenInstanceMetadataDto, TerminateTokenSwapDto, TokenAllowance, TokenBalance, @@ -72,6 +77,7 @@ import { TokenClass, TokenClassKey, TokenInstanceKey, + TokenInstanceMetadata, TokenSwapFill, TokenSwapRequest, TransferTokenDto, @@ -101,6 +107,7 @@ import { defineFeeSchedule, defineFeeSplitFormula, deleteAllowances, + deleteTokenInstanceMetadata, fetchAllowancesWithPagination, fetchBalances, fetchBalancesWithTokenMetadata, @@ -110,6 +117,8 @@ import { fetchFeeThresholdUsesWithPagination, fetchTokenClasses, fetchTokenClassesWithPagination, + fetchTokenInstanceMetadata, + fetchTokenInstanceMetadataWithPagination, fetchVestingToken, fulfillMintRequest, fullAllowanceCheck, @@ -122,6 +131,7 @@ import { refreshAllowances, requestMint, resolveUserAlias, + setTokenInstanceMetadata, transferToken, unlockToken, unlockTokens, @@ -215,6 +225,65 @@ export default class GalaChainTokenContract extends GalaContract { return fetchTokenClassesWithPagination(ctx, dto); } + @Submit({ + in: SetTokenInstanceMetadataDto, + out: TokenInstanceMetadata + }) + public SetTokenInstanceMetadata( + ctx: GalaChainContext, + dto: SetTokenInstanceMetadataDto + ): Promise { + return setTokenInstanceMetadata(ctx, { + tokenInstance: dto.tokenInstance, + project: dto.project, + name: dto.name, + description: dto.description, + image: dto.image, + external_url: dto.external_url, + animation_url: dto.animation_url, + background_color: dto.background_color, + youtube_url: dto.youtube_url, + attributes: dto.attributes, + customFields: dto.customFields + }); + } + + @UnsignedEvaluate({ + in: FetchTokenInstanceMetadataDto, + out: { arrayOf: TokenInstanceMetadata } + }) + public FetchTokenInstanceMetadata( + ctx: GalaChainContext, + dto: FetchTokenInstanceMetadataDto + ): Promise { + return fetchTokenInstanceMetadata(ctx, { + tokenInstance: dto.tokenInstance, + project: dto.project + }); + } + + @UnsignedEvaluate({ + in: FetchTokenInstanceMetadataWithPaginationDto, + out: FetchTokenInstanceMetadataResponse + }) + public FetchTokenInstanceMetadataWithPagination( + ctx: GalaChainContext, + dto: FetchTokenInstanceMetadataWithPaginationDto + ): Promise { + return fetchTokenInstanceMetadataWithPagination(ctx, dto); + } + + @Submit({ + in: DeleteTokenInstanceMetadataDto, + out: TokenInstanceKey + }) + public DeleteTokenInstanceMetadata( + ctx: GalaChainContext, + dto: DeleteTokenInstanceMetadataDto + ): Promise { + return deleteTokenInstanceMetadata(ctx, { tokenInstance: dto.tokenInstance, project: dto.project }); + } + @Submit({ in: GrantAllowanceDto, out: { arrayOf: TokenAllowance } diff --git a/chaincode/src/index.ts b/chaincode/src/index.ts index 3bf71235f3..d1ac1415ce 100755 --- a/chaincode/src/index.ts +++ b/chaincode/src/index.ts @@ -25,6 +25,7 @@ export * from "./oracle"; export * from "./services"; export * from "./swaps"; export * from "./token"; +export * from "./tokenInstanceMetadata"; export * from "./types"; export * from "./utils"; export * from "./transfer"; diff --git a/chaincode/src/tokenInstanceMetadata/TokenInstanceMetadataError.ts b/chaincode/src/tokenInstanceMetadata/TokenInstanceMetadataError.ts new file mode 100644 index 0000000000..f40a042a55 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/TokenInstanceMetadataError.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ForbiddenError, NotFoundError, ValidationFailedError } from "@gala-chain/api"; + +export class TokenInstanceNotFoundError extends NotFoundError { + constructor(tokenInstanceKey: string) { + super(`Token instance not found: ${tokenInstanceKey}`, { tokenInstanceKey }); + } +} + +export class TokenInstanceMetadataNotFoundError extends NotFoundError { + constructor(tokenInstanceKey: string, project: string) { + super(`Token instance metadata of project ${project} not found: ${tokenInstanceKey}`, { + tokenInstanceKey, + project + }); + } +} + +export class NftInstanceRequiredError extends ValidationFailedError { + constructor(tokenInstanceKey: string) { + super(`Token instance metadata is supported for NFT instances only (got: ${tokenInstanceKey})`, { + tokenInstanceKey + }); + } +} + +export class NotProjectMetadataOwnerError extends ForbiddenError { + constructor(user: string, project: string, tokenClassKey: string, owner: string) { + super( + `User ${user} is not the owner of metadata of project ${project} ` + + `for token class ${tokenClassKey}. Owner: ${owner}`, + { user, project, tokenClassKey, owner } + ); + } +} diff --git a/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.spec.ts b/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.spec.ts new file mode 100644 index 0000000000..7ced12c79d --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.spec.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + DeleteTokenInstanceMetadataDto, + GalaChainResponse, + TokenInstanceKey, + createValidSubmitDTO +} from "@gala-chain/api"; +import { currency, fixture, nft, users } from "@gala-chain/test"; + +import GalaChainTokenContract from "../__test__/GalaChainTokenContract"; +import { + NftInstanceRequiredError, + NotProjectMetadataOwnerError, + TokenInstanceMetadataNotFoundError +} from "./TokenInstanceMetadataError"; + +const project = "TestProject"; + +it("should delete token instance metadata as project owner", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + const savedOwnership = nft.tokenInstance1MetadataProject(); // owned by users.admin + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.admin) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership, savedMetadata); + + const dto = await defaultDeleteDto(nft.tokenInstance1Key()).signed(users.admin.privateKey); + + // When + const response = await contract.DeleteTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(nft.tokenInstance1Key())); + // deletions are recorded as writes with empty values; ownership record is kept + expect(getWrites()).toEqual({ [savedMetadata.getCompositeKey()]: "" }); +}); + +it("should fail if metadata does not exist", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.admin) + .savedState(nft.tokenClass(), nft.tokenInstance1()); // no saved ownership or metadata + + const tokenInstanceKey = nft.tokenInstance1Key(); + const dto = await defaultDeleteDto(tokenInstanceKey).signed(users.admin.privateKey); + + // When + const response = await contract.DeleteTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new TokenInstanceMetadataNotFoundError(tokenInstanceKey.toStringKey(), project)) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail if metadata document is missing for the instance", async () => { + // Given: ownership exists (from another instance), but no document for this instance + const savedOwnership = nft.tokenInstance1MetadataProject(); + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.admin) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership); + + const tokenInstanceKey = nft.tokenInstance1Key(); + const dto = await defaultDeleteDto(tokenInstanceKey).signed(users.admin.privateKey); + + // When + const response = await contract.DeleteTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new TokenInstanceMetadataNotFoundError(tokenInstanceKey.toStringKey(), project)) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail if callingUser is not the project metadata owner", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + const savedOwnership = nft.tokenInstance1MetadataProject(); // owned by users.admin + const callingUser = users.testUser2; + expect(savedOwnership.owner).not.toEqual(callingUser.identityKey); + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(callingUser) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership, savedMetadata); + + const dto = await defaultDeleteDto(nft.tokenInstance1Key()).signed(callingUser.privateKey); + + // When + const response = await contract.DeleteTokenInstanceMetadata(ctx, dto); + + // Then + const classKey = nft.tokenClass().getCompositeKey(); + expect(response).toEqual( + GalaChainResponse.Error( + new NotProjectMetadataOwnerError(callingUser.identityKey, project, classKey, savedOwnership.owner) + ) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail for fungible token instances", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.admin) + .savedState(currency.tokenClass(), currency.tokenInstance()); + + const tokenInstanceKey = currency.tokenInstanceKey(); + const dto = await defaultDeleteDto(tokenInstanceKey).signed(users.admin.privateKey); + + // When + const response = await contract.DeleteTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new NftInstanceRequiredError(tokenInstanceKey.toStringKey())) + ); + expect(getWrites()).toEqual({}); +}); + +function defaultDeleteDto(tokenInstance: TokenInstanceKey) { + return createValidSubmitDTO(DeleteTokenInstanceMetadataDto, { tokenInstance, project }); +} diff --git a/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.ts b/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.ts new file mode 100644 index 0000000000..361bab2cb9 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/deleteTokenInstanceMetadata.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TokenClass, TokenInstanceKey, TokenInstanceMetadata } from "@gala-chain/api"; + +import { GalaChainContext } from "../types"; +import { deleteChainObject, getObjectByKey } from "../utils/state"; +import { + NotProjectMetadataOwnerError, + TokenInstanceMetadataNotFoundError +} from "./TokenInstanceMetadataError"; +import { + buildTokenInstanceMetadataCompositeKey, + ensureNftInstance, + fetchTokenInstanceMetadataProject +} from "./tokenInstanceMetadataHelpers"; + +export interface DeleteTokenInstanceMetadataParams { + tokenInstance: TokenInstanceKey; + project: string; +} + +export async function deleteTokenInstanceMetadata( + ctx: GalaChainContext, + params: DeleteTokenInstanceMetadataParams +): Promise { + const { tokenInstance, project } = params; + + await ensureNftInstance(ctx, tokenInstance); + + const ownership = await fetchTokenInstanceMetadataProject(ctx, tokenInstance, project); + + if (ownership === undefined) { + throw new TokenInstanceMetadataNotFoundError(tokenInstance.toStringKey(), project); + } + + if (ownership.owner !== ctx.callingUser) { + const classKey = TokenClass.buildTokenClassCompositeKey(tokenInstance); + throw new NotProjectMetadataOwnerError(ctx.callingUser, project, classKey, ownership.owner); + } + + const metadataKey = buildTokenInstanceMetadataCompositeKey(tokenInstance, project); + + const existing = await getObjectByKey(ctx, TokenInstanceMetadata, metadataKey).catch(() => { + throw new TokenInstanceMetadataNotFoundError(tokenInstance.toStringKey(), project); + }); + + await deleteChainObject(ctx, existing); + + return tokenInstance; +} diff --git a/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.spec.ts b/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.spec.ts new file mode 100644 index 0000000000..e32b4c3ef6 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.spec.ts @@ -0,0 +1,151 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + FetchTokenInstanceMetadataDto, + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationDto, + GalaChainResponse, + TokenInstanceMetadata, + createValidDTO +} from "@gala-chain/api"; +import { fixture, nft } from "@gala-chain/test"; + +import GalaChainTokenContract from "../__test__/GalaChainTokenContract"; +import { TokenInstanceMetadataNotFoundError } from "./TokenInstanceMetadataError"; + +it("should fetch metadata documents of all projects for an instance", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); // project "TestProject" + const otherProjectMetadata = nft.tokenInstance1Metadata((plain) => ({ + ...plain, + project: "OtherProject", + name: "Other Project View" + })); + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract).savedState( + savedMetadata, + otherProjectMetadata + ); + + const dto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: nft.tokenInstance1Key() + }); + + // When + const response = await contract.FetchTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Success(expect.arrayContaining([savedMetadata, otherProjectMetadata])) + ); + expect((response.Data as TokenInstanceMetadata[]).length).toEqual(2); + expect(getWrites()).toEqual({}); +}); + +it("should fetch metadata document of a single project", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + + const { ctx, contract } = fixture(GalaChainTokenContract).savedState(savedMetadata); + + const dto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: nft.tokenInstance1Key(), + project: savedMetadata.project + }); + + // When + const response = await contract.FetchTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success([savedMetadata])); +}); + +it("should throw an error if project metadata is missing", async () => { + // Given + const { ctx, contract } = fixture(GalaChainTokenContract); // no saved metadata + + const missingKey = nft.tokenInstance1Key(); + const dto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: missingKey, + project: "TestProject" + }); + + // When + const response = await contract.FetchTokenInstanceMetadata(ctx, dto).catch((e) => e); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new TokenInstanceMetadataNotFoundError(missingKey.toStringKey(), "TestProject")) + ); +}); + +it("should return an empty list if instance has no metadata", async () => { + // Given + const { ctx, contract } = fixture(GalaChainTokenContract); // no saved metadata + + const dto = await createValidDTO(FetchTokenInstanceMetadataDto, { + tokenInstance: nft.tokenInstance1Key() + }); + + // When + const response = await contract.FetchTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success([])); +}); + +it("should FetchTokenInstanceMetadataWithPagination", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + + const { ctx, contract } = fixture(GalaChainTokenContract).savedState(savedMetadata); + + const dto = await createValidDTO(FetchTokenInstanceMetadataWithPaginationDto, { + collection: savedMetadata.collection + }); + + const expectedResponse = await createValidDTO(FetchTokenInstanceMetadataResponse, { + nextPageBookmark: "", + results: [savedMetadata] + }); + + // When + const response = await contract.FetchTokenInstanceMetadataWithPagination(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedResponse)); +}); + +it("should not throw a 404 error if no metadata is found when using the pagination method", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + + const { ctx, contract } = fixture(GalaChainTokenContract).savedState(savedMetadata); + + const dto = await createValidDTO(FetchTokenInstanceMetadataWithPaginationDto, { + collection: "Missing" + }); + + const expectedResponse = await createValidDTO(FetchTokenInstanceMetadataResponse, { + nextPageBookmark: "", + results: [] + }); + + // When + const response = await contract.FetchTokenInstanceMetadataWithPagination(ctx, dto).catch((e) => e); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedResponse)); +}); diff --git a/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.ts b/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.ts new file mode 100644 index 0000000000..93ef161378 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/fetchTokenInstanceMetadata.ts @@ -0,0 +1,102 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + FetchTokenInstanceMetadataResponse, + FetchTokenInstanceMetadataWithPaginationDto, + TokenInstance, + TokenInstanceKey, + TokenInstanceMetadata, + createValidDTO +} from "@gala-chain/api"; + +import { GalaChainContext } from "../types"; +import { + getObjectByKey, + getObjectsByPartialCompositeKey, + getObjectsByPartialCompositeKeyWithPagination, + takeUntilUndefined +} from "../utils"; +import { TokenInstanceMetadataNotFoundError } from "./TokenInstanceMetadataError"; +import { buildTokenInstanceMetadataCompositeKey } from "./tokenInstanceMetadataHelpers"; + +export interface FetchTokenInstanceMetadataParams { + tokenInstance: TokenInstanceKey; + project?: string; +} + +export async function fetchTokenInstanceMetadata( + ctx: GalaChainContext, + params: FetchTokenInstanceMetadataParams +): Promise { + const { tokenInstance, project } = params; + + if (project !== undefined) { + const metadataKey = buildTokenInstanceMetadataCompositeKey(tokenInstance, project); + const metadata = await getObjectByKey(ctx, TokenInstanceMetadata, metadataKey).catch(() => { + throw new TokenInstanceMetadataNotFoundError(tokenInstance.toStringKey(), project); + }); + return [metadata]; + } + + const instanceKeyParts = TokenInstance.buildInstanceKeyList(tokenInstance); + + return await getObjectsByPartialCompositeKey( + ctx, + TokenInstanceMetadata.INDEX_KEY, + instanceKeyParts, + TokenInstanceMetadata + ); +} + +export interface FetchTokenInstanceMetadataWithPaginationParams { + collection?: string; + category?: string; + type?: string; + additionalKey?: string; + instance?: string; + project?: string; + bookmark?: string; + limit?: number; +} + +export async function fetchTokenInstanceMetadataWithPagination( + ctx: GalaChainContext, + params: FetchTokenInstanceMetadataWithPaginationParams +): Promise { + const queryParams: string[] = takeUntilUndefined( + params.collection, + params.category, + params.type, + params.additionalKey, + params.instance, + params.project + ); + + const getObjectsResponse = await getObjectsByPartialCompositeKeyWithPagination( + ctx, + TokenInstanceMetadata.INDEX_KEY, + queryParams, + TokenInstanceMetadata, + params.bookmark, + params.limit ?? FetchTokenInstanceMetadataWithPaginationDto.DEFAULT_LIMIT + ); + + const response = await createValidDTO(FetchTokenInstanceMetadataResponse, { + results: getObjectsResponse.results, + nextPageBookmark: getObjectsResponse.metadata.bookmark + }); + + return response; +} diff --git a/chaincode/src/tokenInstanceMetadata/index.ts b/chaincode/src/tokenInstanceMetadata/index.ts new file mode 100644 index 0000000000..37c0f17271 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/index.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + NftInstanceRequiredError, + NotProjectMetadataOwnerError, + TokenInstanceMetadataNotFoundError, + TokenInstanceNotFoundError +} from "./TokenInstanceMetadataError"; +import { + DeleteTokenInstanceMetadataParams, + deleteTokenInstanceMetadata +} from "./deleteTokenInstanceMetadata"; +import { + FetchTokenInstanceMetadataParams, + FetchTokenInstanceMetadataWithPaginationParams, + fetchTokenInstanceMetadata, + fetchTokenInstanceMetadataWithPagination +} from "./fetchTokenInstanceMetadata"; +import { SetTokenInstanceMetadataParams, setTokenInstanceMetadata } from "./setTokenInstanceMetadata"; +import { + buildTokenInstanceMetadataCompositeKey, + buildTokenInstanceMetadataProjectCompositeKey, + ensureNftInstance, + fetchTokenInstanceMetadataProject +} from "./tokenInstanceMetadataHelpers"; + +export { + setTokenInstanceMetadata, + SetTokenInstanceMetadataParams, + fetchTokenInstanceMetadata, + FetchTokenInstanceMetadataParams, + fetchTokenInstanceMetadataWithPagination, + FetchTokenInstanceMetadataWithPaginationParams, + deleteTokenInstanceMetadata, + DeleteTokenInstanceMetadataParams, + ensureNftInstance, + fetchTokenInstanceMetadataProject, + buildTokenInstanceMetadataCompositeKey, + buildTokenInstanceMetadataProjectCompositeKey, + NftInstanceRequiredError, + NotProjectMetadataOwnerError, + TokenInstanceMetadataNotFoundError, + TokenInstanceNotFoundError +}; diff --git a/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.spec.ts b/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.spec.ts new file mode 100644 index 0000000000..5aa58def50 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.spec.ts @@ -0,0 +1,250 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + GalaChainResponse, + SetTokenInstanceMetadataDto, + TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataAttribute, + TokenInstanceMetadataCustomField, + TokenInstanceMetadataProject, + createValidChainObject, + createValidSubmitDTO +} from "@gala-chain/api"; +import { currency, fixture, nft, users, writesMap } from "@gala-chain/test"; +import { plainToInstance } from "class-transformer"; + +import GalaChainTokenContract from "../__test__/GalaChainTokenContract"; +import { TokenClassNotFoundError } from "../token/TokenError"; +import { + NftInstanceRequiredError, + NotProjectMetadataOwnerError, + TokenInstanceNotFoundError +} from "./TokenInstanceMetadataError"; +import { SetTokenInstanceMetadataParams } from "./setTokenInstanceMetadata"; + +const project = "TestProject"; + +it("should set token instance metadata and create project ownership", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1) + .savedState(nft.tokenClass(), nft.tokenInstance1()); + + const metadataProps = defaultMetadataProps(); + const dto = await defaultSetDto(nft.tokenInstance1Key(), metadataProps).signed(users.testUser1.privateKey); + + const expectedMetadata = await createValidChainObject(TokenInstanceMetadata, { + ...nft.tokenInstance1KeyPlain(), + project, + ...metadataProps, + createdBy: users.testUser1.identityKey, + lastModifiedBy: users.testUser1.identityKey, + created: ctx.txUnixTime, + lastModified: ctx.txUnixTime + }); + + const expectedOwnership = await createValidChainObject(TokenInstanceMetadataProject, { + ...nft.tokenClassKeyPlain(), + project, + owner: users.testUser1.identityKey, + created: ctx.txUnixTime + }); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedMetadata)); + expect(getWrites()).toEqual(writesMap(expectedOwnership, expectedMetadata)); +}); + +it("should replace existing metadata as owner, preserving creation info", async () => { + // Given + const savedMetadata = nft.tokenInstance1Metadata(); + const savedOwnership = nft.tokenInstance1MetadataProject(); // owned by users.admin + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.admin) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership, savedMetadata); + + const update = { name: "Renamed Elixir", description: "Replaced document." }; + const dto = await defaultSetDto(nft.tokenInstance1Key(), update).signed(users.admin.privateKey); + + // full-document semantics: fields omitted in the dto (image, attributes, etc.) are removed + const expectedMetadata = await createValidChainObject(TokenInstanceMetadata, { + ...nft.tokenInstance1KeyPlain(), + project, + ...update, + createdBy: savedMetadata.createdBy, + created: savedMetadata.created, + lastModifiedBy: users.admin.identityKey, + lastModified: ctx.txUnixTime + }); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedMetadata)); + expect(getWrites()).toEqual(writesMap(expectedMetadata)); +}); + +it("should allow a different user to set metadata for a different project", async () => { + // Given + const savedOwnership = nft.tokenInstance1MetadataProject(); // "TestProject" owned by users.admin + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser2) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership); + + const otherProject = "OtherProject"; + const dto = await createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance: nft.tokenInstance1Key(), + project: otherProject, + name: "Other Project View" + }).signed(users.testUser2.privateKey); + + const expectedMetadata = await createValidChainObject(TokenInstanceMetadata, { + ...nft.tokenInstance1KeyPlain(), + project: otherProject, + name: "Other Project View", + createdBy: users.testUser2.identityKey, + lastModifiedBy: users.testUser2.identityKey, + created: ctx.txUnixTime, + lastModified: ctx.txUnixTime + }); + + const expectedOwnership = await createValidChainObject(TokenInstanceMetadataProject, { + ...nft.tokenClassKeyPlain(), + project: otherProject, + owner: users.testUser2.identityKey, + created: ctx.txUnixTime + }); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual(GalaChainResponse.Success(expectedMetadata)); + expect(getWrites()).toEqual(writesMap(expectedOwnership, expectedMetadata)); +}); + +it("should fail if callingUser is not the project metadata owner", async () => { + // Given + const savedOwnership = nft.tokenInstance1MetadataProject(); // owned by users.admin + const callingUser = users.testUser2; + expect(savedOwnership.owner).not.toEqual(callingUser.identityKey); + + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(callingUser) + .savedState(nft.tokenClass(), nft.tokenInstance1(), savedOwnership); + + const dto = await defaultSetDto(nft.tokenInstance1Key()).signed(callingUser.privateKey); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + const classKey = nft.tokenClass().getCompositeKey(); + expect(response).toEqual( + GalaChainResponse.Error( + new NotProjectMetadataOwnerError(callingUser.identityKey, project, classKey, savedOwnership.owner) + ) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail if token class does not exist", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract).registeredUsers(users.testUser1); // no saved token class + + const tokenInstanceKey = nft.tokenInstance1Key(); + const dto = await defaultSetDto(tokenInstanceKey).signed(users.testUser1.privateKey); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new TokenClassNotFoundError(tokenInstanceKey.getTokenClassKey().toStringKey())) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail if token instance does not exist", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1) + .savedState(nft.tokenClass()); // class saved, but no instance + + const tokenInstanceKey = nft.tokenInstance1Key(); + const dto = await defaultSetDto(tokenInstanceKey).signed(users.testUser1.privateKey); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new TokenInstanceNotFoundError(tokenInstanceKey.toStringKey())) + ); + expect(getWrites()).toEqual({}); +}); + +it("should fail for fungible token instances", async () => { + // Given + const { ctx, contract, getWrites } = fixture(GalaChainTokenContract) + .registeredUsers(users.testUser1) + .savedState(currency.tokenClass(), currency.tokenInstance()); + + const tokenInstanceKey = currency.tokenInstanceKey(); + const dto = await defaultSetDto(tokenInstanceKey).signed(users.testUser1.privateKey); + + // When + const response = await contract.SetTokenInstanceMetadata(ctx, dto); + + // Then + expect(response).toEqual( + GalaChainResponse.Error(new NftInstanceRequiredError(tokenInstanceKey.toStringKey())) + ); + expect(getWrites()).toEqual({}); +}); + +function defaultMetadataProps() { + return { + name: "Test Elixir #1", + description: "Generated via automated test suite.", + image: "https://app.gala.games/test-image-placeholder-url.png", + attributes: [ + plainToInstance(TokenInstanceMetadataAttribute, { + trait_type: "Potency", + value: 9, + display_type: "number" + }) + ], + customFields: [plainToInstance(TokenInstanceMetadataCustomField, { key: "gameId", value: "elixir-001" })] + }; +} + +function defaultSetDto( + tokenInstance: TokenInstanceKey, + metadataProps: Omit = defaultMetadataProps() +) { + return createValidSubmitDTO(SetTokenInstanceMetadataDto, { + tokenInstance, + project, + ...metadataProps + }); +} diff --git a/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.ts b/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.ts new file mode 100644 index 0000000000..f632b9e210 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/setTokenInstanceMetadata.ts @@ -0,0 +1,109 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + TokenClass, + TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataAttribute, + TokenInstanceMetadataCustomField, + TokenInstanceMetadataProject, + createValidChainObject +} from "@gala-chain/api"; + +import { fetchTokenInstance } from "../token/fetchTokenInstance"; +import { GalaChainContext } from "../types"; +import { getObjectByKey, putChainObject } from "../utils/state"; +import { NotProjectMetadataOwnerError, TokenInstanceNotFoundError } from "./TokenInstanceMetadataError"; +import { + buildTokenInstanceMetadataCompositeKey, + ensureNftInstance, + fetchTokenInstanceMetadataProject +} from "./tokenInstanceMetadataHelpers"; + +export interface SetTokenInstanceMetadataParams { + tokenInstance: TokenInstanceKey; + project: string; + name?: string; + description?: string; + image?: string; + external_url?: string; + animation_url?: string; + background_color?: string; + youtube_url?: string; + attributes?: TokenInstanceMetadataAttribute[]; + customFields?: TokenInstanceMetadataCustomField[]; +} + +export async function setTokenInstanceMetadata( + ctx: GalaChainContext, + params: SetTokenInstanceMetadataParams +): Promise { + const { tokenInstance, project } = params; + + await ensureNftInstance(ctx, tokenInstance); + + await fetchTokenInstance(ctx, tokenInstance).catch(() => { + throw new TokenInstanceNotFoundError(tokenInstance.toStringKey()); + }); + + // the first user to create metadata for a project on a token class becomes + // the owner of that project's metadata for the whole class + const ownership = await fetchTokenInstanceMetadataProject(ctx, tokenInstance, project); + + if (ownership === undefined) { + const newOwnership = await createValidChainObject(TokenInstanceMetadataProject, { + collection: tokenInstance.collection, + category: tokenInstance.category, + type: tokenInstance.type, + additionalKey: tokenInstance.additionalKey, + project, + owner: ctx.callingUser, + created: ctx.txUnixTime + }); + await putChainObject(ctx, newOwnership); + } else if (ownership.owner !== ctx.callingUser) { + const classKey = TokenClass.buildTokenClassCompositeKey(tokenInstance); + throw new NotProjectMetadataOwnerError(ctx.callingUser, project, classKey, ownership.owner); + } + + const metadataKey = buildTokenInstanceMetadataCompositeKey(tokenInstance, project); + const existing = await getObjectByKey(ctx, TokenInstanceMetadata, metadataKey).catch(() => undefined); + + const metadata = await createValidChainObject(TokenInstanceMetadata, { + collection: tokenInstance.collection, + category: tokenInstance.category, + type: tokenInstance.type, + additionalKey: tokenInstance.additionalKey, + instance: tokenInstance.instance, + project, + name: params.name, + description: params.description, + image: params.image, + external_url: params.external_url, + animation_url: params.animation_url, + background_color: params.background_color, + youtube_url: params.youtube_url, + attributes: params.attributes, + customFields: params.customFields, + createdBy: existing?.createdBy ?? ctx.callingUser, + created: existing?.created ?? ctx.txUnixTime, + lastModifiedBy: ctx.callingUser, + lastModified: ctx.txUnixTime + }); + + await putChainObject(ctx, metadata); + + return metadata; +} diff --git a/chaincode/src/tokenInstanceMetadata/tokenInstanceMetadataHelpers.ts b/chaincode/src/tokenInstanceMetadata/tokenInstanceMetadataHelpers.ts new file mode 100644 index 0000000000..6a8f602de4 --- /dev/null +++ b/chaincode/src/tokenInstanceMetadata/tokenInstanceMetadataHelpers.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + ChainObject, + TokenClass, + TokenInstance, + TokenInstanceKey, + TokenInstanceMetadata, + TokenInstanceMetadataProject +} from "@gala-chain/api"; + +import { TokenClassNotFoundError } from "../token/TokenError"; +import { GalaChainContext } from "../types"; +import { getObjectByKey } from "../utils/state"; +import { NftInstanceRequiredError } from "./TokenInstanceMetadataError"; + +export async function ensureNftInstance( + ctx: GalaChainContext, + tokenInstance: TokenInstanceKey +): Promise { + const classKey = TokenClass.buildTokenClassCompositeKey(tokenInstance); + + const tokenClass = await getObjectByKey(ctx, TokenClass, classKey).catch(() => { + throw new TokenClassNotFoundError(tokenInstance.getTokenClassKey().toStringKey()); + }); + + if (!tokenClass.isNonFungible || tokenInstance.isFungible()) { + throw new NftInstanceRequiredError(tokenInstance.toStringKey()); + } + + return tokenClass; +} + +export function buildTokenInstanceMetadataCompositeKey( + tokenInstance: TokenInstanceKey, + project: string +): string { + const compositeKeyParts = [...TokenInstance.buildInstanceKeyList(tokenInstance), project]; + return ChainObject.getCompositeKeyFromParts(TokenInstanceMetadata.INDEX_KEY, compositeKeyParts); +} + +export function buildTokenInstanceMetadataProjectCompositeKey( + tokenInstance: TokenInstanceKey, + project: string +): string { + const compositeKeyParts = [ + tokenInstance.collection, + tokenInstance.category, + tokenInstance.type, + tokenInstance.additionalKey, + project + ]; + return ChainObject.getCompositeKeyFromParts(TokenInstanceMetadataProject.INDEX_KEY, compositeKeyParts); +} + +export async function fetchTokenInstanceMetadataProject( + ctx: GalaChainContext, + tokenInstance: TokenInstanceKey, + project: string +): Promise { + const key = buildTokenInstanceMetadataProjectCompositeKey(tokenInstance, project); + return await getObjectByKey(ctx, TokenInstanceMetadataProject, key).catch(() => undefined); +}