diff --git a/framework/src/modules/nft/cc_commands/cc_transfer.ts b/framework/src/modules/nft/cc_commands/cc_transfer.ts index 3213c30fbb..d4a28e89c6 100644 --- a/framework/src/modules/nft/cc_commands/cc_transfer.ts +++ b/framework/src/modules/nft/cc_commands/cc_transfer.ts @@ -26,7 +26,7 @@ import { import { InternalMethod } from '../internal_method'; import { BaseCCCommand } from '../../interoperability/base_cc_command'; import { CrossChainMessageContext } from '../../interoperability/types'; -import { MAX_RESERVED_ERROR_STATUS } from '../../interoperability/constants'; +import { CCMStatusCode, MAX_RESERVED_ERROR_STATUS } from '../../interoperability/constants'; import { FeeMethod } from '../types'; import { EscrowStore } from '../stores/escrow'; import { CcmTransferEvent } from '../events/ccm_transfer'; @@ -75,12 +75,20 @@ export class CrossChainTransferCommand extends BaseCCCommand { throw new Error('Non-existent entry in the NFT substore'); } - const owner = await this._method.getNFTOwner(getMethodContext(), nftID); - if (!owner.equals(sendingChainID)) { + const nft = await nftStore.get(getMethodContext(), nftID); + if (!nft.owner.equals(sendingChainID)) { throw new Error('NFT has not been properly escrowed'); } } + if ( + !nftChainID.equals(ownChainID) && + (ccm.status === CCMStatusCode.MODULE_NOT_SUPPORTED || + ccm.status === CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED) + ) { + throw new Error('Module or cross-chain command not supported'); + } + if (!nftChainID.equals(ownChainID) && nftExists) { throw new Error('NFT substore entry already exists'); } diff --git a/framework/src/modules/nft/commands/transfer.ts b/framework/src/modules/nft/commands/transfer.ts index 3da6043b42..8588035a0d 100644 --- a/framework/src/modules/nft/commands/transfer.ts +++ b/framework/src/modules/nft/commands/transfer.ts @@ -20,9 +20,7 @@ import { } from '../../../state_machine'; import { BaseCommand } from '../../base_command'; import { transferParamsSchema } from '../schemas'; -import { NFTStore } from '../stores/nft'; import { NFTMethod } from '../method'; -import { LENGTH_CHAIN_ID, NFT_NOT_LOCKED } from '../constants'; import { InternalMethod } from '../internal_method'; export interface Params { @@ -43,31 +41,24 @@ export class TransferCommand extends BaseCommand { public async verify(context: CommandVerifyContext): Promise { const { params } = context; + const methodContext = context.getMethodContext(); - const nftStore = this.stores.get(NFTStore); - - const nftExists = await nftStore.has(context, params.nftID); - - if (!nftExists) { - throw new Error('NFT substore entry does not exist'); + let nft; + try { + nft = await this._method.getNFT(methodContext, params.nftID); + } catch (error) { + throw new Error('NFT does not exist'); } - const owner = await this._method.getNFTOwner(context.getMethodContext(), params.nftID); - - if (owner.length === LENGTH_CHAIN_ID) { + if (this._method.isNFTEscrowed(nft)) { throw new Error('NFT is escrowed to another chain'); } - if (!owner.equals(context.transaction.senderAddress)) { + if (!nft.owner.equals(context.transaction.senderAddress)) { throw new Error('Transfer not initiated by the NFT owner'); } - const lockingModule = await this._method.getLockingModule( - context.getMethodContext(), - params.nftID, - ); - - if (lockingModule !== NFT_NOT_LOCKED) { + if (this._method.isNFTLocked(nft)) { throw new Error('Locked NFTs cannot be transferred'); } diff --git a/framework/src/modules/nft/commands/transfer_cross_chain.ts b/framework/src/modules/nft/commands/transfer_cross_chain.ts index 9fa0cd4f20..9fef1ab8f1 100644 --- a/framework/src/modules/nft/commands/transfer_cross_chain.ts +++ b/framework/src/modules/nft/commands/transfer_cross_chain.ts @@ -13,9 +13,7 @@ */ import { crossChainTransferParamsSchema } from '../schemas'; -import { NFTStore } from '../stores/nft'; import { NFTMethod } from '../method'; -import { LENGTH_CHAIN_ID, NFT_NOT_LOCKED } from '../constants'; import { InteroperabilityMethod, TokenMethod } from '../types'; import { BaseCommand } from '../../base_command'; import { @@ -57,21 +55,20 @@ export class TransferCrossChainCommand extends BaseCommand { public async verify(context: CommandVerifyContext): Promise { const { params } = context; - - const nftStore = this.stores.get(NFTStore); - const nftExists = await nftStore.has(context.getMethodContext(), params.nftID); + const methodContext = context.getMethodContext(); if (params.receivingChainID.equals(context.chainID)) { throw new Error('Receiving chain cannot be the sending chain'); } - if (!nftExists) { - throw new Error('NFT substore entry does not exist'); + let nft; + try { + nft = await this._nftMethod.getNFT(methodContext, params.nftID); + } catch (error) { + throw new Error('NFT does not exist'); } - const owner = await this._nftMethod.getNFTOwner(context.getMethodContext(), params.nftID); - - if (owner.length === LENGTH_CHAIN_ID) { + if (this._nftMethod.isNFTEscrowed(nft)) { throw new Error('NFT is escrowed to another chain'); } @@ -82,25 +79,20 @@ export class TransferCrossChainCommand extends BaseCommand { } const messageFeeTokenID = await this._interoperabilityMethod.getMessageFeeTokenID( - context.getMethodContext(), + methodContext, params.receivingChainID, ); - if (!owner.equals(context.transaction.senderAddress)) { + if (!nft.owner.equals(context.transaction.senderAddress)) { throw new Error('Transfer not initiated by the NFT owner'); } - const lockingModule = await this._nftMethod.getLockingModule( - context.getMethodContext(), - params.nftID, - ); - - if (lockingModule !== NFT_NOT_LOCKED) { + if (this._nftMethod.isNFTLocked(nft)) { throw new Error('Locked NFTs cannot be transferred'); } const availableBalance = await this._tokenMethod.getAvailableBalance( - context.getMethodContext(), + methodContext, context.transaction.senderAddress, messageFeeTokenID, ); diff --git a/framework/src/modules/nft/endpoint.ts b/framework/src/modules/nft/endpoint.ts index 6c71df1900..6a4c18b093 100644 --- a/framework/src/modules/nft/endpoint.ts +++ b/framework/src/modules/nft/endpoint.ts @@ -28,7 +28,7 @@ import { import { NFTStore } from './stores/nft'; import { ALL_SUPPORTED_NFTS_KEY, LENGTH_ADDRESS, LENGTH_NFT_ID } from './constants'; import { UserStore } from './stores/user'; -import { NFT } from './types'; +import { NFTJSON } from './types'; import { SupportedNFTsStore } from './stores/supported_nfts'; import { NFTMethod } from './method'; @@ -41,7 +41,7 @@ export class NFTEndpoint extends BaseEndpoint { public async getNFTs( context: ModuleEndpointContext, - ): Promise<{ nfts: JSONObject & { id: string }>[] }> { + ): Promise<{ nfts: JSONObject & { id: string }>[] }> { validator.validate<{ address: string }>(getNFTsRequestSchema, context.params); const nftStore = this.stores.get(NFTStore); @@ -97,7 +97,7 @@ export class NFTEndpoint extends BaseEndpoint { return { hasNFT: nftData.owner.equals(owner) }; } - public async getNFT(context: ModuleEndpointContext): Promise> { + public async getNFT(context: ModuleEndpointContext): Promise> { const { params } = context; validator.validate<{ id: string }>(getNFTRequestSchema, params); @@ -106,7 +106,7 @@ export class NFTEndpoint extends BaseEndpoint { const nftExists = await nftStore.has(context.getImmutableMethodContext(), nftID); if (!nftExists) { - throw new Error('NFT does not exist'); + throw new Error('NFT substore entry does not exist'); } const userStore = this.stores.get(UserStore); @@ -118,6 +118,13 @@ export class NFTEndpoint extends BaseEndpoint { })); if (nftData.owner.length === LENGTH_ADDRESS) { + const userExists = await userStore.has( + context.getImmutableMethodContext(), + userStore.getKey(nftData.owner, nftID), + ); + if (!userExists) { + throw new Error('User substore entry does not exist'); + } const userData = await userStore.get( context.getImmutableMethodContext(), userStore.getKey(nftData.owner, nftID), diff --git a/framework/src/modules/nft/method.ts b/framework/src/modules/nft/method.ts index c1443aabb6..1ce1885561 100644 --- a/framework/src/modules/nft/method.ts +++ b/framework/src/modules/nft/method.ts @@ -15,7 +15,7 @@ import { validator } from '@liskhq/lisk-validator'; import { codec } from '@liskhq/lisk-codec'; import { BaseMethod } from '../base_method'; -import { FeeMethod, InteroperabilityMethod, ModuleConfig, TokenMethod } from './types'; +import { FeeMethod, InteroperabilityMethod, ModuleConfig, NFT, TokenMethod } from './types'; import { NFTAttributes, NFTStore, NFTStoreData, nftStoreSchema } from './stores/nft'; import { ImmutableMethodContext, MethodContext } from '../../state_machine'; import { @@ -79,9 +79,20 @@ export class NFTMethod extends BaseMethod { return nftID.subarray(0, LENGTH_CHAIN_ID); } - public async getNFTOwner(methodContext: ImmutableMethodContext, nftID: Buffer): Promise { - const nftStore = this.stores.get(NFTStore); + public isNFTEscrowed(nft: NFT): boolean { + return nft.owner.length !== LENGTH_ADDRESS; + } + + public isNFTLocked(nft: NFT): boolean { + if (!nft.lockingModule) { + return false; + } + return nft.lockingModule !== NFT_NOT_LOCKED; + } + + public async getNFT(methodContext: ImmutableMethodContext, nftID: Buffer): Promise { + const nftStore = this.stores.get(NFTStore); const nftExists = await nftStore.has(methodContext, nftID); if (!nftExists) { @@ -89,24 +100,19 @@ export class NFTMethod extends BaseMethod { } const data = await nftStore.get(methodContext, nftID); + const { owner } = data; - return data.owner; - } - - public async getLockingModule( - methodContext: ImmutableMethodContext, - nftID: Buffer, - ): Promise { - const owner = await this.getNFTOwner(methodContext, nftID); - - if (owner.length === LENGTH_CHAIN_ID) { - throw new Error('NFT is escrowed to another chain'); + if (owner.length === LENGTH_ADDRESS) { + const userStore = this.stores.get(UserStore); + const userExists = await userStore.has(methodContext, userStore.getKey(owner, nftID)); + if (!userExists) { + throw new Error('User substore entry does not exist'); + } + const userData = await userStore.get(methodContext, userStore.getKey(owner, nftID)); + return { ...data, lockingModule: userData.lockingModule }; } - const userStore = this.stores.get(UserStore); - const userData = await userStore.get(methodContext, userStore.getKey(owner, nftID)); - - return userData.lockingModule; + return data; } public async destroy( @@ -114,11 +120,10 @@ export class NFTMethod extends BaseMethod { address: Buffer, nftID: Buffer, ): Promise { - const nftStore = this.stores.get(NFTStore); - - const nftExists = await nftStore.has(methodContext, nftID); - - if (!nftExists) { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { this.events.get(DestroyEvent).error( methodContext, { @@ -128,42 +133,36 @@ export class NFTMethod extends BaseMethod { NftEventResult.RESULT_NFT_DOES_NOT_EXIST, ); - throw new Error('NFT substore entry does not exist'); + throw new Error('NFT does not exist'); } - const owner = await this.getNFTOwner(methodContext, nftID); - - if (owner.length === LENGTH_CHAIN_ID) { + if (!nft.owner.equals(address)) { this.events.get(DestroyEvent).error( methodContext, { address, nftID, }, - NftEventResult.RESULT_NFT_ESCROWED, + NftEventResult.RESULT_INITIATED_BY_NONOWNER, ); - throw new Error('NFT is escrowed to another chain'); + throw new Error('Not initiated by the NFT owner'); } - if (!owner.equals(address)) { + if (this.isNFTEscrowed(nft)) { this.events.get(DestroyEvent).error( methodContext, { address, nftID, }, - NftEventResult.RESULT_INITIATED_BY_NONOWNER, + NftEventResult.RESULT_NFT_ESCROWED, ); - throw new Error('Not initiated by the NFT owner'); + throw new Error('NFT is escrowed to another chain'); } - const userStore = this.stores.get(UserStore); - const userKey = userStore.getKey(owner, nftID); - const { lockingModule } = await userStore.get(methodContext, userKey); - - if (lockingModule !== NFT_NOT_LOCKED) { + if (this.isNFTLocked(nft)) { this.events.get(DestroyEvent).error( methodContext, { @@ -176,9 +175,10 @@ export class NFTMethod extends BaseMethod { throw new Error('Locked NFTs cannot be destroyed'); } + const nftStore = this.stores.get(NFTStore); + const userStore = this.stores.get(UserStore); await nftStore.del(methodContext, nftID); - - await userStore.del(methodContext, userKey); + await userStore.del(methodContext, userStore.getKey(nft.owner, nftID)); this.events.get(DestroyEvent).log(methodContext, { address, @@ -227,42 +227,6 @@ export class NFTMethod extends BaseMethod { return false; } - public async getAttributesArray( - methodContext: MethodContext, - nftID: Buffer, - ): Promise { - const nftStore = this.stores.get(NFTStore); - const nftExists = await nftStore.has(methodContext, nftID); - if (!nftExists) { - throw new Error('NFT substore entry does not exist'); - } - - const storeData = await nftStore.get(methodContext, nftID); - return storeData.attributesArray; - } - - public async getAttributes( - methodContext: MethodContext, - module: string, - nftID: Buffer, - ): Promise { - const nftStore = this.stores.get(NFTStore); - const nftExists = await nftStore.has(methodContext, nftID); - if (!nftExists) { - throw new Error('NFT substore entry does not exist'); - } - - const storeData = await nftStore.get(methodContext, nftID); - - for (const nftAttributes of storeData.attributesArray) { - if (nftAttributes.module === module) { - return nftAttributes.attributes; - } - } - - throw new Error('Specific module did not set any attributes.'); - } - public async getNextAvailableIndex( methodContext: MethodContext, collectionID: Buffer, @@ -333,11 +297,10 @@ export class NFTMethod extends BaseMethod { throw new Error('Cannot be locked by NFT module'); } - const nftStore = this.stores.get(NFTStore); - - const nftExists = await nftStore.has(methodContext, nftID); - - if (!nftExists) { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { this.events.get(LockEvent).error( methodContext, { @@ -347,12 +310,10 @@ export class NFTMethod extends BaseMethod { NftEventResult.RESULT_NFT_DOES_NOT_EXIST, ); - throw new Error('NFT substore entry does not exist'); + throw new Error('NFT does not exist'); } - const owner = await this.getNFTOwner(methodContext, nftID); - - if (owner.length === LENGTH_CHAIN_ID) { + if (this.isNFTEscrowed(nft)) { this.events.get(LockEvent).error( methodContext, { @@ -365,11 +326,7 @@ export class NFTMethod extends BaseMethod { throw new Error('NFT is escrowed to another chain'); } - const userStore = this.stores.get(UserStore); - const userKey = userStore.getKey(owner, nftID); - const userData = await userStore.get(methodContext, userKey); - - if (userData.lockingModule !== NFT_NOT_LOCKED) { + if (this.isNFTLocked(nft)) { this.events.get(LockEvent).error( methodContext, { @@ -382,9 +339,10 @@ export class NFTMethod extends BaseMethod { throw new Error('NFT is already locked'); } - userData.lockingModule = module; - - await userStore.set(methodContext, userKey, userData); + const userStore = this.stores.get(UserStore); + await userStore.set(methodContext, userStore.getKey(nft.owner, nftID), { + lockingModule: module, + }); this.events.get(LockEvent).log(methodContext, { module, @@ -393,11 +351,10 @@ export class NFTMethod extends BaseMethod { } public async unlock(methodContext: MethodContext, module: string, nftID: Buffer): Promise { - const nftStore = this.stores.get(NFTStore); - - const nftExists = await nftStore.has(methodContext, nftID); - - if (!nftExists) { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { this.events.get(LockEvent).error( methodContext, { @@ -407,20 +364,14 @@ export class NFTMethod extends BaseMethod { NftEventResult.RESULT_NFT_DOES_NOT_EXIST, ); - throw new Error('NFT substore entry does not exist'); + throw new Error('NFT does not exist'); } - const nftData = await nftStore.get(methodContext, nftID); - - if (nftData.owner.length === LENGTH_CHAIN_ID) { + if (this.isNFTEscrowed(nft)) { throw new Error('NFT is escrowed to another chain'); } - const userStore = this.stores.get(UserStore); - const userKey = userStore.getKey(nftData.owner, nftID); - const userData = await userStore.get(methodContext, userKey); - - if (userData.lockingModule === NFT_NOT_LOCKED) { + if (!this.isNFTLocked(nft)) { this.events.get(LockEvent).error( methodContext, { @@ -433,7 +384,7 @@ export class NFTMethod extends BaseMethod { throw new Error('NFT is not locked'); } - if (userData.lockingModule !== module) { + if (nft.lockingModule !== module) { this.events.get(LockEvent).error( methodContext, { @@ -446,9 +397,10 @@ export class NFTMethod extends BaseMethod { throw new Error('Unlocking NFT via module that did not lock it'); } - userData.lockingModule = NFT_NOT_LOCKED; - - await userStore.set(methodContext, userKey, userData); + const userStore = this.stores.get(UserStore); + await userStore.set(methodContext, userStore.getKey(nft.owner, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); this.events.get(LockEvent).log(methodContext, { module, @@ -462,9 +414,10 @@ export class NFTMethod extends BaseMethod { recipientAddress: Buffer, nftID: Buffer, ): Promise { - const nftStore = this.stores.get(NFTStore); - const nftExists = await nftStore.has(methodContext, nftID); - if (!nftExists) { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { this.events.get(TransferEvent).error( methodContext, { @@ -474,11 +427,11 @@ export class NFTMethod extends BaseMethod { }, NftEventResult.RESULT_NFT_DOES_NOT_EXIST, ); - throw new Error('NFT substore entry does not exist'); + + throw new Error('NFT does not exist'); } - const owner = await this.getNFTOwner(methodContext, nftID); - if (owner.length === LENGTH_CHAIN_ID) { + if (this.isNFTEscrowed(nft)) { this.events.get(TransferEvent).error( methodContext, { @@ -491,7 +444,7 @@ export class NFTMethod extends BaseMethod { throw new Error('NFT is escrowed to another chain'); } - if (!owner.equals(senderAddress)) { + if (!nft.owner.equals(senderAddress)) { this.events.get(TransferEvent).error( methodContext, { @@ -504,9 +457,7 @@ export class NFTMethod extends BaseMethod { throw new Error('Transfer not initiated by the NFT owner'); } - const userStore = this.stores.get(UserStore); - const userData = await userStore.get(methodContext, userStore.getKey(owner, nftID)); - if (userData.lockingModule !== NFT_NOT_LOCKED) { + if (this.isNFTLocked(nft)) { this.events.get(TransferEvent).error( methodContext, { @@ -563,9 +514,10 @@ export class NFTMethod extends BaseMethod { throw new Error('Data field is too long'); } - const nftStore = this.stores.get(NFTStore); - const nftExists = await nftStore.has(methodContext, nftID); - if (!nftExists) { + let nft; + try { + nft = await this.getNFT(methodContext, nftID); + } catch (error) { this.events.get(TransferCrossChainEvent).error( methodContext, { @@ -577,11 +529,11 @@ export class NFTMethod extends BaseMethod { }, NftEventResult.RESULT_NFT_DOES_NOT_EXIST, ); - throw new Error('NFT substore entry does not exist'); + + throw new Error('NFT does not exist'); } - const owner = await this.getNFTOwner(methodContext, nftID); - if (owner.length === LENGTH_CHAIN_ID) { + if (this.isNFTEscrowed(nft)) { this.events.get(TransferCrossChainEvent).error( methodContext, { @@ -612,7 +564,7 @@ export class NFTMethod extends BaseMethod { throw new Error('NFT must be native either to the sending chain or the receiving chain'); } - if (!owner.equals(senderAddress)) { + if (!nft.owner.equals(senderAddress)) { this.events.get(TransferCrossChainEvent).error( methodContext, { @@ -627,9 +579,7 @@ export class NFTMethod extends BaseMethod { throw new Error('Transfer not initiated by the NFT owner'); } - const userStore = this.stores.get(UserStore); - const userData = await userStore.get(methodContext, userStore.getKey(owner, nftID)); - if (userData.lockingModule !== NFT_NOT_LOCKED) { + if (this.isNFTLocked(nft)) { this.events.get(TransferCrossChainEvent).error( methodContext, { diff --git a/framework/src/modules/nft/types.ts b/framework/src/modules/nft/types.ts index 64ccefa1a5..9a2e8bfc37 100644 --- a/framework/src/modules/nft/types.ts +++ b/framework/src/modules/nft/types.ts @@ -13,6 +13,7 @@ */ import { ImmutableMethodContext, MethodContext } from '../../state_machine'; +import { JSONObject } from '../../types'; import { CCMsg } from '../interoperability'; export interface ModuleConfig { @@ -58,6 +59,14 @@ export interface NFTAttributes { } export interface NFT { + owner: Buffer; + attributesArray: NFTAttributes[]; + lockingModule?: string; +} + +export type NFTJSON = JSONObject; + +export interface NFTOutputEndpoint { owner: string; attributesArray: NFTAttributes[]; lockingModule?: string; diff --git a/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts b/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts index ecfb6bc4e8..78d5ab0037 100644 --- a/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts +++ b/framework/test/unit/modules/nft/cc_comands/cc_transfer.spec.ts @@ -41,6 +41,7 @@ import { CcmTransferEvent } from '../../../../../src/modules/nft/events/ccm_tran import { EscrowStore } from '../../../../../src/modules/nft/stores/escrow'; import { UserStore } from '../../../../../src/modules/nft/stores/user'; import { SupportedNFTsStore } from '../../../../../src/modules/nft/stores/supported_nfts'; +import { CCMStatusCode } from '../../../../../src/modules/interoperability/constants'; describe('CrossChain Transfer Command', () => { const module = new NFTModule(); @@ -272,7 +273,17 @@ describe('CrossChain Transfer Command', () => { await expect(command.verify(context)).rejects.toThrow('NFT has not been properly escrowed'); }); - it('should not throw if nft chain id is not equal to own chain id and no entry exists in nft substore for the nft id', async () => { + it('throw if nft chain id is not equal to own chain id and ccm status code is CCMStatusCode.MODULE_NOT_SUPPORTED', async () => { + const newCcm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCMStatusCode.MODULE_NOT_SUPPORTED, + params, + }; const newConfig = { ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), escrowAccountInitializationFee: BigInt(50000000), @@ -282,7 +293,7 @@ describe('CrossChain Transfer Command', () => { internalMethod.addDependencies(method, interopMethod); internalMethod.init(newConfig); context = { - ccm, + ccm: newCcm, transaction: defaultTransaction, header: defaultHeader, stateStore, @@ -295,7 +306,47 @@ describe('CrossChain Transfer Command', () => { }; await nftStore.del(methodContext, nftID); - await expect(command.verify(context)).resolves.toBeUndefined(); + await expect(command.verify(context)).rejects.toThrow( + 'Module or cross-chain command not supported', + ); + }); + + it('throw if nft chain id is not equal to own chain id and ccm status code is CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED', async () => { + const newCcm = { + crossChainCommand: CROSS_CHAIN_COMMAND_NAME_TRANSFER, + module: module.name, + nonce: BigInt(1), + sendingChainID, + receivingChainID, + fee: BigInt(30000), + status: CCMStatusCode.CROSS_CHAIN_COMMAND_NOT_SUPPORTED, + params, + }; + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod); + internalMethod.init(newConfig); + context = { + ccm: newCcm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).rejects.toThrow( + 'Module or cross-chain command not supported', + ); }); it('throw if nft chain id is not equal to own chain id and entry already exists in nft substore for the nft id', async () => { @@ -322,6 +373,32 @@ describe('CrossChain Transfer Command', () => { await expect(command.verify(context)).rejects.toThrow('NFT substore entry already exists'); }); + + it('should not throw if nft chain id is not equal to own chain id and no entry exists in nft substore for the nft id', async () => { + const newConfig = { + ownChainID: utils.getRandomBytes(LENGTH_CHAIN_ID), + escrowAccountInitializationFee: BigInt(50000000), + userAccountInitializationFee: BigInt(50000000), + }; + method.init(newConfig); + internalMethod.addDependencies(method, interopMethod); + internalMethod.init(newConfig); + context = { + ccm, + transaction: defaultTransaction, + header: defaultHeader, + stateStore, + contextStore, + getMethodContext, + eventQueue: new EventQueue(0), + getStore, + logger: fakeLogger, + chainID: newConfig.ownChainID, + }; + await nftStore.del(methodContext, nftID); + + await expect(command.verify(context)).resolves.toBeUndefined(); + }); }); describe('execute', () => { diff --git a/framework/test/unit/modules/nft/commands/transfer.spec.ts b/framework/test/unit/modules/nft/commands/transfer.spec.ts index b9889fa9f0..010b45c5b1 100644 --- a/framework/test/unit/modules/nft/commands/transfer.spec.ts +++ b/framework/test/unit/modules/nft/commands/transfer.spec.ts @@ -106,7 +106,7 @@ describe('Transfer command', () => { await expect( command.verify(nftIDNotExistingContext.createCommandVerifyContext(transferParamsSchema)), - ).rejects.toThrow('NFT substore entry does not exist'); + ).rejects.toThrow('NFT does not exist'); }); it('should fail if NFT is escrowed to another chain', async () => { @@ -128,11 +128,19 @@ describe('Transfer command', () => { const nftIncorrectOwnerContext = createTransactionContextWithOverridingParams({ nftID, }); + const newOwner = utils.getRandomBytes(LENGTH_ADDRESS); await nftStore.save(createStoreGetter(nftIncorrectOwnerContext.stateStore), nftID, { - owner: utils.getRandomBytes(LENGTH_ADDRESS), + owner: newOwner, attributesArray: [], }); + await userStore.set( + createStoreGetter(nftIncorrectOwnerContext.stateStore), + userStore.getKey(newOwner, nftID), + { + lockingModule: 'token', + }, + ); await expect( command.verify(nftIncorrectOwnerContext.createCommandVerifyContext(transferParamsSchema)), diff --git a/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts b/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts index 83eeb065f6..a863839d5a 100644 --- a/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts +++ b/framework/test/unit/modules/nft/commands/transfer_cross_chain.spec.ts @@ -222,7 +222,7 @@ describe('TransferCrossChainComand', () => { await expect( command.verify(context.createCommandVerifyContext(crossChainTransferParamsSchema)), - ).rejects.toThrow('NFT substore entry does not exist'); + ).rejects.toThrow('NFT does not exist'); }); it('should fail if NFT is escrowed', async () => { @@ -236,7 +236,7 @@ describe('TransferCrossChainComand', () => { }); it('should fail if NFT is not native to either the sending or receiving chain', async () => { - const nftID = utils.getRandomBytes(LENGTH_ADDRESS); + const nftID = utils.getRandomBytes(LENGTH_NFT_ID); const context = createTransactionContextWithOverridingParams({ nftID, @@ -247,9 +247,13 @@ describe('TransferCrossChainComand', () => { attributesArray: [], }); + await userStore.set(methodContext, userStore.getKey(owner, nftID), { + lockingModule: NFT_NOT_LOCKED, + }); + await expect( command.verify(context.createCommandVerifyContext(crossChainTransferParamsSchema)), - ).rejects.toThrow(''); + ).rejects.toThrow('NFT must be native to either the sending or the receiving chain'); }); it('should fail if the owner of the NFT is not the sender', async () => { @@ -258,8 +262,12 @@ describe('TransferCrossChainComand', () => { }); const nft = await nftStore.get(methodContext, existingNFT.nftID); - nft.owner = utils.getRandomBytes(LENGTH_ADDRESS); + const newOwner = utils.getRandomBytes(LENGTH_ADDRESS); + nft.owner = newOwner; await nftStore.save(methodContext, existingNFT.nftID, nft); + await userStore.set(methodContext, userStore.getKey(newOwner, existingNFT.nftID), { + lockingModule: NFT_NOT_LOCKED, + }); await expect( command.verify(context.createCommandVerifyContext(crossChainTransferParamsSchema)), diff --git a/framework/test/unit/modules/nft/endpoint.spec.ts b/framework/test/unit/modules/nft/endpoint.spec.ts index 9515ac1abf..a9dca0e4e7 100644 --- a/framework/test/unit/modules/nft/endpoint.spec.ts +++ b/framework/test/unit/modules/nft/endpoint.spec.ts @@ -319,7 +319,7 @@ describe('NFTEndpoint', () => { }, }); - await expect(endpoint.getNFT(context)).rejects.toThrow('NFT does not exist'); + await expect(endpoint.getNFT(context)).rejects.toThrow('NFT substore entry does not exist'); }); it('should return NFT details', async () => { diff --git a/framework/test/unit/modules/nft/method.spec.ts b/framework/test/unit/modules/nft/method.spec.ts index 054c601ab5..16cde615f6 100644 --- a/framework/test/unit/modules/nft/method.spec.ts +++ b/framework/test/unit/modules/nft/method.spec.ts @@ -104,7 +104,6 @@ describe('NFTMethod', () => { utils.getRandomBytes(LENGTH_CHAIN_ID), firstIndex, ]); - let owner: Buffer; const checkEventResult = ( eventQueue: EventQueue, @@ -137,7 +136,6 @@ describe('NFTMethod', () => { method.init(config); internalMethod.addDependencies(method, interopMethod); internalMethod.init(config); - owner = utils.getRandomBytes(LENGTH_ADDRESS); methodContext = createMethodContext({ stateStore: new PrefixedStateReadWriter(new InMemoryPrefixedStateDB()), @@ -215,54 +213,52 @@ describe('NFTMethod', () => { }); }); - describe('getNFTOwner', () => { - it('should fail if NFT does not exist', async () => { - await expect(method.getNFTOwner(methodContext, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', - ); + describe('isNFTEscrowed', () => { + it('should return true if nft owner is a chain', () => { + expect(method.isNFTEscrowed({ ...escrowedNFT, attributesArray: [] })).toBeTrue(); }); - it('should return the owner if NFT exists', async () => { - await nftStore.save(methodContext, nftID, { - owner, - attributesArray: [], - }); - - await expect(method.getNFTOwner(methodContext, nftID)).resolves.toEqual(owner); + it('should return false if nft owner is not a chain', () => { + expect(method.isNFTEscrowed({ ...existingNFT, attributesArray: [] })).toBeFalse(); }); }); - describe('getLockingModule', () => { + describe('getNFT', () => { it('should fail if NFT does not exist', async () => { - await expect(method.getLockingModule(methodContext, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', - ); + await expect( + method.getNFT(methodContext, utils.getRandomBytes(LENGTH_NFT_ID)), + ).rejects.toThrow('NFT substore entry does not exist'); }); - it('should fail if NFT is escrowed', async () => { - owner = utils.getRandomBytes(LENGTH_CHAIN_ID); - - await nftStore.save(methodContext, nftID, { - owner, - attributesArray: [], - }); - - await expect(method.getLockingModule(methodContext, nftID)).rejects.toThrow( - 'NFT is escrowed to another chain', + it('should fail if NFT exist but the corresponding entry in the user store does not exist', async () => { + await userStore.del(methodContext, userStore.getKey(existingNFT.owner, existingNFT.nftID)); + await expect(method.getNFT(methodContext, existingNFT.nftID)).rejects.toThrow( + 'User substore entry does not exist', ); }); - it('should return the lockingModule for the owner of the NFT', async () => { - await nftStore.save(methodContext, nftID, { - owner, + it('should return NFT details if NFT and corresponding user store entry exist', async () => { + await expect(method.getNFT(methodContext, existingNFT.nftID)).resolves.toStrictEqual({ + owner: existingNFT.owner, attributesArray: [], + lockingModule: NFT_NOT_LOCKED, }); + }); + }); - await userStore.set(methodContext, userStore.getKey(owner, nftID), { - lockingModule, - }); + describe('isNFTLocked', () => { + it('should return true if nft is locked', () => { + expect(method.isNFTLocked({ ...lockedExistingNFT, attributesArray: [] })).toBeTrue(); + }); + + it('should return false if nft does not have locking module property', () => { + expect(method.isNFTLocked({ ...existingNFT, attributesArray: [] })).toBeFalse(); + }); - await expect(method.getLockingModule(methodContext, nftID)).resolves.toEqual(lockingModule); + it('should return false if nft is locked by module NFT_NOT_LOCKED', () => { + expect( + method.isNFTLocked({ ...existingNFT, lockingModule: NFT_NOT_LOCKED, attributesArray: [] }), + ).toBeFalse(); }); }); @@ -271,7 +267,7 @@ describe('NFTMethod', () => { const address = utils.getRandomBytes(LENGTH_ADDRESS); await expect(method.destroy(methodContext, address, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', + 'NFT does not exist', ); checkEventResult( @@ -427,63 +423,6 @@ describe('NFTMethod', () => { }); }); - describe('getAttributesArray', () => { - const expectedAttributesArray = [ - { module: 'customMod1', attributes: Buffer.alloc(5) }, - { module: 'customMod2', attributes: Buffer.alloc(2) }, - ]; - - it('should throw if entry does not exist in the nft substore for the nft id', async () => { - await expect(method.getAttributesArray(methodContext, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', - ); - }); - - it('should return attributes array if entry exists in the nft substore for the nft id', async () => { - await nftStore.save(methodContext, nftID, { - owner: utils.getRandomBytes(LENGTH_CHAIN_ID), - attributesArray: expectedAttributesArray, - }); - const returnedAttributesArray = await method.getAttributesArray(methodContext, nftID); - expect(returnedAttributesArray).toStrictEqual(expectedAttributesArray); - }); - }); - - describe('getAttributes', () => { - const module1 = 'customMod1'; - const module2 = 'customMod2'; - const module3 = 'customMod3'; - const expectedAttributesArray = [ - { module: module1, attributes: Buffer.alloc(5) }, - { module: module2, attributes: Buffer.alloc(2) }, - ]; - - beforeEach(async () => { - await nftStore.save(methodContext, nftID, { - owner: utils.getRandomBytes(LENGTH_CHAIN_ID), - attributesArray: expectedAttributesArray, - }); - }); - - it('should throw if entry does not exist in the nft substore for the nft id', async () => { - await nftStore.del(methodContext, nftID); - await expect(method.getAttributes(methodContext, module1, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', - ); - }); - - it('should return attributes if entry exists in the nft substore for the nft id and attributes exists for the requested module', async () => { - const returnedAttributes = await method.getAttributes(methodContext, module1, nftID); - expect(returnedAttributes).toStrictEqual(expectedAttributesArray[0].attributes); - }); - - it('should throw if entry exists in the nft substore for the nft id but no attributes exists for the requested module', async () => { - await expect(method.getAttributes(methodContext, module3, nftID)).rejects.toThrow( - 'Specific module did not set any attributes.', - ); - }); - }); - describe('getNextAvailableIndex', () => { const attributesArray = [ { module: 'customMod1', attributes: Buffer.alloc(5) }, @@ -631,7 +570,7 @@ describe('NFTMethod', () => { it('should throw and log LockEvent if NFT does not exist', async () => { await expect(method.lock(methodContext, lockingModule, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', + 'NFT does not exist', ); checkEventResult( @@ -713,7 +652,7 @@ describe('NFTMethod', () => { describe('unlock', () => { it('should throw and log LockEvent if NFT does not exist', async () => { await expect(method.unlock(methodContext, module.name, nftID)).rejects.toThrow( - 'NFT substore entry does not exist', + 'NFT does not exist', ); checkEventResult( @@ -804,7 +743,7 @@ describe('NFTMethod', () => { it('should throw and emit error transfer event if nft does not exist', async () => { await expect( method.transfer(methodContext, senderAddress, recipientAddress, nftID), - ).rejects.toThrow('NFT substore entry does not exist'); + ).rejects.toThrow('NFT does not exist'); checkEventResult( methodContext.eventQueue, 1, @@ -948,7 +887,7 @@ describe('NFTMethod', () => { data, includeAttributes, ), - ).rejects.toThrow('NFT substore entry does not exist'); + ).rejects.toThrow('NFT does not exist'); checkEventResult( methodContext.eventQueue, 1, @@ -1856,12 +1795,9 @@ describe('NFTMethod', () => { }, NftEventResult.RESULT_SUCCESSFUL, ); - const storedAttributes = await method.getAttributes( - methodContext, - module.name, - existingNFT.nftID, - ); - expect(storedAttributes).toStrictEqual(attributes); + const storedNFT = await method.getNFT(methodContext, existingNFT.nftID); + const storedAttributes = storedNFT.attributesArray.find(a => a.module === module.name); + expect(storedAttributes?.attributes).toStrictEqual(attributes); }); it('should update attributes if NFT exists and an entry already exists for the given module', async () => { @@ -1894,12 +1830,11 @@ describe('NFTMethod', () => { }, NftEventResult.RESULT_SUCCESSFUL, ); - const storedAttributes = await method.getAttributes( - methodContext, - attributesArray1[0].module, - existingNFT.nftID, + const storedNFT = await method.getNFT(methodContext, existingNFT.nftID); + const storedAttributes = storedNFT.attributesArray.find( + a => a.module === attributesArray1[0].module, ); - expect(storedAttributes).toStrictEqual(newAttributes); + expect(storedAttributes?.attributes).toStrictEqual(newAttributes); }); }); });