Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Implement methods for NFT module (#8590)
Browse files Browse the repository at this point in the history
### What was the problem?

This PR resolves #8543 #8544 #8585 #8586 

### How was it solved?

- Implement methods `transfer` and `transferCrossChain` as specified
- Resolve circular dependency by adding interface
- Register & init commands in module

### How was it tested?

Added unit tests
  • Loading branch information
Incede authored Jun 15, 2023
2 parents 13bde5c + c4dc4ae commit b21584f
Show file tree
Hide file tree
Showing 11 changed files with 652 additions and 38 deletions.
3 changes: 3 additions & 0 deletions framework/src/modules/nft/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const EMPTY_BYTES = Buffer.alloc(0);
export const ALL_SUPPORTED_NFTS_KEY = EMPTY_BYTES;
export const FEE_CREATE_NFT = 5000000;
export const LENGTH_TOKEN_ID = 8;
export const MAX_LENGTH_DATA = 64;

export const enum NftEventResult {
RESULT_SUCCESSFUL = 0,
Expand All @@ -42,6 +43,8 @@ export const enum NftEventResult {
RESULT_RECOVER_FAIL_INVALID_INPUTS = 11,
RESULT_INSUFFICIENT_BALANCE = 12,
RESULT_DATA_TOO_LONG = 13,
INVALID_RECEIVING_CHAIN = 14,
RESULT_INVALID_ACCOUNT = 15,
}

export type NftErrorEventResult = Exclude<NftEventResult, NftEventResult.RESULT_SUCCESSFUL>;
6 changes: 5 additions & 1 deletion framework/src/modules/nft/events/recover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import { BaseEvent, EventQueuer } from '../../base_event';
import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult } from '../constants';
import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult, NftErrorEventResult } from '../constants';

export interface RecoverEventData {
terminatedChainID: Buffer;
Expand Down Expand Up @@ -50,4 +50,8 @@ export class RecoverEvent extends BaseEvent<RecoverEventData & { result: NftEven
public log(ctx: EventQueuer, data: RecoverEventData): void {
this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [data.nftID]);
}

public error(ctx: EventQueuer, data: RecoverEventData, result: NftErrorEventResult): void {
this.add(ctx, { ...data, result }, [data.nftID], true);
}
}
6 changes: 5 additions & 1 deletion framework/src/modules/nft/events/set_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import { BaseEvent, EventQueuer } from '../../base_event';
import { LENGTH_NFT_ID, NftEventResult } from '../constants';
import { LENGTH_NFT_ID, NftErrorEventResult, NftEventResult } from '../constants';

export interface SetAttributesEventData {
nftID: Buffer;
Expand Down Expand Up @@ -50,4 +50,8 @@ export class SetAttributesEvent extends BaseEvent<
public log(ctx: EventQueuer, data: SetAttributesEventData): void {
this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [data.nftID]);
}

public error(ctx: EventQueuer, data: SetAttributesEventData, result: NftErrorEventResult): void {
this.add(ctx, { ...data, result }, [data.nftID], true);
}
}
15 changes: 14 additions & 1 deletion framework/src/modules/nft/events/transfer_cross_chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

import { BaseEvent, EventQueuer } from '../../base_event';
import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult } from '../constants';
import { LENGTH_NFT_ID, LENGTH_CHAIN_ID, NftEventResult, NftErrorEventResult } from '../constants';

export interface TransferCrossChainEventData {
senderAddress: Buffer;
Expand Down Expand Up @@ -73,4 +73,17 @@ export class TransferCrossChainEvent extends BaseEvent<
data.receivingChainID,
]);
}

public error(
ctx: EventQueuer,
data: TransferCrossChainEventData,
result: NftErrorEventResult,
): void {
this.add(
ctx,
{ ...data, result },
[data.senderAddress, data.recipientAddress, data.receivingChainID],
true,
);
}
}
3 changes: 1 addition & 2 deletions framework/src/modules/nft/internal_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
import { codec } from '@liskhq/lisk-codec';
import { BaseMethod } from '../base_method';
import { NFTStore, NFTAttributes } from './stores/nft';
import { InteroperabilityMethod, ModuleConfig } from './types';
import { InteroperabilityMethod, ModuleConfig, NFTMethod } from './types';
import { MethodContext } from '../../state_machine';
import { TransferEvent } from './events/transfer';
import { UserStore } from './stores/user';
import { CROSS_CHAIN_COMMAND_NAME_TRANSFER, MODULE_NAME_NFT, NFT_NOT_LOCKED } from './constants';
import { NFTMethod } from './method';
import { EscrowStore } from './stores/escrow';
import { TransferCrossChainEvent } from './events/transfer_cross_chain';
import { CCM_STATUS_OK } from '../token/constants';
Expand Down
228 changes: 225 additions & 3 deletions framework/src/modules/nft/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
*
* Removal or modification of this copyright notice is prohibited.
*/

import { BaseMethod } from '../base_method';
import { FeeMethod, InteroperabilityMethod, ModuleConfig } from './types';
import { FeeMethod, InteroperabilityMethod, ModuleConfig, TokenMethod } from './types';
import { NFTAttributes, NFTStore } from './stores/nft';
import { ImmutableMethodContext, MethodContext } from '../../state_machine';
import {
Expand All @@ -21,6 +22,7 @@ import {
LENGTH_CHAIN_ID,
LENGTH_COLLECTION_ID,
LENGTH_NFT_ID,
MAX_LENGTH_DATA,
NFT_NOT_LOCKED,
NftEventResult,
} from './constants';
Expand All @@ -29,6 +31,9 @@ import { DestroyEvent } from './events/destroy';
import { SupportedNFTsStore } from './stores/supported_nfts';
import { CreateEvent } from './events/create';
import { LockEvent } from './events/lock';
import { TransferEvent } from './events/transfer';
import { InternalMethod } from './internal_method';
import { TransferCrossChainEvent } from './events/transfer_cross_chain';
import { AllNFTsSupportedEvent } from './events/all_nfts_supported';
import { AllNFTsSupportRemovedEvent } from './events/all_nfts_support_removed';
import { AllNFTsFromChainSupportedEvent } from './events/all_nfts_from_chain_suported';
Expand All @@ -38,17 +43,25 @@ import { AllNFTsFromChainSupportRemovedEvent } from './events/all_nfts_from_chai

export class NFTMethod extends BaseMethod {
private _config!: ModuleConfig;
// @ts-expect-error TODO: unused error. Remove when implementing.
private _interoperabilityMethod!: InteroperabilityMethod;
private _internalMethod!: InternalMethod;
private _feeMethod!: FeeMethod;
private _tokenMethod!: TokenMethod;

public init(config: ModuleConfig): void {
this._config = config;
}

public addDependencies(interoperabilityMethod: InteroperabilityMethod, feeMethod: FeeMethod) {
public addDependencies(
interoperabilityMethod: InteroperabilityMethod,
internalMethod: InternalMethod,
feeMethod: FeeMethod,
tokenMethod: TokenMethod,
) {
this._interoperabilityMethod = interoperabilityMethod;
this._internalMethod = internalMethod;
this._feeMethod = feeMethod;
this._tokenMethod = tokenMethod;
}

public getChainID(nftID: Buffer): Buffer {
Expand Down Expand Up @@ -427,6 +440,215 @@ export class NFTMethod extends BaseMethod {
});
}

public async transfer(
methodContext: MethodContext,
senderAddress: Buffer,
recipientAddress: Buffer,
nftID: Buffer,
): Promise<void> {
const nftStore = this.stores.get(NFTStore);
const nftExists = await nftStore.has(methodContext, nftID);
if (!nftExists) {
this.events.get(TransferEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
nftID,
},
NftEventResult.RESULT_NFT_DOES_NOT_EXIST,
);
throw new Error('NFT substore entry does not exist');
}

const owner = await this.getNFTOwner(methodContext, nftID);
if (owner.length === LENGTH_CHAIN_ID) {
this.events.get(TransferEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
nftID,
},
NftEventResult.RESULT_NFT_ESCROWED,
);
throw new Error('NFT is escrowed to another chain');
}

if (!owner.equals(senderAddress)) {
this.events.get(TransferEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
nftID,
},
NftEventResult.RESULT_INITIATED_BY_NONOWNER,
);
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) {
this.events.get(TransferEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
nftID,
},
NftEventResult.RESULT_NFT_LOCKED,
);
throw new Error('Locked NFTs cannot be transferred');
}

await this._internalMethod.transferInternal(methodContext, recipientAddress, nftID);
}

public async transferCrossChain(
methodContext: MethodContext,
senderAddress: Buffer,
recipientAddress: Buffer,
nftID: Buffer,
receivingChainID: Buffer,
messageFee: bigint,
data: string,
includeAttributes: boolean,
): Promise<void> {
if (data.length > MAX_LENGTH_DATA) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_DATA_TOO_LONG,
);
throw new Error('Data field is too long');
}

const nftStore = this.stores.get(NFTStore);
const nftExists = await nftStore.has(methodContext, nftID);
if (!nftExists) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_NFT_DOES_NOT_EXIST,
);
throw new Error('NFT substore entry does not exist');
}

const owner = await this.getNFTOwner(methodContext, nftID);
if (owner.length === LENGTH_CHAIN_ID) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_NFT_ESCROWED,
);
throw new Error('NFT is escrowed to another chain');
}

const nftChainID = this.getChainID(nftID);
const ownChainID = this._internalMethod.getOwnChainID();
if (![ownChainID, receivingChainID].some(allowedChainID => nftChainID.equals(allowedChainID))) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_NFT_NOT_NATIVE,
);
throw new Error('NFT must be native either to the sending chain or the receiving chain');
}

if (!owner.equals(senderAddress)) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_INITIATED_BY_NONOWNER,
);
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) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_NFT_LOCKED,
);
throw new Error('Locked NFTs cannot be transferred');
}

const messageFeeTokenID = await this._interoperabilityMethod.getMessageFeeTokenID(
methodContext,
receivingChainID,
);
const availableBalance = await this._tokenMethod.getAvailableBalance(
methodContext,
senderAddress,
messageFeeTokenID,
);
if (availableBalance < messageFee) {
this.events.get(TransferCrossChainEvent).error(
methodContext,
{
senderAddress,
recipientAddress,
receivingChainID,
nftID,
includeAttributes,
},
NftEventResult.RESULT_INSUFFICIENT_BALANCE,
);
throw new Error('Insufficient balance for the message fee');
}

await this._internalMethod.transferCrossChainInternal(
methodContext,
senderAddress,
recipientAddress,
nftID,
receivingChainID,
messageFee,
data,
includeAttributes,
);
}

public async supportAllNFTs(methodContext: MethodContext): Promise<void> {
const supportedNFTsStore = this.stores.get(SupportedNFTsStore);

Expand Down
Loading

0 comments on commit b21584f

Please sign in to comment.