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

Commit

Permalink
transferCrossChainInternal Method for NFT Module (#8520)
Browse files Browse the repository at this point in the history
* 🌱 Adds EscrowStore#getKey

* 🐛 Fixes schema for DestroyEvent

* ♻️ Adds result parameter to  DestroyEvent#log

* 🌱 Adds NFTMethod.getChainID

* 🌱 Adds NFTMethod.destroy

* 🌱 Adds InternalMethod.createEscrowEntry

* ♻️ test for InternalMethod

* 🌱 Adds InternalMethod.transferCrossChainInternal

* ♻️ /nft/crossChainNFTTransferMessageParamsSchema

* ♻️ specs for NFTMethod

* ♻️ NFTMethod.destroy

* ♻️ NFTMethod.destroy consumes NFTMethod.getLockingModule

* ⏪ NFTMethod.destroy consumes NFTMethod.getLockingModule

* ✅ for NFTMethod.destroy

* ♻️ ✅ for NFTMethod.destroy

Co-authored-by: Incede <[email protected]>

---------

Co-authored-by: Incede <[email protected]>
  • Loading branch information
has5aan and Incede authored Jun 7, 2023
1 parent e9e7f2e commit 0ae50b1
Show file tree
Hide file tree
Showing 10 changed files with 824 additions and 23 deletions.
4 changes: 3 additions & 1 deletion framework/src/modules/nft/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export const LENGTH_COLLECTION_ID = 4;
export const MIN_LENGTH_MODULE_NAME = 1;
export const MAX_LENGTH_MODULE_NAME = 32;
export const LENGTH_ADDRESS = 20;
export const NFT_NOT_LOCKED = 'nft';
export const MODULE_NAME_NFT = 'nft';
export const NFT_NOT_LOCKED = MODULE_NAME_NFT;
export const CROSS_CHAIN_COMMAND_NAME_TRANSFER = 'crossChainTransfer';

export const enum NftEventResult {
RESULT_SUCCESSFUL = 0,
Expand Down
13 changes: 7 additions & 6 deletions framework/src/modules/nft/events/destroy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const createEventSchema = {
nftID: {
dataType: 'bytes',
minLength: LENGTH_NFT_ID,
maxLenght: LENGTH_NFT_ID,
maxLength: LENGTH_NFT_ID,
fieldNumber: 2,
},
result: {
Expand All @@ -46,10 +46,11 @@ export const createEventSchema = {
export class DestroyEvent extends BaseEvent<DestroyEventData & { result: NftEventResult }> {
public schema = createEventSchema;

public log(ctx: EventQueuer, data: DestroyEventData): void {
this.add(ctx, { ...data, result: NftEventResult.RESULT_SUCCESSFUL }, [
data.address,
data.nftID,
]);
public log(
ctx: EventQueuer,
data: DestroyEventData,
result: NftEventResult = NftEventResult.RESULT_SUCCESSFUL,
): void {
this.add(ctx, { ...data, result }, [data.address, data.nftID]);
}
}
91 changes: 85 additions & 6 deletions framework/src/modules/nft/internal_method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@
* Removal or modification of this copyright notice is prohibited.
*/

import { codec } from '@liskhq/lisk-codec';
import { BaseMethod } from '../base_method';
import { NFTStore, NFTAttributes } from './stores/nft';
import { InteroperabilityMethod, ModuleConfig } from './types';
import { MethodContext } from '../../state_machine';
import { TransferEvent } from './events/transfer';
import { UserStore } from './stores/user';
import { NFT_NOT_LOCKED } from './constants';
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';
import { crossChainNFTTransferMessageParamsSchema } from './schemas';

export class InternalMethod extends BaseMethod {
// @ts-expect-error TODO: unused error. Remove when implementing.
private _config!: ModuleConfig;

// @ts-expect-error TODO: unused error. Remove when implementing.
private _method!: NFTMethod;

// @ts-expect-error TODO: unused error. Remove when implementing.
private _interoperabilityMethod!: InteroperabilityMethod;

public init(config: ModuleConfig): void {
Expand All @@ -40,6 +40,16 @@ export class InternalMethod extends BaseMethod {
this._interoperabilityMethod = interoperabilityMethod;
}

public async createEscrowEntry(
methodContext: MethodContext,
receivingChainID: Buffer,
nftID: Buffer,
): Promise<void> {
const escrowStore = this.stores.get(EscrowStore);

await escrowStore.set(methodContext, escrowStore.getKey(receivingChainID, nftID), {});
}

public async createUserEntry(
methodContext: MethodContext,
address: Buffer,
Expand Down Expand Up @@ -89,4 +99,73 @@ export class InternalMethod extends BaseMethod {
nftID,
});
}

public async transferCrossChainInternal(
methodContext: MethodContext,
senderAddress: Buffer,
recipientAddress: Buffer,
nftID: Buffer,
receivingChainID: Buffer,
messageFee: bigint,
data: string,
includeAttributes: boolean,
): Promise<void> {
const chainID = this._method.getChainID(nftID);
const nftStore = this.stores.get(NFTStore);
const nft = await nftStore.get(methodContext, nftID);

if (chainID.equals(this._config.ownChainID)) {
const escrowStore = this.stores.get(EscrowStore);
const userStore = this.stores.get(UserStore);

nft.owner = receivingChainID;
await nftStore.save(methodContext, nftID, nft);

await userStore.del(methodContext, userStore.getKey(senderAddress, nftID));

const escrowExists = await escrowStore.has(
methodContext,
escrowStore.getKey(receivingChainID, nftID),
);

if (!escrowExists) {
await this.createEscrowEntry(methodContext, receivingChainID, nftID);
}
}

if (chainID.equals(receivingChainID)) {
await this._method.destroy(methodContext, senderAddress, nftID);
}

let attributesArray: { module: string; attributes: Buffer }[] = [];

if (includeAttributes) {
attributesArray = nft.attributesArray;
}

this.events.get(TransferCrossChainEvent).log(methodContext, {
senderAddress,
recipientAddress,
nftID,
receivingChainID,
includeAttributes,
});

await this._interoperabilityMethod.send(
methodContext,
senderAddress,
MODULE_NAME_NFT,
CROSS_CHAIN_COMMAND_NAME_TRANSFER,
receivingChainID,
messageFee,
CCM_STATUS_OK,
codec.encode(crossChainNFTTransferMessageParamsSchema, {
nftID,
senderAddress,
recipientAddress,
attributesArray,
data,
}),
);
}
}
90 changes: 88 additions & 2 deletions framework/src/modules/nft/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@
import { BaseMethod } from '../base_method';
import { InteroperabilityMethod, ModuleConfig } from './types';
import { NFTStore } from './stores/nft';
import { ImmutableMethodContext } from '../../state_machine';
import { LENGTH_CHAIN_ID } from './constants';
import { ImmutableMethodContext, MethodContext } from '../../state_machine';
import { LENGTH_CHAIN_ID, LENGTH_NFT_ID, NFT_NOT_LOCKED, NftEventResult } from './constants';
import { UserStore } from './stores/user';
import { DestroyEvent } from './events/destroy';

export class NFTMethod extends BaseMethod {
// @ts-expect-error TODO: unused error. Remove when implementing.
Expand All @@ -32,6 +33,14 @@ export class NFTMethod extends BaseMethod {
this._interoperabilityMethod = interoperabilityMethod;
}

public getChainID(nftID: Buffer): Buffer {
if (nftID.length !== LENGTH_NFT_ID) {
throw new Error(`NFT ID must have length ${LENGTH_NFT_ID}`);
}

return nftID.slice(0, LENGTH_CHAIN_ID);
}

public async getNFTOwner(methodContext: ImmutableMethodContext, nftID: Buffer): Promise<Buffer> {
const nftStore = this.stores.get(NFTStore);

Expand Down Expand Up @@ -61,4 +70,81 @@ export class NFTMethod extends BaseMethod {

return userData.lockingModule;
}

public async destroy(
methodContext: MethodContext,
address: Buffer,
nftID: Buffer,
): Promise<void> {
const nftStore = this.stores.get(NFTStore);

const nftExists = await nftStore.has(methodContext, nftID);

if (!nftExists) {
this.events.get(DestroyEvent).log(
methodContext,
{
address,
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(DestroyEvent).log(
methodContext,
{
address,
nftID,
},
NftEventResult.RESULT_NFT_ESCROWED,
);

throw new Error('NFT is escrowed to another chain');
}

if (!owner.equals(address)) {
this.events.get(DestroyEvent).log(
methodContext,
{
address,
nftID,
},
NftEventResult.RESULT_INITIATED_BY_NONOWNER,
);

throw new Error('Not initiated by the NFT owner');
}

const userStore = this.stores.get(UserStore);
const userKey = userStore.getKey(owner, nftID);
const { lockingModule } = await userStore.get(methodContext, userKey);

if (lockingModule !== NFT_NOT_LOCKED) {
this.events.get(DestroyEvent).log(
methodContext,
{
address,
nftID,
},
NftEventResult.RESULT_NFT_LOCKED,
);

throw new Error('Locked NFTs cannot be destroyed');
}

await nftStore.del(methodContext, nftID);

await userStore.del(methodContext, userKey);

this.events.get(DestroyEvent).log(methodContext, {
address,
nftID,
});
}
}
52 changes: 51 additions & 1 deletion framework/src/modules/nft/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*/

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

export const transferParamsSchema = {
$id: '/lisk/nftTransferParams',
Expand All @@ -39,3 +39,53 @@ export const transferParamsSchema = {
},
},
};

export const crossChainNFTTransferMessageParamsSchema = {
$id: '/lisk/crossChainNFTTransferMessageParamsSchmema',
type: 'object',
required: ['nftID', 'senderAddress', 'recipientAddress', 'attributesArray', 'data'],
properties: {
nftID: {
dataType: 'bytes',
minLength: LENGTH_NFT_ID,
maxLength: LENGTH_NFT_ID,
fieldNumber: 1,
},
senderAddress: {
dataType: 'bytes',
format: 'lisk32',
fieldNumber: 2,
},
recipientAddress: {
dataType: 'bytes',
format: 'lisk32',
fieldNumber: 3,
},
attributesArray: {
type: 'array',
fieldNumber: 4,
items: {
type: 'object',
required: ['module', 'attributes'],
properties: {
module: {
dataType: 'string',
minLength: MIN_LENGTH_MODULE_NAME,
maxLength: MAX_LENGTH_MODULE_NAME,
pattern: '^[a-zA-Z0-9]*$',
fieldNumber: 1,
},
attributes: {
dataType: 'bytes',
fieldNumber: 2,
},
},
},
},
data: {
dataType: 'string',
maxLength: MAX_DATA_LENGTH,
fieldNumber: 5,
},
},
};
4 changes: 4 additions & 0 deletions framework/src/modules/nft/stores/escrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ type EscrowStoreData = Record<string, never>;

export class EscrowStore extends BaseStore<EscrowStoreData> {
public schema = escrowStoreSchema;

public getKey(receivingChainID: Buffer, nftID: Buffer): Buffer {
return Buffer.concat([receivingChainID, nftID]);
}
}
4 changes: 3 additions & 1 deletion framework/src/modules/nft/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import { MethodContext } from '../../state_machine';
import { CCMsg } from '../interoperability';

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ModuleConfig {}
export interface ModuleConfig {
ownChainID: Buffer;
}

export interface InteroperabilityMethod {
send(
Expand Down
Loading

0 comments on commit 0ae50b1

Please sign in to comment.