MOVE 언어 기본 (렌탈)

윤따·2025년 2월 25일
0

sui 체인에서 개발을 하려면 move 언어로 개발을 해야한다.

struct

// 기본 구조체 정의
public struct MyStruct has key, store {
    id: UID,   // 고유 ID
    value: u64 // 64비트 정수값
}

public struct MyStruct → 공개 구조체 선언 (다른 모듈에서도 접근 가능)
has key, store → 구조체가 가질 수 있는 "능력(Abilities)"을 정의
id: UID → Sui에서 모든 객체는 고유한 UID를 가짐 (ID 필수!)
value: u64 → Move에서 정수형 데이터(u64)를 사용함

&Mut

Move에서 &mut는 가변 참조(mutable reference)를 나타내는 키워드

NFT 렌탈 대여를 위한 구조체

Rentable

public struct Rentables has drop {}
// Rentables 는 이름

has drop → 삭제 가능함 (더 이상 사용하지 않을 때 제거 가능)
✔ 특징:
내부에 데이터 필드가 없음 → 오직 "확장 키"로 사용됨
어떤 기능을 활성화하거나, 특정 상태를 표현할 때, 빈구조체를 사용함 빈구조체를 사용함
drop 능력만 있음 → 삭제는 가능하지만, 저장하거나 복사할 수 없음

Rented

public struct Rented has store, copy, drop { id: ID }

현재 대여 중인 NFT 를 나타냄
has 뒤에는 능력을 나타냄
차용자의 (BAG) 안에서 관리됨
NFT가 대여중인지 확인할 때 사용

Listed

public struct Listed has store, copy, drop { id: ID }

NFT가 대여 가능할 때 저장도는 구조체
NFT가 대여자로부터 등록되었을 때 관리됨
차용자가 빌릴 수 있도록 공개된 상태

Promise

Promise는 단순히 "이 NFT가 빌려졌다"라는 정보를 가지고 있을 뿐.

public struct Promise {
  item: Rented,
  duration: u64,
  start_date: u64,
  price_per_day: u64,
  renter_kiosk: address,
  borrower_kiosk: ID
}



일반 NFT 대여시 "반납 보장"을 위한 계약 정보
drop이 없음 → 이 객체는 계약이 끝나야만 제거 가능

Rentable 대여 정보 저장

public struct Rentable<T: key + store> has store {
  object: T,
  duration: u64,
  start_date: Option<u64>,
  price_per_day: u64,
  kiosk_id: ID
}

역할: NFT가 현재 대여 상태일 때 저장되는 구조체
<T: key + store> → 이 구조체는 제네릭 타입 T를 사용하며, T는 블록체인에 저장 가능해야 함
object: T → 대여 중인 NFT 객체
start_date: Option → 초기값이 없을 수도 있음

RentalPolicy (로열티 정책)

public struct RentalPolicy<phantom T> has key, store {
id: UID,
balance: Balance<SUI>,
amount_bp: u64
}

<phantomㅇ T> → 이 구조체는 특정 타입 T에 대한 정책을 의미하지만, 실제 T 값을 저장하지 않음
특징: amount_bp: u64 → Basis Point 단위로 로열티 비율 저장 (1% = 100 BP)


NFT에는 일반적으로 로열티(Royalty)나 전송 제한(Lock) 같은 규칙이 적용되지만,
렌탈 시스템에서는 크리에이터(창작자)가 별도로 설정한 정책만 적용되도록 하는 것이 적절하다는 개념을 반영한 코드

policy_cap: TransferPolicyCap:

정책을 수정할 수 있는 권한(cap)을 증명하는 객체
NFT의 원래 크리에이터(창작자)만이 이 정책을 생성하고 변경할 수 있음.

Function

Move 함수 기본 문법
Move 언어에서 함수는 fun 키워드를 사용하여 정의하며, 다음과 같은 기본 구조를 가집니다:
fun 함수이름(파라미터1: 타입, 파라미터2: 타입): 반환타입 {
// 함수 로직
}

install 함수

public fun install(
kiosk: &mut Kiosk,
cap: &KioskOwnerCap,
ctx: &mut TxContext
) {
kiosk_extension::add(Rentables {}, kiosk, cap, PERMISSIONS, ctx);
}

역할: 사용자의 kiosk에 "Rentables Extension" 을 설치함

인자: kiosk: &mut Kiosk: 설치할 키오스크의 참조.
cap: &KioskOwnerCap: 키오스크 소유자 권한을 인증하는 객체.
ctx: &mut TxContext: 트랜잭션의 컨텍스트(블록체인 내에서 상태 관리용).

실행 과정:
Kiosk의 확장 설치 기능인 kioskextension::add를 호출하여 확장 설치. > "Kiosk에 Rentables 기능을 추가하라"

remove 함수

public fun remove(kiosk: &mut Kiosk, cap: &KioskOwnerCap, _ctx: &mut TxContext) {
kiosk_extension::remove<Rentables>(kiosk, cap);
}

역할: kiosk에서 NFT 대여 확장(Kiosk Extension) 을 제거

조건:
오직 Kiosk의 소유자만 호출 가능.
확장 내에 대여 중인 아이템이 없어야 제거 가능.

remove<T.>(...)는 특정 타입 T의 확장을 제거할 수 있는 제네릭 함수인데,
remove<Rentables.>(...)라고 호출함으로써 NFT 대여 기능(Rentables)을 제거하도록 지정하는 것

setup_renting 함수 (렌탈 초기 설정)

publisher는 nft의 creater 여야함.

public fun setup_renting<T>(publisher: &Publisher, amount_bp: u64, ctx: &mut TxContext) {

// transfer_policy::new<T> 함수는 NFT의 전송 규칙을 설정하는 객체를 생성.
//transfer_policy : NFT를 주고받을 때 규칙을 정한 정책 객체
//policy_cap: 이 전송 정책을 추후 변경할 수 있는 권한을 가진 객체(수정 권한이 있는 키).

let (transfer_policy, policy_cap) = transfer_policy::new<T>(publisher, ctx);

// ProtectedTP 객체 생성 및 변수 protected_tp에 저장.
let protected_tp = ProtectedTP {
  id: object::new(ctx),
//object::new(ctx)는 Sui 블록체인에 객체 생성 및 등록.
  transfer_policy,
  policy_cap,
};

//RentalPolicy<T> 구조체는 로열티(수수료) 관련 정보를 저장.
let rental_policy = RentalPolicy<T> {
  id: object::new(ctx),
  balance: balance::zero<SUI>(),
  amount_bp,
};

transfer::share_object(protected_tp);
transfer::share_object(rental_policy);

이제 생성된 정책(protected_tp)과 수수료 정책(rental_policy)을 블록체인에서 공유함
다른 사람들이 이 정책을 사용할 수 있도록 NFT 렌탈 시스템에 등록됨
즉, "이 NFT를 렌탈할 때 적용할 정책은 이것이다"라는 것을 블록체인에 올리는 과정! 🚀

역할:
NFT를 대여(rent)할 때 필요한 두 가지 객체인
① ProtectedTP 와 ② RentalPolicy 를 생성하는 함수.

이 두 객체는 NFT 렌탈 시스템을 초기화하고, 로열티 정책 등을 정의하는 데 쓰임.

참고로 ctx는 블록체인에서 객체 생성 및 전송 시에 필수로 사용


List 함수

nft를 대여 리스트에 등록
2. key → T는 Move에서 "고유한 리소스"여야 함

public fun list<T: key + store>(
kiosk: &mut Kiosk,
cap: &KioskOwnerCap,
protected_tp: &ProtectedTP<T>,
item_id: ID,
duration: u64,
price_per_day: u64,
ctx: &mut TxContext,
) {

//NFT 렌탈 확장(Rentables)이 설치되어 있는지 체크함.
//확장이 없으면 함수가 중단됨.
assert!(kiosk_extension::is_installed<Rentables>(kiosk), EExtensionNotInstalled);

//Kiosk의 현재 소유자를 트랜잭션을 실행하는 사람으로 설정
kiosk.set_owner(cap, ctx);

//item_id를 가진 NFT를 일단 가격을 0으로 설정하여 Kiosk에 임시로 등록함.
  kiosk.list<T>(cap, item_id, 0);

//일단 NFT를 Kiosk에서 임시로 구매 처리를 함 (진짜 돈이 아니라 0원으로 구매).
NFT 소유권을 잠시 Kiosk에서 가져오는 과정임.
//NFT를 렌탈 가능 상태로 만들기 위해, 소유권을 잠시 Kiosk에서 가져와야 함.
let coin = coin::zero<SUI>(ctx);
let (object, request) = kiosk.purchase<T>(item_id, coin);


//보호된 전송 정책(ProtectedTP)을 통해, NFT의 전송을 승인 처리.
이렇게 하면 대여 리스트로 옮기기 전에 보호된 방식으로 NFT 전송 처리가 완료됨.
let (_item, _paid, _from) = protected_tp.transfer_policy.confirm_request(request);

let rentable = Rentable {
  object, // 대여할 실제 nft 객체
  duration,//대여기간
  start_date: option::none<u64>(),
  price_per_day, // 일 단위 대여 비용 
  kiosk_id: object::id(kiosk), // 현재 kiosk 의 아이디(반환할 때 필요)
};

//생성된 Rentable 객체를 Kiosk 내의 Rentables 확장 공간(Bag)에 저장.
저장할 때의 키는 item_id로 하여 나중에 아이템을 쉽게 찾을 수 있음.
place_in_bag<T, Listed>(kiosk, Listed { id: item_id }, rentable);
}

<T: key + store>
타입 매개변수 T는 고유한(key) 객체이며 저장 가능한(store) 객체여야 한다는 뜻.

매개변수:
item_id: 리스트할 NFT의 ID
duration: 빌려줄 기간
price_per_day: 하루당 가격
ctx: 트랜잭션 컨텍스트 (블록체인 상태를 변경할 때 사용)

delist 함수

사용자가 NFT를 대여 목록에서 제외하고 다시 자신의 Kiosk에 돌려놓고 싶을 때 호출.

public fun delist<T: key + store>(
  kiosk: &mut Kiosk,
  cap: &KioskOwnerCap,
  transfer_policy: &TransferPolicy<T>,
  item_id: ID,
  _ctx: &mut TxContext,
) {
  //소유자 검증
  assert!(kiosk.has_access(cap), ENotOwner);
    
  //take_from_bag: Rentables 확장(extension)의 보관함(Bag)에서 NFT를 빼옴.
  //보관함에서 NFT를 꺼내고, Rentable 객체 정보를 rentable 변수에 저장함.
  let rentable = take_from_bag<T, Listed>(kiosk, Listed { id: item_id });
    
  //Rentable 구조체 해체 (정보 추출)
  let Rentable { object, .. } = rentable;

  if (has_rule<T, LockRule>(transfer_policy)) {
    kiosk.lock(cap, transfer_policy, object);
  } else {
    kiosk.place(cap, object);
  };
} 

Rent (대여)

대여자가 올려놓은 NFT를 다른 사용자가 일정 비용을 지불하고 빌려가는 과정을 처리하는 함수

public fun rent<T: key + store>(
 renter_kiosk: &mut Kiosk, // 대여자의 키오스크
 borrower_kiosk: &mut Kiosk, // 빌리는 사람의 키오스크
 rental_policy: &mut RentalPolicy<T>,
 item_id: ID,
 mut coin: Coin<SUI>,
 clock: &Clock, // 차용자가 지불할 코인
 ctx: &mut TxContext,
) {
 assert!(kiosk_extension::is_installed<Rentables>(borrower_kiosk), EExtensionNotInstalled);
   
//대여자가 빌릴 수 있도록 올려둔(Listed) 상태의 아이템을 키오스크 보관함에서 꺼냄
//보관소(Bag)에서 Listed라는 상태로 있는 NFT를 제거해서 rentable 변수에 저장
 let mut rentable = take_from_bag<T, Listed>(renter_kiosk, Listed { id: item_id });

// 돈이 가능한지 확인
 let total_price = rentable.price_per_day * rentable.duration;
 assert!(coin.value() == total_price, ENotEnoughCoins);

// 수수료(royalty)를 계산하여 NFT 제작자에게 지급.
전체 지불한 코인에서 일부(수수료)를 떼어내 제작자의 RentalPolicy 객체에 보관함.
 let fees_amount = (total_price as u128) * (rental_policy.amount_bp as u128) / MAX_BASIS_POINTS as u128;
 let fees = coin.split(fees_amount as u64, ctx);

// 대여자에게 대여비를 전송
 coin::put(&mut rental_policy.balance, fees);
 transfer::public_transfer(coin, renter_kiosk.owner());

   
 rentable.start_date.fill(clock.timestamp_ms());
   
 //아이템을 차용자의 키오스크 내에 Rented 상태로 저장하여, 차용자가 빌린 상태임을 기록.  
 place_in_bag<T, Rented>(borrower_kiosk, Rented { id: item_id }, rentable);
}

Borrow

차용자가 대여 중인 아이템을 사용(조회)할 때, 아이템을 직접 소유하지 않고 읽기 전용으로 빌리는(참조만 가져오는) 함수

public fun borrow<T: key + store>(
kiosk: &mut Kiosk,
cap: &KioskOwnerCap,
item_id: ID,
_ctx: &mut TxContext,
): &T {
// Aborts if the cap doesn't match the Kiosk.
assert!(kiosk.has_access(cap), ENotOwner);
let ext_storage_mut = kiosk_extension::storage_mut(Rentables {}, kiosk);
let rentable: &Rentable<T> = &ext_storage_mut[Rented { id: item_id }];
&rentable.object
}

Borrow_val

NFT를 차용자(borrower)가 실제 값으로 빌려가는 상황을 처리하는 함수임.
빌린 아이템을 실제로 키오스크에서 꺼내서 임시로 빌려가고, 반드시 다시 돌려주겠다는 약속(Promise)과 함께 반환함.

public fun borrow_val<T: key + store>(
 kiosk: &mut Kiosk, //nft를 빌려가는 사람의 kiosk
 cap: &KioskOwnerCap,
 item_id: ID,
 _ctx: &mut TxContext,
): (T, Promise) {
 // Aborts if the cap doesn't match the Kiosk.
 assert!(kiosk.has_access(cap), ENotOwner);
 let borrower_kiosk = object::id(kiosk);

 let rentable = take_from_bag<T, Rented>(kiosk, Rented { id: item_id });

 // Construct a Promise struct containing the Rentable's metadata.
 let promise = Promise {
   item: Rented { id: item_id },
   duration: rentable.duration,
   start_date: *option::borrow(&rentable.start_date),
   price_per_day: rentable.price_per_day,
   renter_kiosk: rentable.kiosk_id,
   borrower_kiosk
 };

 // Deconstructs the rentable and returns the promise along with the wrapped item T.
 let Rentable {
   object,
   duration: _,
   start_date: _,
   price_per_day: _,
   kiosk_id: _,
 } = rentable;

 (object, promise)
}

The return_val function enables the borrower to return the borrowed item.

public fun return_val<T: key + store>(
 kiosk: &mut Kiosk,
 object: T,
 promise: Promise,
 _ctx: &mut TxContext,
) {
 assert!(kiosk_extension::is_installed<Rentables>(kiosk), EExtensionNotInstalled);

// 차용 약속 생성 
 let Promise {
   item,
   duration,
   start_date,
   price_per_day,
   renter_kiosk,
   borrower_kiosk,
 } = promise;

 let kiosk_id = object::id(kiosk);
 assert!(kiosk_id == borrower_kiosk, EInvalidKiosk);

 let rentable = Rentable {
   object,
   duration,
   start_date: option::some(start_date),
   price_per_day,
   kiosk_id: renter_kiosk,
 };

 place_in_bag(kiosk, item, rentable);
}

Reclaim 함수

대여기간이 끝난 NFT를 원래 소유자의 키오스크로 돌려놓는 기능

public fun reclaim<T: key + store>(
 renter_kiosk: &mut Kiosk, //원래 nft 를 소유하던 사람의 키오스크
 borrower_kiosk: &mut Kiosk, // 빌렸던 사람 키오스크
 transfer_policy: &TransferPolicy<T>,
 clock: &Clock, // 블록체인의 현재시간을 가져오는 객체
 item_id: ID,
 _ctx: &mut TxContext,
) {

 // Aborts if Rentables extension is not installed.
 assert!(kiosk_extension::is_installed<Rentables>(renter_kiosk), EExtensionNotInstalled);
   
// 대여자의 키오스크에서 빌린 아이템 꺼내기 
 let rentable = take_from_bag<T, Rented>(borrower_kiosk, Rented { id: item_id });

//Rentable 구조체 해체
 let Rentable {
   object,
   duration,
   start_date,
   price_per_day: _,
   kiosk_id,
 } = rentable;

//올바른 키오스크인지 확인 
 assert!(object::id(renter_kiosk) == kiosk_id, EInvalidKiosk);

 let start_date_ms = *option::borrow(&start_date);
 let current_timestamp = clock.timestamp_ms();
 let final_timestamp = start_date_ms + duration * SECONDS_IN_A_DAY;

 // Aborts if rental duration has not elapsed.
 assert!(current_timestamp > final_timestamp, ERentingPeriodNotOver);

 // Respects the lock rule, if present, by re-locking the asset in the owner's kiosk.
 if (transfer_policy.has_rule<T, LockRule>()) {
   kiosk_extension::lock<Rentables, T>(
     Rentables {},
     renter_kiosk,
     object,
     transfer_policy,
   );
 } else {
   kiosk_extension::place<Rentables, T>(
     Rentables {},
     renter_kiosk,
     object,
     transfer_policy,
   );
 };
}
profile
윤따와더나은인생

0개의 댓글