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

Commit

Permalink
Crosschain NFT Transfer (#8560)
Browse files Browse the repository at this point in the history
* 🏷️ Updates InteroperabilityMethod

* 🌱 NFT TransferCrossChainCommand

* ♻️ 📝 NFTErrorEventResult 🌱 DestroyEvent.error

* 🏷️ Updates InteroperabilityMethod mock defintion

* ♻️ Removes unwanted comments
  • Loading branch information
has5aan authored Jun 9, 2023
1 parent 293bb65 commit d742596
Show file tree
Hide file tree
Showing 10 changed files with 682 additions and 16 deletions.
136 changes: 136 additions & 0 deletions framework/src/modules/nft/commands/transfer_cross_chain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright © 2023 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

import { validator } from '@liskhq/lisk-validator';
import { crossChainTransferParamsSchema } from '../schemas';
import { NFTStore } from '../stores/nft';
import { NFTMethod } from '../method';
import { LENGTH_CHAIN_ID, NFT_NOT_LOCKED } from '../constants';
import { TokenMethod } from '../../token';
import { InteroperabilityMethod } from '../types';
import { BaseCommand } from '../../base_command';
import {
CommandExecuteContext,
CommandVerifyContext,
VerificationResult,
VerifyStatus,
} from '../../../state_machine';
import { InternalMethod } from '../internal_method';

export interface Params {
nftID: Buffer;
receivingChainID: Buffer;
recipientAddress: Buffer;
data: string;
messageFee: bigint;
messageFeeTokenID: Buffer;
includeAttributes: boolean;
}

export class TransferCrossChainCommand extends BaseCommand {
public schema = crossChainTransferParamsSchema;

private _nftMethod!: NFTMethod;
private _tokenMethod!: TokenMethod;
private _interoperabilityMethod!: InteroperabilityMethod;
private _internalMethod!: InternalMethod;

public init(args: {
nftMethod: NFTMethod;
tokenMethod: TokenMethod;
interoperabilityMethod: InteroperabilityMethod;
internalMethod: InternalMethod;
}): void {
this._nftMethod = args.nftMethod;
this._tokenMethod = args.tokenMethod;
this._interoperabilityMethod = args.interoperabilityMethod;
this._internalMethod = args.internalMethod;
}

public async verify(context: CommandVerifyContext<Params>): Promise<VerificationResult> {
const { params } = context;

validator.validate(this.schema, params);

const nftStore = this.stores.get(NFTStore);
const nftExists = await nftStore.has(context.getMethodContext(), params.nftID);

if (!nftExists) {
throw new Error('NFT substore entry does not exist');
}

const owner = await this._nftMethod.getNFTOwner(context.getMethodContext(), params.nftID);

if (owner.length === LENGTH_CHAIN_ID) {
throw new Error('NFT is escrowed to another chain');
}

const nftChainID = this._nftMethod.getChainID(params.nftID);

if (!nftChainID.equals(context.chainID) && !nftChainID.equals(params.receivingChainID)) {
throw new Error('NFT must be native to either the sending or the receiving chain');
}

const messageFeeTokenID = await this._interoperabilityMethod.getMessageFeeTokenID(
context.getMethodContext(),
params.receivingChainID,
);

if (!params.messageFeeTokenID.equals(messageFeeTokenID)) {
throw new Error('Mismatching message fee Token ID');
}

if (!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) {
throw new Error('Locked NFTs cannot be transferred');
}

const availableBalance = await this._tokenMethod.getAvailableBalance(
context.getMethodContext(),
context.transaction.senderAddress,
params.messageFeeTokenID,
);

if (availableBalance < params.messageFee) {
throw new Error('Insufficient balance for the message fee');
}

return {
status: VerifyStatus.OK,
};
}

public async execute(context: CommandExecuteContext<Params>): Promise<void> {
const { params } = context;

await this._internalMethod.transferCrossChainInternal(
context.getMethodContext(),
context.transaction.senderAddress,
params.recipientAddress,
params.nftID,
params.receivingChainID,
params.messageFee,
params.data,
params.includeAttributes,
);
}
}
6 changes: 2 additions & 4 deletions framework/src/modules/nft/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const CCM_STATUS_CODE_OK = 0;
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 enum NftEventResult {
RESULT_SUCCESSFUL = 0,
Expand All @@ -43,7 +44,4 @@ export const enum NftEventResult {
RESULT_DATA_TOO_LONG = 13,
}

export type NftErrorEventResult = Exclude<
NftEventResult,
NftEventResult.RESULT_NFT_ESCROWED | NftEventResult.RESULT_SUCCESSFUL
>;
export type NftErrorEventResult = Exclude<NftEventResult, NftEventResult.RESULT_SUCCESSFUL>;
15 changes: 9 additions & 6 deletions framework/src/modules/nft/events/destroy.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 DestroyEventData {
address: Buffer;
Expand Down Expand Up @@ -46,11 +46,14 @@ export const createEventSchema = {
export class DestroyEvent extends BaseEvent<DestroyEventData & { result: NftEventResult }> {
public schema = createEventSchema;

public log(
ctx: EventQueuer,
data: DestroyEventData,
result: NftEventResult = NftEventResult.RESULT_SUCCESSFUL,
): void {
public log(ctx: EventQueuer, data: DestroyEventData): void {
this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [
data.address,
data.nftID,
]);
}

public error(ctx: EventQueuer, data: DestroyEventData, result: NftErrorEventResult): void {
this.add(ctx, { ...data, result }, [data.address, data.nftID]);
}
}
8 changes: 4 additions & 4 deletions framework/src/modules/nft/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export class NFTMethod extends BaseMethod {
const nftExists = await nftStore.has(methodContext, nftID);

if (!nftExists) {
this.events.get(DestroyEvent).log(
this.events.get(DestroyEvent).error(
methodContext,
{
address,
Expand All @@ -107,7 +107,7 @@ export class NFTMethod extends BaseMethod {
const owner = await this.getNFTOwner(methodContext, nftID);

if (owner.length === LENGTH_CHAIN_ID) {
this.events.get(DestroyEvent).log(
this.events.get(DestroyEvent).error(
methodContext,
{
address,
Expand All @@ -120,7 +120,7 @@ export class NFTMethod extends BaseMethod {
}

if (!owner.equals(address)) {
this.events.get(DestroyEvent).log(
this.events.get(DestroyEvent).error(
methodContext,
{
address,
Expand All @@ -137,7 +137,7 @@ export class NFTMethod extends BaseMethod {
const { lockingModule } = await userStore.get(methodContext, userKey);

if (lockingModule !== NFT_NOT_LOCKED) {
this.events.get(DestroyEvent).log(
this.events.get(DestroyEvent).error(
methodContext,
{
address,
Expand Down
61 changes: 60 additions & 1 deletion framework/src/modules/nft/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
*/

import { MAX_DATA_LENGTH } from '../token/constants';
import { LENGTH_NFT_ID, MAX_LENGTH_MODULE_NAME, MIN_LENGTH_MODULE_NAME } from './constants';
import {
LENGTH_CHAIN_ID,
LENGTH_NFT_ID,
LENGTH_TOKEN_ID,
MAX_LENGTH_MODULE_NAME,
MIN_LENGTH_MODULE_NAME,
} from './constants';

export const transferParamsSchema = {
$id: '/lisk/nftTransferParams',
Expand Down Expand Up @@ -97,3 +103,56 @@ export interface CCTransferMessageParams {
recipientAddress: Buffer;
data: string;
}

export const crossChainTransferParamsSchema = {
$id: '/lisk/crossChainNFTTransferParamsSchema',
type: 'object',
required: [
'nftID',
'receivingChainID',
'recipientAddress',
'data',
'messageFee',
'messageFeeTokenID',
'includeAttributes',
],
properties: {
nftID: {
dataType: 'bytes',
minLength: LENGTH_NFT_ID,
maxLength: LENGTH_NFT_ID,
fieldNumber: 1,
},
receivingChainID: {
dataType: 'bytes',
minLength: LENGTH_CHAIN_ID,
maxLength: LENGTH_CHAIN_ID,
fieldNumber: 2,
},
recipientAddress: {
dataType: 'bytes',
format: 'lisk32',
fieldNumber: 3,
},
data: {
dataType: 'string',
minLength: 0,
maxLength: MAX_DATA_LENGTH,
fieldNumber: 4,
},
messageFee: {
dataType: 'uint64',
fieldNumber: 5,
},
messageFeeTokenID: {
dataType: 'bytes',
minLength: LENGTH_TOKEN_ID,
maxLength: LENGTH_TOKEN_ID,
fieldNumber: 6,
},
includeAttributes: {
dataType: 'boolean',
fieldNumber: 7,
},
},
};
3 changes: 2 additions & 1 deletion framework/src/modules/nft/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { MethodContext } from '../../state_machine';
import { ImmutableMethodContext, MethodContext } from '../../state_machine';
import { CCMsg } from '../interoperability';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand All @@ -34,6 +34,7 @@ export interface InteroperabilityMethod {
): Promise<void>;
error(methodContext: MethodContext, ccm: CCMsg, code: number): Promise<void>;
terminateChain(methodContext: MethodContext, chainID: Buffer): Promise<void>;
getMessageFeeTokenID(methodContext: ImmutableMethodContext, chainID: Buffer): Promise<Buffer>;
}

export interface FeeMethod {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ describe('CrossChain Transfer Command', () => {
send: jest.fn(),
error: jest.fn(),
terminateChain: jest.fn(),
getMessageFeeTokenID: jest.fn(),
};
const defaultHeader = {
height: 0,
Expand Down
Loading

0 comments on commit d742596

Please sign in to comment.